Respan Dataset Explorer

Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.

5,167,182physical rows
86shards
0.00%qualified row coverage
0.00%qualified cell coverage
Random row JSON API

turns-00084.parquet:32590

fe7dc6ee1bcc842cbd312759
turn 2/5gpt-4.1-mini-2025-04-14ArabicLibya6 words
degenerate_repetitionAbsentFinal dense release
USER
مرحبا
ASSISTANT
مرحبًا! كيف يمكنني مساعدتك اليوم؟

turns-00084.parquet:32591

47bb32eede4841714110bd4b
turn 3/5gpt-4.1-mini-2025-04-14ArabicLibya506 words
degenerate_repetitionAbsentFinal dense release
USER
اريدك ان تولد ملف zip لمشروع لعبة godot 4 android  شبيهة للعبة  ل Specimen Zero 
ASSISTANT
يمكنني مساعدتك في إعداد مشروع بسيط للعبة على Godot 4 للاندرويد تكون شبيهة بلعبة Specimen Zero، وسأقدم لك الشرح والخطوات لإنشاء المشروع، ثم أوجهك لكيفية تصديره إلى ملف ZIP بنفسك لأنني لا أستطيع إنشاء ملفات مباشرة.

### أولاً: ملاحظات حول لعبة Specimen Zero
لعبة Specimen Zero هي لعبة بقاء (Survival) في بيئة مظلمة مع عناصر حركة ومهام. سأساعدك بإنشاء مشروع يحتوي على مشاهد أساسية، حركة شخصية، والتحكم باللمس (للاندرويد).

---

## خطوات إنشاء مشروع Godot 4 شبيه بـ Specimen Zero

### 1. إعداد المشروع
- افتح Godot 4.
- أنشئ مشروع جديد باسم `SpecimenZeroClone`.
- حدد مجلد المشروع في جهازك.

### 2. إنشاء مشهد اللاعب
- أنشئ مشهد جديد (`Player.tscn`) يحتوي على Node من النوع `CharacterBody3D`.
- أضف له MeshInstance3D أو نموذج أساسي (مثل Capsule) لتمثيل اللاعب.
- أضف كاميرا (Camera3D) كابن للـ Player ليكون منظور الشخص الثالث/الأول.

### 3. سكربت حركة اللاعب (`Player.gd`)

مثال بسيط لحركة 3D مع دعم للتحكم اللمسي (مفعل ضمن إعدادات المشروع للاندرويد):

```gdscript
extends CharacterBody3D

@export var speed := 5.0
@export var mouse_sensitivity := 0.3

var velocity = Vector3.ZERO
var rotation_x = 0.0

func _ready():
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

func _process(delta):
    var input_dir = Vector3.ZERO
    if Input.is_action_pressed("move_forward"):
        input_dir.z -= 1
    if Input.is_action_pressed("move_backward"):
        input_dir.z += 1
    if Input.is_action_pressed("move_left"):
        input_dir.x -= 1
    if Input.is_action_pressed("move_right"):
        input_dir.x += 1

    input_dir = input_dir.normalized()

    var direction = (transform.basis * input_dir).normalized()

    velocity.x = direction.x * speed
    velocity.z = direction.z * speed

    velocity.y += ProjectSettings.get_setting("physics/3d/default_gravity") * delta

    velocity = move_and_slide(velocity, Vector3.UP)

func _unhandled_input(event):
    if event is InputEventMouseMotion:
        rotate_y(-event.relative.x * mouse_sensitivity * 0.01)
        rotation_x = clamp(rotation_x - event.relative.y * mouse_sensitivity * 0.01, deg2rad(-80), deg2rad(80))
        $Camera3D.rotation.x = rotation_x
```

### 4. إعداد التحكم (InputMap)
- في إعدادات المشروع > Input Map أضف الحركات:
  - `move_forward` → W / Up
  - `move_backward` → S / Down
  - `move_left` → A / Left
  - `move_right` → D / Right

(Godot يحول تلقائياً حتى تعمل هذه الحركات على الأندرويد باستخدام الشاشة اللمسية مع إضافة واجهة تحكم - لمزيد من التخصيص في حالة اللمس يمكنك إضافة أزرار على الشاشة)

---

### 5. المشاهد الإضافية

- مشهد البيئة (Simple ground plane).
- اضف بعض الخصوم أو العقبات حسب المطلوب.

---

### 6. تصدير المشروع كـ APK للأندرويد

- قم بتثبيت Android SDK & NDK وتهيئة Godot للعمل مع أندرويد.
- انشئ ملف export template (إن لم يكن موجود).
- اذهب إلى Project > Export > Add Android.
- عدل الإعدادات مثل اسم الحزمة، الرمز...
- Export to APK.

---

### 7. حفظ المشروع على شكل ملف ZIP

- بعد الانتهاء من إنشاء المشروع، توجه إلى مجلد المشروع على جهازك.
- قم بتحديد كافة الملفات والمجلدات.
- اضغط كليك يمين → “Send to” → “Compressed (zipped) folder” (على ويندوز) أو استخدم أي أداة ضغط.

---

## هل تود أن أكتبه لك في شكل مشروع جاهز للنسخ؟

يمكنني تزويدك بمجلد مشاريع بسيط على شكل نص أو روابط التعليمات البرمجية مفصلة لتقوم بنسخها إلى مشروعك :)

---

هل تود أن أزودك بالمجلدات/الملفات الأساسية على شكل نصوص لنسخها؟ وهل تريد تعليمات أكثر تفصيلاً لإنشاء التحكم باللمس أو البيئة؟

turns-00084.parquet:32592

10184c6b93354434f87450fa
turn 4/5gpt-4.1-mini-2025-04-14ArabicLibya125 words
degenerate_repetitionAbsentFinal dense release
USER
اريد ملف zip
ASSISTANT
كما ذكرت سابقًا، لا أستطيع إنشاء ملف ZIP مباشر أو رفع ملفات هنا. لكن بإمكاني مساعدتك على إنشاء مشروع Godot 4 كامل خطوة بخطوة، وإعطاؤك ملفات المشروع (مشاهد، سكربتات، إعدادات) بتنسيق نص يمكنك نسخه ولصقه في ملف على جهازك، ثم تضغط على المجلد بنفسك ليصبح ZIP.

هل تود أن أبدأ بإنشاء مشروع بسيط من الصفر لك وأرسل لك الملفات (مثل `project.godot`، مشاهد `.tscn`، سكربتات `.gd`) كملفات نصية يمكنك نسخها؟

إذا نعم، فقط أخبرني، وإذا أردت يمكنني أيضًا أن أوضح لك كيف تقوم بضغط المجلد إلى ملف zip على جهازك.

---

**ملخص:**

- أرسل لك ملفات المشروع نصياً.
- تنشئ مجلد جديد في جهازك.
- تنسخ الملفات النصية داخل المجلد.
- تستخدم أداة الضغط في جهازك لتكوين ملف ZIP للمشروع.

هل تود ذلك؟

turns-00084.parquet:32593

428fa70d8251d9aa09103aa9
turn 5/5gpt-4.1-mini-2025-04-14ArabicLibya144 words
degenerate_repetitionAbsentFinal dense release
USER
ايهم اذكى gpt 4.1 mini او o4
ASSISTANT
حتى تاريخ معرفتي (يونيو 2024)، لا توجد معلومات رسمية أو موثوقة عن نسخة تسمى "GPT-4.1 mini"، لذا من الصعب المقارنة تحديدًا بينها وبين "GPT-4" (أو ما تشير إليه بـ "o4").

لكن بشكل عام:

- **GPT-4** هو نموذج كبير ومتقدم جدًا من سلسلة GPT، ويمتاز بفهم أعمق، استجابات أدق، وقدرة على التعامل مع مهام معقدة.
- إذا "GPT-4.1 mini" موجود، فمن المحتمل أنه إصدار مصغر أو مخفف من GPT-4، موجه لأداء أسرع أو استهلاك أقل للموارد، وبالتالي قد يكون أقل ذكاءً أو قدرة مقارنة النسخة الكاملة GPT-4.

لذا، بشكل عام:

> **GPT-4 كامل (o4)** هو أكثر ذكاءً وقدرة مقارنة بإصدارات مصغرة أو "mini"، التي قد تكون مخصصة لأغراض محدودة أو لتقليل تكلفة المعالجة.

---

إذا كنت تشير إلى شيء محدد أو منصة معينة تستخدم هذه المصطلحات، من فضلك قدّم لي مزيدًا من التفاصيل لأتمكن من إعطائك جواب أدق.

turns-00084.parquet:32594

cc8234f6d370ac0c5781ebbc
turn 1/1gpt-4.1-mini-2025-04-14FrenchDR Congo169 words
degenerate_repetitionAbsentFinal dense release
USER
À quoi sert l'application korn ferry
ASSISTANT
L'application Korn Ferry est généralement utilisée pour la gestion des talents, le développement des compétences, l'évaluation des employés et la planification de la succession au sein des entreprises. Korn Ferry est une société spécialisée dans le conseil en ressources humaines, notamment en recrutement, leadership, gestion des talents et développement organisationnel.

Plus précisément, l'application Korn Ferry peut permettre de :

- Réaliser des évaluations de compétences et de personnalité.
- Identifier les potentiels et les besoins en formation des collaborateurs.
- Suivre les parcours de carrière et planifier la succession.
- Faciliter le développement du leadership.
- Gérer les processus de recrutement et de gestion des performances.

Selon le contexte et la version de l'application, elle peut être utilisée par les RH, les managers ou les employés eux-mêmes pour accéder à des outils de développement professionnel.

Si vous avez une version ou un usage spécifique en tête, n’hésitez pas à me le préciser pour que je puisse vous donner une réponse plus précise !

turns-00084.parquet:32595

939aaaf8f64e54ded06dcd4d
turn 1/1gpt-4.1-mini-2025-04-14MaoriNigeria8 words
degenerate_repetitionAbsentFinal dense release
USER
Hi
ASSISTANT
Hello! How can I assist you today?

turns-00084.parquet:32596

0471e5c4859fa3fabb062e2c
turn 1/1gpt-4.1-mini-2025-04-14EnglishRussia27201 words
degenerate_repetitionAbsentFinal dense release
USER
Вот исходный код моего приложения:
import re
import json
from tkinter import *
from tkinter import ttk, filedialog, messagebox, simpledialog
from collections import defaultdict
import os
import sys

# ===== 🛡️ ЗАЩИЩЕННЫЕ АББРЕВИАТУРЫ v4.8 =====
PROTECTED_ABBREVIATIONS = {
    # Форматы сжатия видео/аудио
    'H.264', 'H.265', 'H.263', 'H.261', 'HEVC', 'AVC', 'MJPEG', 'MPEG-4', 'MPEG-2',
    'G.711', 'G.722', 'G.726', 'G.729', 'AAC', 'MP3', 'PCM', 'ADPCM',
    
    # Степени защиты  
    'IP65', 'IP66', 'IP67', 'IP68', 'IP54', 'IP44', 'IK08', 'IK10',
    
    # Стандарты и протоколы
    'TCP/IP', 'HTTP', 'HTTPS', 'FTP', 'SMTP', 'DNS', 'DHCP', 'NTP',
    'IEEE', 'USB', 'HDMI', 'VGA', 'DVI', 'PoE', 'PoE+', 'UPS',
    'Wi-Fi', 'Bluetooth', 'Zigbee', 'Z-Wave', 'LoRa', 'NFC', 'RFID',
    
    # Технические аббревиатуры
    'LED', 'LCD', 'OLED', 'TFT', 'CRT', 'IR', 'UV', 'RGB', 'CMYK',
    'CPU', 'GPU', 'RAM', 'ROM', 'SSD', 'HDD', 'API', 'SDK', 'IDE',
    'ISO', 'IEC', 'FCC', 'CE', 'RoHS', 'UL', 'CSA', 'ETL'
}

# Добавляем путь к модулям расширенной системы переводов
try:
    current_dir = os.path.dirname(os.path.abspath(__file__))
except NameError:
    # Fallback для случая выполнения через exec
    current_dir = os.getcwd()

code_dir = os.path.join(current_dir, 'code')
if code_dir not in sys.path:
    sys.path.insert(0, code_dir)

# Опциональные импорты
try:
    import pandas as pd
    PANDAS_AVAILABLE = True
except ImportError:
    print("⚠️ pandas не найден - некоторые функции могут быть недоступны")
    PANDAS_AVAILABLE = False

try:
    import pyperclip
    PYPERCLIP_AVAILABLE = True
except ImportError:
    print("⚠️ pyperclip не найден - копирование в буфер недоступно")
    PYPERCLIP_AVAILABLE = False

try:
    from bs4 import BeautifulSoup
    BEAUTIFULSOUP_AVAILABLE = True
except ImportError:
    print("⚠️ BeautifulSoup не найден - парсинг HTML ограничен")
    BEAUTIFULSOUP_AVAILABLE = False

try:
    from rapidfuzz import process, fuzz
    RAPIDFUZZ_AVAILABLE = True
except ImportError:
    print("⚠️ rapidfuzz не найден - нечеткий поиск недоступен")
    RAPIDFUZZ_AVAILABLE = False

# 🆕 ИНТЕГРАЦИЯ РАСШИРЕННОЙ СИСТЕМЫ ПЕРЕВОДОВ
print("🔧 Загрузка расширенной системы переводов...")

# Fallback импорты словарей
try:
    from translation_dict import FULL_TRANSLATION_DICT, TRANSLATION_DICT, ELECTRONICS_DICT, COMPUTER_DICT
except ImportError:
    # Fallback словарь если файл не найден
    FULL_TRANSLATION_DICT = {
        "Camera": "Камера",
        "Lens": "Объектив",
        "Video": "Изображение", 
        "Audio": "Аудио",
        "Network": "Сеть",
        "Power": "Питание",
        "Megapixel": "Мп",
        "Auto": "Авто",
        "Manual": "Ручная",
        "Supported": "Поддерживается"
    }

# Импорт расширенной системы переводов с fallback
try:
    from translation_integration import (
        IntegratedTranslationSystem,
        IntegratedTranslationDialog,
        TRANSLATION_AVAILABLE as ADVANCED_TRANSLATION_AVAILABLE,
        is_advanced_system_available
    )
    
    print("✅ Расширенная система переводов загружена")
    TRANSLATION_INTEGRATION_SUCCESS = True
    
    # Устанавливаем расширенные классы
    TranslationSystem = IntegratedTranslationSystem
    TranslationManagementDialog = IntegratedTranslationDialog
    TRANSLATION_AVAILABLE = ADVANCED_TRANSLATION_AVAILABLE
    
    if is_advanced_system_available():
        print("🌟 Полнофункциональная система переводов активирована")
        TRANSLATION_SYSTEM_TYPE = "advanced"
    else:
        print("⚙️ Базовая интегрированная система переводов активирована")
        TRANSLATION_SYSTEM_TYPE = "basic_integrated"
        
except ImportError as e:
    print(f"⚠️ Расширенная система переводов недоступна: {e}")
    print("🔄 Переключение на встроенную базовую систему...")
    TRANSLATION_INTEGRATION_SUCCESS = False
    TRANSLATION_SYSTEM_TYPE = "builtin"

    # 🌐 ВСТРОЕННАЯ БАЗОВАЯ СИСТЕМА ПЕРЕВОДОВ (FALLBACK)
    class BuiltinTranslationSystem:
        """Встроенная система переводов с основными терминами из ваших таблиц"""
        def __init__(self):
            self.auto_translate = True
            self.fuzzy_match_threshold = 80
            self.translate_values = True
            self.translate_parameters = True
            self.custom_translations = {}
            
            # Переводы на основе ваших таблиц характеристик камеры
            self.translation_dict = {
                # Основные разделы
                "Camera": "Камера",
                "Lens": "Объектив", 
                "Video": "Изображение",
                "Audio": "Аудио",
                "Network": "Сеть",
                "Alarm": "Тревога",
                "Interfaces": "Интерфейсы",
                "Environmental": "Условия эксплуатации",
                "Electrical": "Питание",
                "Mechanical": "Габариты",
                "Infrared": "ИК подсветка",
                
                # Параметры камеры
                "Image Sensor": "Матрица",
                "Pixels": "Количество пикселей",
                "Min. Illumination": "Чувствительность",
                "Shutter Speed": "Скорость затвора",
                "Day / Night": "Режим «День / Ночь»",
                "Wide Dynamic Range": "Динамический диапазон",
                "S/N Ratio": "Соотношение сигнал / шум",
                "White Balance": "Регулировка баланс белого",
                "Gain Control": "Регулировка усиления",
                "Backlight Compensation": "Улучшение изображения",
                
                # Объектив
                "Mount Type": "Крепление объектива",
                "Auto Iris": "Диафрагма",
                "Focal Length": "Фокусное расстояние",
                "Focus Control": "Фокусировка",
                "Angle of View": "Угол обзора",
                "Adjustment Range": "Диапазон поворота",
                
                # ИК подсветка
                "IR Distance": "Дальность ИК подсветки",
                "IR Intensity": "Тип ИК подсветки",
                
                # Видео
                "Compression": "Формат сжатия",
                "Image Resolution": "Разрешение",
                "Frame Rate": "Частота кадров",
                "Bitrate": "Битрейт",
                "Bitrate Control": "Тип битрейта",
                "Multi-stream": "Дополнительный поток",
                "Caption": "Отображение информации",
                "Privacy Masking": "Маска приватности",
                "Motion Detection": "Детектор движения",
                "Video Freeze": "Video Freeze",
                "ROI": "ROI",
                "Digital Noise Reduction": "Цифровое шумоподавление",
                "Digital Defog": "Антитуман",
                "Image Flip": "Режим отображения",
                
                # Аудио
                "Audio Function": "Функции аудио",
                
                # Сеть
                "Network Protocols": "Сетевые протоколы",
                "User Interface": "Поддержка браузеров",
                "Max. User Access": "Количество подключений",
                "Application Programming": "Совместимость",
                "Automatic Network Replenishment (ANR)": "Автоматическая детекция сетевого статуса (ANR)",
                
                # Тревога
                "Alarm Triggers": "Системные оповещения",
                "Intelligent": "Интеллектуальные функции",
                "Alarm Events": "Действия при тревоге",
                
                # Интерфейсы
                "Ethernet": "Сетевой интерфейс",
                "Audio In / Out": "Аудио вход / выход",
                "Control": "Последоват. интерфейс",
                "Alarm In / Out": "Тревожный вход / выход",
                "Video Out": "Видео выход",
                "Memory Slot": "Слот для карты памяти",
                
                # Условия эксплуатации
                "Operating Temperature": "Диапазон температур",
                "Operating Humidity": "Диапазон влажности",
                "Protection Level": "Уровень защиты",
                
                # Питание
                "Power": "Источник питания",
                "Power Consumption": "Потребляемая мощность",
                
                # Габариты
                "Weight": "Вес",
                "Dimensions": "Размеры",
                
                # Значения
                "Auto": "Авто",
                "Manual": "Ручная",
                "Auto / Manual": "Авто / Ручная",
                "Supported": "Поддерживается",
                "Megapixel": "Мп",
                "4.0 Megapixel": "4 Мп",
                "Digital WDR": "Цифровой WDR",
                "Smart IR": "Смарт ИК",
                "VBR / CBR": "VBR / CBR",
                "3D noise reduction": "3D DNR",
                "Bi-directional audio": "Двухсторонняя связь",
                "10/100M": "10M/100M",
                "DC12V": "12В DC",
                "PoE": "PoE",
                "Max.": "До",
                "Up to": "До",
                "fps": "к/с",
                "kbps": "кбит/с", 
                "Mbps": "Мбит/с",
                "mm": "мм",
                "kg": "кг",
                "GB": "Гб"
            }
        
        def translate_text(self, text, is_parameter=True):
            """Перевод текста с английского на русский"""
            if not text or not isinstance(text, str):
                return text
            
            # Прямой поиск
            if text in self.translation_dict:
                return self.translation_dict[text]
            
            # Поиск по частям (для составных терминов)
            words = text.split()
            if len(words) > 1:
                translated_words = []
                for word in words:
                    # Убираем знаки препинания для поиска
                    clean_word = word.strip('.,;:()[]{}/')
                    if clean_word in self.translation_dict:
                        # Сохраняем знаки препинания
                        punctuation = word[len(clean_word):]
                        translated_words.append(self.translation_dict[clean_word] + punctuation)
                    else:
                        translated_words.append(word)
                
                result = " ".join(translated_words)
                if result != text:
                    return result
            
            # 🔧 ИСПРАВЛЕНО: Поиск ЦЕЛЫХ СЛОВ (не подстрок) для предотвращения частичного перевода
            import re
            original_text = text
            for original, translation in self.translation_dict.items():
                # Создаем паттерн для поиска целого слова
                pattern = r'\b' + re.escape(original) + r'\b'
                if re.search(pattern, text, re.IGNORECASE):
                    old_text = text
                    text = re.sub(pattern, translation, text, flags=re.IGNORECASE)
                    if old_text != text:
                        print(f"🔄 Словарный перевод (целое слово): '{original}' → '{translation}' в '{old_text}' → '{text}'")
            
            return text
        
        def save_custom_translations(self):
            """Заглушка для совместимости"""
            pass
        
        def get_translation_count(self):
            """Получение количества переводов"""
            return len(self.translation_dict)

    class BuiltinTranslationDialog:
        """Встроенный диалог настроек переводов с информацией о системе"""
        def __init__(self, parent, translation_system):
            system_info = {
                'advanced': "🌟 Полнофункциональная система переводов",
                'basic_integrated': "⚙️ Базовая интегрированная система переводов", 
                'builtin': "📋 Встроенная система переводов"
            }
            
            count = getattr(translation_system, 'get_translation_count', lambda: len(getattr(translation_system, 'translation_dict', {})))()
            
            info_text = f"""{system_info.get(TRANSLATION_SYSTEM_TYPE, 'Система переводов')}

📚 Доступно переводов: {count}
🎯 Автоперевод: {'Включен' if getattr(translation_system, 'auto_translate', True) else 'Отключен'}
🔍 Нечеткий поиск: {getattr(translation_system, 'fuzzy_match_threshold', 80)}%

Основные переводы:
• Camera → Камера
• Image Sensor → Матрица  
• Motion Detection → Детектор движения
• Power Consumption → Потребляемая мощность
• 4.0 Megapixel → 4 Мп
• Supported → Поддерживается

{
'Полнофункциональный GUI редактор переводов с поиском, категориями, импортом/экспортом и статистикой.' 
if TRANSLATION_SYSTEM_TYPE == 'advanced' else
'Базовая система с автоматическим fallback. Для полного функционала установите модули расширенной системы переводов.'
if TRANSLATION_SYSTEM_TYPE == 'basic_integrated' else
'Автономная версия с базовым набором переводов. Для расширенной функциональности установите полную систему переводов.'
}"""
            
            messagebox.showinfo("🌐 Система переводов", info_text)

    # Устанавливаем fallback классы
    TranslationSystem = BuiltinTranslationSystem
    TranslationManagementDialog = BuiltinTranslationDialog
    TRANSLATION_AVAILABLE = True

# Информация о загруженной системе переводов
system_info = {
    'advanced': "🌟 Полнофункциональная система переводов готова к работе",
    'basic_integrated': "⚙️ Базовая интегрированная система переводов готова к работе", 
    'builtin': "📋 Встроенная система переводов готова к работе"
}
print(system_info.get(TRANSLATION_SYSTEM_TYPE, "✅ Система переводов готова к работе"))

from difflib import SequenceMatcher
class ParameterMergeDialog:
    """Диалог для выбора основного параметра при объединении"""
    def __init__(self, parent, parameters):
        self.result = None
        
        self.dialog = Toplevel(parent)
        self.dialog.title("Выбор основного параметра")
        self.dialog.geometry("400x300")
        self.dialog.transient(parent)
        self.dialog.grab_set()
        
        # Центрируем диалог
        self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
        
        Label(self.dialog, text="Выберите основной параметр для объединения:", font=("Arial", 10, "bold")).pack(pady=10)
        
        # Создаем список параметров
        self.listbox = Listbox(self.dialog, height=8)
        self.listbox.pack(fill=BOTH, expand=True, padx=20, pady=10)
        
        for param in parameters:
            self.listbox.insert(END, param)
        
        # Выбираем первый параметр по умолчанию
        if parameters:
            self.listbox.selection_set(0)
        
        # Кнопки
        button_frame = Frame(self.dialog)
        button_frame.pack(pady=10)
        
        Button(button_frame, text="OK", command=self.ok_clicked, width=10).pack(side=LEFT, padx=5)
        Button(button_frame, text="Отмена", command=self.cancel_clicked, width=10).pack(side=LEFT, padx=5)
        
        # Обработка Enter и Escape
        self.dialog.bind('<Return>', lambda e: self.ok_clicked())
        self.dialog.bind('<Escape>', lambda e: self.cancel_clicked())
        
        # Ждем закрытия диалога
        self.dialog.wait_window()
    
    def ok_clicked(self):
        selection = self.listbox.curselection()
        if selection:
            self.result = self.listbox.get(selection[0])
        self.dialog.destroy()
    
    def cancel_clicked(self):
        self.result = None
        self.dialog.destroy()


class ValueSynonymsManager:
    """🆕 УЛУЧШЕННЫЙ Менеджер синонимов с поддержкой callbacks v4.9"""
    def __init__(self):
        self.synonyms = {}  # {synonym: preferred_value}
        self.callbacks = []  # 🆕 Список функций для уведомления об изменениях
        self.load_synonyms()
    
    def add_callback(self, callback_func):
        """🆕 Добавляет функцию обратного вызова для уведомления об изменениях"""
        if callback_func not in self.callbacks:
            self.callbacks.append(callback_func)
            print(f"📞 Добавлен callback для синонимов: {callback_func.__name__}")
    
    def remove_callback(self, callback_func):
        """🆕 Удаляет функцию обратного вызова"""
        if callback_func in self.callbacks:
            self.callbacks.remove(callback_func)
            print(f"📞 Удален callback для синонимов: {callback_func.__name__}")
    
    def notify_callbacks(self):
        """🆕 Уведомляет все зарегистрированные функции об изменениях"""
        print(f"📞 Уведомляем {len(self.callbacks)} callbacks о изменении синонимов")
        for callback in self.callbacks:
            try:
                callback()
                print(f"✅ Callback выполнен: {callback.__name__}")
            except Exception as e:
                print(f"❌ Ошибка в callback {callback.__name__}: {e}")

    def load_synonyms(self):
        """Загружает синонимы из файла"""
        try:
            if os.path.exists("value_synonyms.json"):
                with open("value_synonyms.json", "r", encoding="utf-8") as f:
                    self.synonyms = json.load(f)
            else:
                # Создаем примеры синонимов по умолчанию
                self.synonyms = {
                    "Support": "Supported",
                    "Yes": "Supported", 
                    "Enable": "Supported",
                    "True": "Supported",
                    "No": "Not Supported",
                    "False": "Not Supported",
                    "Disable": "Not Supported"
                }
                self.save_synonyms()
        except Exception as e:
            print(f"Ошибка при загрузке синонимов: {e}")
            self.synonyms = {}
    
    def save_synonyms(self):
        """Сохраняет синонимы в файл"""
        try:
            with open("value_synonyms.json", "w", encoding="utf-8") as f:
                json.dump(self.synonyms, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"Ошибка при сохранении синонимов: {e}")
    
    def add_synonym(self, synonym, preferred):
        """🆕 Добавляет синоним и уведомляет подписчиков"""
        self.synonyms[synonym.strip()] = preferred.strip()
        self.save_synonyms()
        self.notify_callbacks()  # 🆕 Уведомляем об изменении
    
    def remove_synonym(self, synonym):
        """🆕 Удаляет синоним и уведомляет подписчиков"""
        if synonym in self.synonyms:
            del self.synonyms[synonym]
            self.save_synonyms()
            self.notify_callbacks()  # 🆕 Уведомляем об изменении
    
    def get_preferred_value(self, value):
        """🔧 ИСПРАВЛЕНО: Возвращает предпочтительное значение для синонима"""
        if not value:
            return value
        
        value = value.strip()
        
        # Прямой поиск
        if value in self.synonyms:
            result = self.synonyms[value]
            print(f"🔄 Синоним найден: '{value}' → '{result}'")
            return result
        
        # Поиск без учета регистра
        value_lower = value.lower()
        for synonym, preferred in self.synonyms.items():
            if synonym.lower() == value_lower:
                print(f"🔄 Синоним найден (регистр): '{value}' → '{preferred}'")
                return preferred
        
        return value
    
    def merge_synonyms_in_list(self, values):
        """🔧 ИСПРАВЛЕНО: Объединяет синонимы в списке значений"""
        if not values:
            return []
        
        print(f"🔄 Обработка синонимов для списка: {values}")
        
        # Применяем замены синонимов
        processed_values = []
        seen_lower = set()
        
        for value in values:
            # Очищаем значение
            cleaned_value = clean_value_enhanced(value)
            
            # Пропускаем невалидные значения
            if not is_valid_value_enhanced(cleaned_value):
                print(f"⚠️ Пропущено невалидное значение: '{value}'")
                continue
            
            # Получаем предпочтительное значение
            preferred = self.get_preferred_value(cleaned_value)
            preferred_cleaned = clean_value_enhanced(preferred)
            
            # Проверяем дубликаты без учета регистра
            preferred_lower = preferred_cleaned.lower()
            if preferred_lower not in seen_lower:
                seen_lower.add(preferred_lower)
                processed_values.append(preferred_cleaned)
                print(f"✅ Добавлено значение: '{preferred_cleaned}'")
            else:
                print(f"🔄 Дубликат пропущен: '{preferred_cleaned}'")
        
        print(f"✅ Результат обработки синонимов: {processed_values}")
        return processed_values


class ValueSynonymsDialog:
    """Диалог для настройки синонимов значений"""
    def __init__(self, parent, synonyms_manager):
        self.synonyms_manager = synonyms_manager
        self.result = False
        
        self.dialog = Toplevel(parent)
        self.dialog.title("Настройки синонимов значений")
        self.dialog.geometry("600x400")
        self.dialog.transient(parent)
        self.dialog.grab_set()
        
        # Центрируем диалог
        self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
        
        self.create_widgets()
        self.refresh_list()
        
        # Ждем закрытия диалога
        self.dialog.wait_window()
    
    def create_widgets(self):
        # Заголовок
        Label(self.dialog, text="🆕 Настройка синонимов значений (с автообновлением)", font=("Arial", 12, "bold")).pack(pady=10)
        Label(self.dialog, text="Синонимы автоматически заменяются на предпочтительные значения. Изменения применяются мгновенно!", font=("Arial", 9), fg="blue").pack(pady=5)
        
        # Основной фрейм
        main_frame = Frame(self.dialog)
        main_frame.pack(fill=BOTH, expand=True, padx=20, pady=10)
        
        # Левая часть - список синонимов
        left_frame = Frame(main_frame)
        left_frame.pack(side=LEFT, fill=BOTH, expand=True, padx=(0, 10))
        
        Label(left_frame, text="Текущие синонимы:", font=("Arial", 10, "bold")).pack(anchor=W)
        
        # Список с прокруткой
        list_frame = Frame(left_frame)
        list_frame.pack(fill=BOTH, expand=True, pady=5)
        
        scrollbar = Scrollbar(list_frame)
        scrollbar.pack(side=RIGHT, fill=Y)
        
        self.synonyms_listbox = Listbox(list_frame, yscrollcommand=scrollbar.set)
        self.synonyms_listbox.pack(side=LEFT, fill=BOTH, expand=True)
        scrollbar.config(command=self.synonyms_listbox.yview)
        
        # Кнопка удаления
        Button(left_frame, text="🗑️ Удалить выбранный", command=self.remove_synonym).pack(pady=5)
        
        # Правая часть - добавление синонимов
        right_frame = Frame(main_frame)
        right_frame.pack(side=RIGHT, fill=Y, padx=(10, 0))
        
        Label(right_frame, text="Добавить синоним:", font=("Arial", 10, "bold")).pack(anchor=W)
        
        Label(right_frame, text="Синоним (будет заменено):").pack(anchor=W, pady=(10, 0))
        self.synonym_entry = Entry(right_frame, width=25)
        self.synonym_entry.pack(fill=X, pady=2)
        
        Label(right_frame, text="Предпочтительное значение:").pack(anchor=W, pady=(10, 0))
        self.preferred_entry = Entry(right_frame, width=25)
        self.preferred_entry.pack(fill=X, pady=2)
        
        Button(right_frame, text="➕ Добавить", command=self.add_synonym).pack(pady=10)
        
        # Примеры
        examples_frame = LabelFrame(right_frame, text="Примеры", padx=5, pady=5)
        examples_frame.pack(fill=X, pady=10)
        
        Label(examples_frame, text="Support → Supported", font=("Arial", 8), fg="gray").pack(anchor=W)
        Label(examples_frame, text="Yes → Supported", font=("Arial", 8), fg="gray").pack(anchor=W)
        Label(examples_frame, text="Enable → Supported", font=("Arial", 8), fg="gray").pack(anchor=W)
        
        # 🆕 Индикатор динамических обновлений
        status_frame = LabelFrame(right_frame, text="🔄 Статус", padx=5, pady=5)
        status_frame.pack(fill=X, pady=5)
        
        Label(status_frame, text="✅ Автообновление включено", font=("Arial", 8), fg="green").pack(anchor=W)
        Label(status_frame, text="Изменения применяются сразу", font=("Arial", 8), fg="gray").pack(anchor=W)
        
        # Кнопки
        button_frame = Frame(self.dialog)
        button_frame.pack(pady=10)
        
        Button(button_frame, text="💾 Сохранить и закрыть", command=self.ok_clicked, width=15).pack(side=LEFT, padx=5)
        Button(button_frame, text="❌ Отмена", command=self.cancel_clicked, width=15).pack(side=LEFT, padx=5)
    
    def refresh_list(self):
        """Обновляет список синонимов"""
        self.synonyms_listbox.delete(0, END)
        for synonym, preferred in sorted(self.synonyms_manager.synonyms.items()):
            self.synonyms_listbox.insert(END, f"{synonym} → {preferred}")
    
    def add_synonym(self):
        """Добавляет новый синоним"""
        synonym = self.synonym_entry.get().strip()
        preferred = self.preferred_entry.get().strip()
        
        if not synonym or not preferred:
            messagebox.showwarning("Внимание", "Заполните оба поля")
            return
        
        if synonym == preferred:
            messagebox.showwarning("Внимание", "Синоним и предпочтительное значение не могут быть одинаковыми")
            return
        
        # 🆕 Добавляем синоним (автоматически триггерит динамическое обновление)
        self.synonyms_manager.add_synonym(synonym, preferred)
        self.refresh_list()
        
        self.synonym_entry.delete(0, END)
        self.preferred_entry.delete(0, END)
        
        messagebox.showinfo("✅ Обновлено", f"Синоним добавлен: {synonym} → {preferred}\n\n🔄 Основная таблица обновлена автоматически!")
    
    def remove_synonym(self):
        """Удаляет выбранный синоним"""
        selection = self.synonyms_listbox.curselection()
        if not selection:
            messagebox.showwarning("Внимание", "Выберите синоним для удаления")
            return
        
        item_text = self.synonyms_listbox.get(selection[0])
        synonym = item_text.split(" → ")[0]
        
        # 🆕 Удаляем синоним (автоматически триггерит динамическое обновление)
        self.synonyms_manager.remove_synonym(synonym)
        self.refresh_list()
        
        messagebox.showinfo("✅ Обновлено", f"Синоним удален: {synonym}\n\n🔄 Основная таблица обновлена автоматически!")
    
    def ok_clicked(self):
        self.result = True
        self.dialog.destroy()
    
    def cancel_clicked(self):
        self.result = False
        self.dialog.destroy()


def normalize_variant(val):
    val = re.sub(r'\s*\(\s*', ' (', val)
    val = re.sub(r'\s*\)\s*', ') ', val)
    val = re.sub(r'\s+', ' ', val).strip()
    return val


def normalize_value_format(val):
    """
    Стандартизирует форматирование значений:
    - Унифицирует пробелы вокруг символов (~, ×, >, <, и др.)
    - Приводит аббревиатуры к верхнему регистру
    - Добавляет пробелы между числами и единицами измерения
    """
    if not val or not isinstance(val, str):
        return val
    
    # Список единиц измерения (расширенный для IP-видеонаблюдения)
    units = [
        # Базовые физические величины
        'mm', 'cm', 'm', 'km', 'in', 'ft', 'yard', 'μm', 'nm', 'pm',  # расстояние
        'mg', 'g', 'kg', 'lb', 'oz',  # вес
        'ml', 'l', 'gal', 'pt', 'qt',  # объем
        
        # Время и частота
        's', 'ms', 'μs', 'ns', 'min', 'h', 'day',  # время
        'hz', 'khz', 'mhz', 'ghz',  # частота
        'fps',  # частота кадров
        
        # Электрические параметры
        'v', 'mv', 'kv', 'a', 'ma', 'μa', 'w', 'mw', 'kw', 'wh', 'kwh',  # электричество
        'ohm', 'ω', 'kohm', 'mohm',  # сопротивление
        'f', 'mf', 'μf', 'nf', 'pf',  # емкость
        
        # Данные и сети
        'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb',  # размер данных
        'bps', 'kbps', 'mbps', 'gbps', 'tbps',  # скорость передачи
        'bit', 'byte',  # основные единицы данных
        
        # Изображения и видео
        'mp', 'px', 'dpi', 'ppi', 'ppc',  # разрешение
        'lpi', 'lpmm',  # линейное разрешение
        
        # Звук и освещение
        'db', 'dba', 'dbc', 'dbi', 'dbm', 'dbw', 'dbfs',  # децибелы
        'lux', 'lm', 'cd', 'cd/m²', 'nit', 'fL',  # освещенность
        
        # Температура (только Цельсий!)
        '°', '°C', 'k',  # температура (убрали °f и исправили °c на °C)
        
        # Давление и влажность
        'pa', 'kpa', 'mpa', 'gpa', 'bar', 'mbar', 'psi', 'atm', 'torr', 'mmhg',  # давление
        'rh', '%rh',  # влажность
        
        # IP-видеонаблюдение специфичные
        'fov', 'aov', 'hfov', 'vfov',  # поле зрения
        'ir', 'uv',  # спектральные диапазоны
        'poe', 'poe+', 'poe++',  # питание по Ethernet
        
        # Прочие единицы
        '%', 'ppm', 'ppb', 'ppt',  # доли
        'rpm', 'rps',  # обороты
        'g-force', 'g'  # ускорение
    ]
    
    # Список распространенных аббревиатур
    abbreviations = [
        'onvif', 'poe', 'ip', 'tcp', 'udp', 'http', 'https', 'ftp', 'smtp',
        'dns', 'dhcp', 'wifi', 'lan', 'wan', 'usb', 'hdmi', 'vga', 'dvi',
        'led', 'lcd', 'oled', 'ccd', 'cmos', 'ir', 'uv', 'rgb', 'yuv',
        'pal', 'ntsc', 'mpeg', 'h264', 'h265', 'mjpeg', 'jpeg', 'png',
        'pdf', 'xml', 'json', 'api', 'sdk', 'gui', 'cli', 'os', 'ios',
        'ptz', 'dvr', 'nvr', 'hdd', 'ssd', 'ram', 'cpu', 'gpu',
        'ac', 'dc', 'rms', 'snr', 'thd', 'agc', 'awb', 'aec', 'roi',
        'rtsp', 'rtmp', 'sip', 'p2p', 'ddns', 'upnp', 'ntp', 'snmp'
    ]
    
    result = val
    
    # 1. Стандартизация пробелов вокруг специальных символов
    symbols = ['~', '×', '>', '<', '≥', '≤', '±', '÷', '−', '-', '+', '=']
    for symbol in symbols:
        # Убираем все пробелы вокруг символа, затем добавляем стандартные
        pattern = rf'\s*{re.escape(symbol)}\s*'
        result = re.sub(pattern, f' {symbol} ', result)
    
    # 2. Специальная обработка для диапазонов (чисел с единицами)
    # Например: "808nm ~ 940nm" -> "808 nm ~ 940 nm"
    for unit in units:
        # Паттерн для числа без пробела перед единицей
        pattern = rf'(\d+(?:\.\d+)?)\s*{re.escape(unit)}(?!\w)'
        replacement = rf'\1 {unit}'
        result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
    
    # 3. Обработка диапазонов типа "1/30,000s" -> "1/30,000 s"
    # Паттерн для дробей и сложных чисел с единицами
    pattern = r'(\d+(?:[,./]\d+)*)\s*([a-zA-Z]+)(?!\w)'
    def replace_number_unit(match):
        number = match.group(1)
        unit_candidate = match.group(2).lower()
        
        # Проверяем, является ли это единицей измерения
        if unit_candidate in [u.lower() for u in units]:
            return f'{number} {unit_candidate}'
        else:
            return match.group(0)  # Оставляем как есть
    
    result = re.sub(pattern, replace_number_unit, result)
    
    # 4. Приведение аббревиатур к верхнему регистру
    for abbr in abbreviations:
        # Ищем аббревиатуру как отдельное слово (с границами слов)
        pattern = rf'\b{re.escape(abbr)}\b'
        result = re.sub(pattern, abbr.upper(), result, flags=re.IGNORECASE)
    
    # 5. Обработка символа градуса с числами
    # "102,5°" -> "102,5 °", но только если нет буквы после (чтобы не трогать °C)
    result = re.sub(r'(\d+(?:[,.]\d+)?)\s*°(?![a-zA-Z])', r'\1 °', result)
    
    # 6. Убираем лишние пробелы
    result = re.sub(r'\s+', ' ', result).strip()
    
    return result


def clean_value_enhanced(value):
    """🔧 ИСПРАВЛЕНО: Улучшенная очистка значения от мусора"""
    if not value:
        return ""
    
    value = str(value).strip()
    
    # Удаляем HTML теги
    value = re.sub(r'<[^>]+>', '', value)
    
    # Удаляем лишние пробелы
    value = re.sub(r'\s+', ' ', value).strip()
    
    # 🔧 ИСПРАВЛЕНО: Удаляем некорректные значения
    if value in ['/', ';', '/;', '/', '&nbsp;', '-', '–', '—', '...', '.', ',', ':-', ':']:
        return ""
    
    # Удаляем значения состоящие только из символов-разделителей
    if re.match(r'^[/;,\s\-–—\.\:]+$', value):
        return ""
    
    return value


def is_valid_value_enhanced(value):
    """🔧 ИСПРАВЛЕНО: Проверяет, является ли значение валидным"""
    if not value:
        return False
    
    value = clean_value_enhanced(value)
    if not value:
        return False
    
    # Минимальная длина для валидного значения
    if len(value.strip()) < 1:
        return False
    
    return True


class ColumnSelectorDialog:
    def __init__(self, parent, columns, selected_columns=None):
        self.parent = parent
        self.columns = columns
        self.selected_columns = selected_columns or []
        self.result = None
        
        # Создаем модальное окно
        self.dialog = Toplevel(parent)
        self.dialog.title("Выбор дополнительных столбцов")
        self.dialog.geometry("500x600")
        self.dialog.grab_set()
        self.dialog.transient(parent)
        
        # Центрируем окно
        self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
        
        # Создаем интерфейс
        self.setup_ui()
        
    def setup_ui(self):
        # Заголовок
        title_frame = Frame(self.dialog)
        title_frame.pack(fill=X, padx=10, pady=10)
        
        Label(title_frame, text="Выбор дополнительных столбцов", font=("Arial", 12, "bold")).pack()
        Label(title_frame, text="Выберите столбцы из исходного файла для включения в результат", 
              font=("Arial", 9)).pack(pady=(5, 0))
        
        # Индикатор выбранных столбцов
        self.status_label = Label(title_frame, text="", font=("Arial", 9), fg="blue")
        self.status_label.pack(pady=(5, 0))
        
        # Фрейм с чекбоксами и скроллбаром
        frame = Frame(self.dialog)
        frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        canvas = Canvas(frame)
        scrollbar = Scrollbar(frame, orient="vertical", command=canvas.yview)
        scrollable_frame = Frame(canvas)
        
        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        # Словарь для хранения переменных чекбосов
        self.check_vars = {}
        
        # Создаем чекбоксы для каждого столбца
        for i, col in enumerate(self.columns, 1):
            var = BooleanVar()
            # Устанавливаем состояние чекбокса если столбец был выбран ранее
            if col in self.selected_columns:
                var.set(True)
            
            # Добавляем callback для обновления статуса
            var.trace('w', self.update_status)
            
            self.check_vars[col] = var
            cb = Checkbutton(scrollable_frame, text=f"{i}. {col}", variable=var, anchor=W, justify=LEFT)
            cb.pack(fill=X, padx=5, pady=1)
        
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # Кнопки управления
        btn_frame = Frame(self.dialog)
        btn_frame.pack(fill=X, padx=10, pady=5)
        
        Button(btn_frame, text="Выбрать все", command=self.select_all).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Снять все", command=self.deselect_all).pack(side=LEFT, padx=5)
        
        # Кнопки OK/Отмена
        btn_frame2 = Frame(self.dialog)
        btn_frame2.pack(fill=X, padx=10, pady=10)
        
        Button(btn_frame2, text="ОК", command=self.ok_clicked, width=10).pack(side=RIGHT, padx=5)
        Button(btn_frame2, text="Отмена", command=self.cancel_clicked, width=10).pack(side=RIGHT)
        
        # Обновляем статус при запуске
        self.update_status()
        
        # Обработка клавиш
        self.dialog.bind('<Return>', lambda e: self.ok_clicked())
        self.dialog.bind('<Escape>', lambda e: self.cancel_clicked())
        
    def update_status(self, *args):
        """Обновляет индикатор количества выбранных столбцов"""
        selected_count = sum(1 for var in self.check_vars.values() if var.get())
        total_count = len(self.check_vars)
        self.status_label.config(text=f"Выбрано столбцов: {selected_count} из {total_count}")
        
    def select_all(self):
        """Выбрать все столбцы"""
        for var in self.check_vars.values():
            var.set(True)
            
    def deselect_all(self):
        """Снять выбор со всех столбцов"""
        for var in self.check_vars.values():
            var.set(False)
        
    def ok_clicked(self):
        self.selected_columns = [col for col, var in self.check_vars.items() if var.get()]
        self.result = self.selected_columns
        self.dialog.destroy()
        
    def cancel_clicked(self):
        self.result = None
        self.dialog.destroy()


class ToolTip:
    def __init__(self, widget, text):
        self.widget = widget
        self.text = text
        self.tipwindow = None
        self.id = None
        self.x = self.y = 0

        self.widget.bind('<Enter>', self.enter)
        self.widget.bind('<Leave>', self.leave)

    def enter(self, event=None):
        if self.tipwindow:
            return
        x, y, cx, cy = self.widget.bbox("insert")
        x = x + self.widget.winfo_rootx() + 25
        y = y + cy + self.widget.winfo_rooty() + 25
        self.tipwindow = tw = Toplevel(self.widget)
        tw.wm_overrideredirect(1)
        tw.wm_geometry("+%d+%d" % (x, y))
        label = Label(tw, text=self.text, justify=LEFT,
                      background="#ffffe0", relief=SOLID, borderwidth=1,
                      font=("tahoma", "8", "normal"))
        label.pack(ipadx=1)

    def leave(self, event=None):
        tw = self.tipwindow
        self.tipwindow = None
        if tw:
            tw.destroy()


class CaseNormalizationDialog:
    def __init__(self, parent, case_normalization, save_callback):
        self.parent = parent
        self.case_normalization = case_normalization.copy()
        self.save_callback = save_callback
        
        self.top = Toplevel(parent)
        self.top.title("Управление нормализацией регистра")
        self.top.geometry("800x600")
        self.top.grab_set()
        
        # Основной фрейм
        main_frame = Frame(self.top)
        main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
        
        # Инструкция
        info_text = """Эта функция позволяет установить предпочтительный регистр для значений.
Варианты в разном регистре (например "Support", "SUPPORT", "support") будут считаться одинаковыми.
Добавьте значение в том регистре, который должен использоваться в итоговой таблице."""
        
        info_label = Label(main_frame, text=info_text, justify=LEFT, wraplength=750)
        info_label.pack(pady=(0, 10))
        
        # Фрейм для добавления новых значений
        add_frame = LabelFrame(main_frame, text="Добавить новое значение")
        add_frame.pack(fill=X, pady=(0, 10))
        
        Label(add_frame, text="Значение:").grid(row=0, column=0, sticky=W, padx=5, pady=5)
        self.new_value_entry = Entry(add_frame, width=40)
        self.new_value_entry.grid(row=0, column=1, sticky=W+E, padx=5, pady=5)
        
        self.add_button = Button(add_frame, text="Добавить", command=self.add_value)
        self.add_button.grid(row=0, column=2, padx=5, pady=5)
        
        add_frame.grid_columnconfigure(1, weight=1)
        
        # Таблица существующих значений
        table_frame = LabelFrame(main_frame, text="Существующие значения")
        table_frame.pack(fill=BOTH, expand=True)
        
        # Создаем Treeview с прокруткой
        tree_frame = Frame(table_frame)
        tree_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
        
        self.tree = ttk.Treeview(tree_frame, columns=('value',), show='headings')
        self.tree.heading('value', text='Предпочтительный регистр')
        self.tree.column('value', width=300)
        
        vsb = Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=vsb.set)
        
        self.tree.pack(side=LEFT, fill=BOTH, expand=True)
        vsb.pack(side=RIGHT, fill=Y)
        
        # Заполняем таблицу
        self.refresh_tree()
        
        # Привязываем события
        self.tree.bind('<Delete>', self.delete_selected)
        self.tree.bind('<Double-1>', self.edit_selected)
        self.new_value_entry.bind('<Return>', lambda e: self.add_value())
        
        # Кнопки управления
        btn_frame = Frame(table_frame)
        btn_frame.pack(fill=X, padx=5, pady=5)
        
        Button(btn_frame, text="Редактировать", command=self.edit_selected).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Удалить", command=self.delete_selected).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Очистить все", command=self.clear_all).pack(side=LEFT, padx=15)
        
        # Кнопки диалога
        dialog_btn_frame = Frame(self.top)
        dialog_btn_frame.pack(fill=X, pady=10)
        
        Button(dialog_btn_frame, text="Сохранить", command=self.save_and_close).pack(side=RIGHT, padx=5)
        Button(dialog_btn_frame, text="Отмена", command=self.top.destroy).pack(side=RIGHT)
        
    def refresh_tree(self):
        # Очищаем дерево
        for item in self.tree.get_children():
            self.tree.delete(item)
        
        # Заполняем значениями из словаря
        for value in sorted(self.case_normalization.keys()):
            self.tree.insert('', END, values=(value,))
    
    def add_value(self):
        value = self.new_value_entry.get().strip()
        if not value:
            return
        
        # Добавляем значение в словарь (само на себя)
        self.case_normalization[value] = value
        
        # Обновляем таблицу
        self.refresh_tree()
        
        # Очищаем поле ввода
        self.new_value_entry.delete(0, END)
    
    def edit_selected(self, event=None):
        selection = self.tree.selection()
        if not selection:
            return
        
        item = selection[0]
        old_value = self.tree.item(item, 'values')[0]
        
        # Диалог редактирования
        new_value = simpledialog.askstring("Редактирование", 
                                          "Введите новое значение:", 
                                          initialvalue=old_value)
        if new_value and new_value.strip():
            new_value = new_value.strip()
            # Удаляем старое и добавляем новое
            del self.case_normalization[old_value]
            self.case_normalization[new_value] = new_value
            self.refresh_tree()
    
    def delete_selected(self, event=None):
        selection = self.tree.selection()
        if not selection:
            return
        
        for item in selection:
            value = self.tree.item(item, 'values')[0]
            if value in self.case_normalization:
                del self.case_normalization[value]
        
        self.refresh_tree()
    
    def clear_all(self):
        if messagebox.askyesno("Подтверждение", "Удалить все значения?"):
            self.case_normalization.clear()
            self.refresh_tree()
    
    def save_and_close(self):
        self.save_callback(self.case_normalization)
        self.top.destroy()


class ValueTranslationRuleDialog:
    """Диалог для создания/редактирования правила перевода значений"""
    def __init__(self, parent, rule=None):
        self.parent = parent
        self.result = None
        
        # Создаем модальное окно
        self.dialog = Toplevel(parent)
        self.dialog.title("Правило перевода значений" if not rule else "Редактирование правила")
        self.dialog.geometry("600x400")
        self.dialog.transient(parent)
        self.dialog.grab_set()
        
        # Центрируем диалог
        self.dialog.geometry("+%d+%d" % (parent.winfo_rootx() + 100, parent.winfo_rooty() + 100))
        
        # Инициализируем данные
        self.rule_data = rule.copy() if rule else {
            "active": True,
            "original": "",
            "translation": "",
            "category": "Пользовательские",
            "description": "",
            "examples": ""
        }
        
        self.create_widgets()
        self.load_data()
        
        # Ждем закрытия диалога
        self.dialog.wait_window()
    
    def create_widgets(self):
        """Создает виджеты диалога"""
        main_frame = Frame(self.dialog)
        main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
        
        # Заголовок
        header_frame = Frame(main_frame)
        header_frame.pack(fill=X, pady=(0, 15))
        
        Label(header_frame, text="🌐 Настройка правила перевода значений", 
              font=("Arial", 12, "bold")).pack()
        Label(header_frame, text="Создайте правило для автоматического перевода значений в столбце 'Результат разделения'",
              font=("Arial", 9), fg="gray").pack()
        
        # Основные поля
        fields_frame = LabelFrame(main_frame, text="📝 Основные параметры", font=("Arial", 10, "bold"))
        fields_frame.pack(fill=X, pady=(0, 10))
        
        # Активность
        active_frame = Frame(fields_frame)
        active_frame.pack(fill=X, padx=10, pady=5)
        
        self.active_var = BooleanVar(value=True)
        Checkbutton(active_frame, text="✓ Правило активно", variable=self.active_var,
                   font=("Arial", 10)).pack(anchor=W)
        
        # Исходное значение
        orig_frame = Frame(fields_frame)
        orig_frame.pack(fill=X, padx=10, pady=5)
        
        Label(orig_frame, text="Исходное значение:", width=20, anchor=W).pack(side=LEFT)
        self.original_entry = Entry(orig_frame, font=("Arial", 10))
        self.original_entry.pack(side=LEFT, fill=X, expand=True, padx=(5, 0))
        
        # Перевод
        trans_frame = Frame(fields_frame)
        trans_frame.pack(fill=X, padx=10, pady=5)
        
        Label(trans_frame, text="Перевод:", width=20, anchor=W).pack(side=LEFT)
        self.translation_entry = Entry(trans_frame, font=("Arial", 10))
        self.translation_entry.pack(side=LEFT, fill=X, expand=True, padx=(5, 0))
        
        # Категория
        cat_frame = Frame(fields_frame)
        cat_frame.pack(fill=X, padx=10, pady=5)
        
        Label(cat_frame, text="Категория:", width=20, anchor=W).pack(side=LEFT)
        self.category_var = StringVar()
        self.category_combo = ttk.Combobox(cat_frame, textvariable=self.category_var, 
                                         values=["Длина", "Вес", "Электричество", "Частота", 
                                               "Освещенность", "Термины", "Логические", "Пользовательские"],
                                         font=("Arial", 10))
        self.category_combo.pack(side=LEFT, fill=X, expand=True, padx=(5, 0))
        
        # Дополнительные поля
        details_frame = LabelFrame(main_frame, text="📋 Дополнительная информация", font=("Arial", 10, "bold"))
        details_frame.pack(fill=BOTH, expand=True, pady=(0, 10))
        
        # Описание
        desc_frame = Frame(details_frame)
        desc_frame.pack(fill=X, padx=10, pady=5)
        
        Label(desc_frame, text="Описание:", anchor=W).pack(anchor=W)
        self.description_entry = Entry(desc_frame, font=("Arial", 10))
        self.description_entry.pack(fill=X, pady=(2, 0))
        
        # Примеры
        examples_frame = Frame(details_frame)
        examples_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        Label(examples_frame, text="Примеры использования:", anchor=W).pack(anchor=W)
        
        text_frame = Frame(examples_frame)
        text_frame.pack(fill=BOTH, expand=True, pady=(2, 0))
        
        self.examples_text = Text(text_frame, height=4, font=("Arial", 9))
        examples_scroll = Scrollbar(text_frame, orient=VERTICAL, command=self.examples_text.yview)
        self.examples_text.configure(yscrollcommand=examples_scroll.set)
        
        self.examples_text.pack(side=LEFT, fill=BOTH, expand=True)
        examples_scroll.pack(side=RIGHT, fill=Y)
        
        # Кнопки
        buttons_frame = Frame(main_frame)
        buttons_frame.pack(fill=X, pady=(10, 0))
        
        Button(buttons_frame, text="💾 Сохранить", command=self.save_rule,
               bg="#E8F5E8", font=("Arial", 10, "bold")).pack(side=RIGHT, padx=(5, 0))
        
        Button(buttons_frame, text="❌ Отмена", command=self.cancel,
               bg="#FFE6E6", font=("Arial", 10)).pack(side=RIGHT, padx=(5, 5))
        
        # Кнопка помощи
        Button(buttons_frame, text="❓ Помощь", command=self.show_help,
               bg="#FFF4E6", font=("Arial", 10)).pack(side=LEFT)
        
        # Привязки событий
        self.original_entry.bind('<KeyRelease>', self.auto_generate_example)
        self.translation_entry.bind('<KeyRelease>', self.auto_generate_example)
    
    def load_data(self):
        """Загружает данные в поля"""
        self.active_var.set(self.rule_data.get("active", True))
        self.original_entry.insert(0, self.rule_data.get("original", ""))
        self.translation_entry.insert(0, self.rule_data.get("translation", ""))
        self.category_var.set(self.rule_data.get("category", "Пользовательские"))
        self.description_entry.insert(0, self.rule_data.get("description", ""))
        self.examples_text.insert("1.0", self.rule_data.get("examples", ""))
    
    def auto_generate_example(self, event=None):
        """Автоматически генерирует пример при вводе"""
        original = self.original_entry.get().strip()
        translation = self.translation_entry.get().strip()
        
        if original and translation:
            # Генерируем пример
            if re.match(r'^[a-zA-Z]+$', original):  # Если это единица измерения
                example = f"100{original} → 100 {translation}"
            else:
                example = f"{original} → {translation}"
            
            # Обновляем поле примеров если оно пустое
            current_examples = self.examples_text.get("1.0", "end-1c").strip()
            if not current_examples:
                self.examples_text.delete("1.0", END)
                self.examples_text.insert("1.0", example)
    
    def save_rule(self):
        """Сохраняет правило"""
        # Валидация
        original = self.original_entry.get().strip()
        translation = self.translation_entry.get().strip()
        
        if not original:
            messagebox.showerror("Ошибка", "Введите исходное значение")
            self.original_entry.focus()
            return
        
        if not translation:
            messagebox.showerror("Ошибка", "Введите перевод")
            self.translation_entry.focus()
            return
        
        # Сохраняем данные
        self.result = {
            "active": self.active_var.get(),
            "original": original,
            "translation": translation,
            "category": self.category_var.get() or "Пользовательские",
            "description": self.description_entry.get().strip() or f"Перевод '{original}' → '{translation}'",
            "examples": self.examples_text.get("1.0", "end-1c").strip() or f"{original} → {translation}"
        }
        
        self.dialog.destroy()
    
    def cancel(self):
        """Отменяет изменения"""
        self.result = None
        self.dialog.destroy()
    
    def show_help(self):
        """Показывает справку"""
        help_text = """🌐 Справка по правилам перевода значений

📋 Основные поля:
• Исходное значение - текст, который нужно переводить (например: "mm", "Support")
• Перевод - на что заменять (например: "мм", "Поддержка")  
• Категория - группировка правил для удобства

📝 Принципы работы:
• Правила применяются к значениям в столбце "Результат разделения"
• Поиск выполняется точно по тексту (с учетом регистра)
• Неактивные правила игнорируются

💡 Примеры использования:
• Единицы: "mm" → "мм", "kg" → "кг"
• Термины: "Auto" → "Автоматически" 
• Логические: "Yes" → "Да", "No" → "Нет"

🔍 Автопоиск единиц:
Используйте кнопку "Найти единицы" для автоматического обнаружения 
единиц измерения в ваших данных."""
        
        messagebox.showinfo("Справка", help_text)


class ReplacementRulesEditorDialog:
    def __init__(self, parent, replacement_rules, save_callback):
        self.parent = parent
        self.replacement_rules = replacement_rules.copy()
        self.save_callback = save_callback
        
        self.top = Toplevel(parent)
        self.top.title("Редактор правил замены")
        self.top.geometry("900x700")
        self.top.grab_set()
        
        # Основной фрейм
        main_frame = Frame(self.top)
        main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
        
        # Инструкция
        info_text = """Правила замены позволяют автоматически изменять значения с помощью регулярных выражений.
Используйте Python-синтаксис для регулярных выражений.
Примеры:
- Удаление скобок: Паттерн: \\([^\\)]*\\)  Замена: (пустая строка)
- Замена "Yes" на "Да": Паттерн: \\bYes\\b  Замена: Да
- Нормализация пробелов: Паттерн: \\s+  Замена: (один пробел)"""
        
        info_label = Label(main_frame, text=info_text, justify=LEFT, wraplength=850)
        info_label.pack(pady=(0, 10))
        
        # Фрейм для добавления нового правила
        add_frame = LabelFrame(main_frame, text="Добавить новое правило")
        add_frame.pack(fill=X, pady=(0, 10))
        
        Label(add_frame, text="Название:").grid(row=0, column=0, sticky=W, padx=5, pady=2)
        self.name_entry = Entry(add_frame, width=30)
        self.name_entry.grid(row=0, column=1, sticky=W+E, padx=5, pady=2)
        
        Label(add_frame, text="Активно:").grid(row=0, column=2, sticky=W, padx=5, pady=2)
        self.active_var = BooleanVar(value=True)
        self.active_check = Checkbutton(add_frame, variable=self.active_var)
        self.active_check.grid(row=0, column=3, sticky=W, padx=5, pady=2)
        
        Label(add_frame, text="Паттерн (regex):").grid(row=1, column=0, sticky=W, padx=5, pady=2)
        self.pattern_entry = Entry(add_frame, width=60)
        self.pattern_entry.grid(row=1, column=1, columnspan=3, sticky=W+E, padx=5, pady=2)
        
        Label(add_frame, text="Замена:").grid(row=2, column=0, sticky=W, padx=5, pady=2)
        self.replacement_entry = Entry(add_frame, width=60)
        self.replacement_entry.grid(row=2, column=1, columnspan=3, sticky=W+E, padx=5, pady=2)
        
        self.add_button = Button(add_frame, text="Добавить правило", command=self.add_rule)
        self.add_button.grid(row=3, column=0, columnspan=4, pady=10)
        
        add_frame.grid_columnconfigure(1, weight=1)
        
        # Таблица существующих правил
        table_frame = LabelFrame(main_frame, text="Существующие правила")
        table_frame.pack(fill=BOTH, expand=True)
        
        # Создаем Treeview с прокруткой
        tree_frame = Frame(table_frame)
        tree_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
        
        columns = ('active', 'name', 'pattern', 'replacement')
        self.tree = ttk.Treeview(tree_frame, columns=columns, show='headings')
        self.tree.heading('active', text='Активно')
        self.tree.heading('name', text='Название')
        self.tree.heading('pattern', text='Паттерн')
        self.tree.heading('replacement', text='Замена')
        
        self.tree.column('active', width=60, anchor=CENTER)
        self.tree.column('name', width=150)
        self.tree.column('pattern', width=300)
        self.tree.column('replacement', width=200)
        
        vsb = Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=vsb.set)
        
        self.tree.pack(side=LEFT, fill=BOTH, expand=True)
        vsb.pack(side=RIGHT, fill=Y)
        
        # Заполняем таблицу
        self.refresh_tree()
        
        # Привязываем события
        self.tree.bind('<Delete>', self.delete_selected)
        self.tree.bind('<Double-1>', self.edit_selected)
        self.tree.bind('<Button-1>', self.on_click)
        
        # Кнопки управления
        btn_frame = Frame(table_frame)
        btn_frame.pack(fill=X, padx=5, pady=5)
        
        Button(btn_frame, text="Редактировать", command=self.edit_selected).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Удалить", command=self.delete_selected).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Переместить вверх", command=self.move_up).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Переместить вниз", command=self.move_down).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Очистить все", command=self.clear_all).pack(side=LEFT, padx=15)
        
        # 🆕 Дополнительные кнопки
        btn_frame2 = Frame(table_frame)
        btn_frame2.pack(fill=X, padx=5, pady=5)
        
        Button(btn_frame2, text="📥 Импорт правил", command=self.import_rules).pack(side=LEFT, padx=5)
        Button(btn_frame2, text="📤 Экспорт правил", command=self.export_rules).pack(side=LEFT, padx=5)
        
        # Кнопка тестирования
        test_frame = Frame(table_frame)
        test_frame.pack(fill=X, padx=5, pady=5)
        
        Label(test_frame, text="Тестовая строка:").pack(side=LEFT)
        self.test_entry = Entry(test_frame, width=40)
        self.test_entry.pack(side=LEFT, padx=5, fill=X, expand=True)
        Button(test_frame, text="Тестировать", command=self.test_rules).pack(side=LEFT, padx=5)
        
        # Кнопки диалога
        dialog_btn_frame = Frame(self.top)
        dialog_btn_frame.pack(fill=X, pady=10)
        
        Button(dialog_btn_frame, text="Сохранить", command=self.save_and_close).pack(side=RIGHT, padx=5)
        Button(dialog_btn_frame, text="Отмена", command=self.top.destroy).pack(side=RIGHT)
        
    def refresh_tree(self):
        # Очищаем дерево
        for item in self.tree.get_children():
            self.tree.delete(item)
        
        # Заполняем правилами
        for rule in self.replacement_rules:
            active_text = "✓" if rule.get('active', True) else "✗"
            self.tree.insert('', END, values=(
                active_text,
                rule.get('name', ''),
                rule.get('pattern', ''),
                rule.get('replacement', '')
            ))
    
    def on_click(self, event):
        region = self.tree.identify("region", event.x, event.y)
        if region == "cell":
            col = self.tree.identify_column(event.x)
            row = self.tree.identify_row(event.y)
            
            if col == '#1':  # Колонка "Активно"
                self.toggle_active(row)
    
    def toggle_active(self, row_id):
        if not row_id:
            return
        
        # Находим индекс строки
        children = self.tree.get_children()
        try:
            index = children.index(row_id)
            rule = self.replacement_rules[index]
            rule['active'] = not rule.get('active', True)
            self.refresh_tree()
        except (ValueError, IndexError):
            pass
    
    def add_rule(self):
        name = self.name_entry.get().strip()
        pattern = self.pattern_entry.get().strip()
        replacement = self.replacement_entry.get()
        
        if not name or not pattern:
            messagebox.showwarning("Ошибка", "Заполните название и паттерн")
            return
        
        # Проверяем валидность регулярного выражения
        try:
            re.compile(pattern)
        except re.error as e:
            messagebox.showerror("Ошибка", f"Неверное регулярное выражение: {e}")
            return
        
        # Добавляем правило
        rule = {
            'name': name,
            'pattern': pattern,
            'replacement': replacement,
            'active': self.active_var.get()
        }
        self.replacement_rules.append(rule)
        
        # Обновляем таблицу
        self.refresh_tree()
        
        # Очищаем поля ввода
        self.name_entry.delete(0, END)
        self.pattern_entry.delete(0, END)
        self.replacement_entry.delete(0, END)
    
    def edit_selected(self, event=None):
        selection = self.tree.selection()
        if not selection:
            return
        
        # Находим индекс выбранного правила
        children = self.tree.get_children()
        try:
            index = children.index(selection[0])
            rule = self.replacement_rules[index]
        except (ValueError, IndexError):
            return
        
        # Создаем диалог редактирования
        edit_dialog = Toplevel(self.top)
        edit_dialog.title("Редактирование правила")
        edit_dialog.geometry("600x200")
        edit_dialog.grab_set()
        
        Label(edit_dialog, text="Название:").grid(row=0, column=0, sticky=W, padx=5, pady=5)
        name_var = StringVar(value=rule.get('name', ''))
        name_entry = Entry(edit_dialog, textvariable=name_var, width=40)
        name_entry.grid(row=0, column=1, sticky=W+E, padx=5, pady=5)
        
        Label(edit_dialog, text="Паттерн:").grid(row=1, column=0, sticky=W, padx=5, pady=5)
        pattern_var = StringVar(value=rule.get('pattern', ''))
        pattern_entry = Entry(edit_dialog, textvariable=pattern_var, width=40)
        pattern_entry.grid(row=1, column=1, sticky=W+E, padx=5, pady=5)
        
        Label(edit_dialog, text="Замена:").grid(row=2, column=0, sticky=W, padx=5, pady=5)
        replacement_var = StringVar(value=rule.get('replacement', ''))
        replacement_entry = Entry(edit_dialog, textvariable=replacement_var, width=40)
        replacement_entry.grid(row=2, column=1, sticky=W+E, padx=5, pady=5)
        
        active_var = BooleanVar(value=rule.get('active', True))
        active_check = Checkbutton(edit_dialog, text="Активно", variable=active_var)
        active_check.grid(row=3, column=1, sticky=W, padx=5, pady=5)
        
        def save_edit():
            new_name = name_var.get().strip()
            new_pattern = pattern_var.get().strip()
            new_replacement = replacement_var.get()
            
            if not new_name or not new_pattern:
                messagebox.showwarning("Ошибка", "Заполните название и паттерн")
                return
            
            try:
                re.compile(new_pattern)
            except re.error as e:
                messagebox.showerror("Ошибка", f"Неверное регулярное выражение: {e}")
                return
            
            # Обновляем правило
            rule['name'] = new_name
            rule['pattern'] = new_pattern
            rule['replacement'] = new_replacement
            rule['active'] = active_var.get()
            
            self.refresh_tree()
            edit_dialog.destroy()
        
        btn_frame = Frame(edit_dialog)
        btn_frame.grid(row=4, column=0, columnspan=2, pady=10)
        Button(btn_frame, text="Сохранить", command=save_edit).pack(side=RIGHT, padx=5)
        Button(btn_frame, text="Отмена", command=edit_dialog.destroy).pack(side=RIGHT)
        
        edit_dialog.grid_columnconfigure(1, weight=1)
    
    def delete_selected(self, event=None):
        selection = self.tree.selection()
        if not selection:
            return
        
        if messagebox.askyesno("Подтверждение", "Удалить выбранные правила?"):
            # Получаем индексы выбранных элементов
            children = self.tree.get_children()
            indices = []
            for item in selection:
                try:
                    indices.append(children.index(item))
                except ValueError:
                    pass
            
            # Удаляем в обратном порядке, чтобы не сбить индексы
            for index in sorted(indices, reverse=True):
                del self.replacement_rules[index]
            
            self.refresh_tree()
    
    def move_up(self):
        selection = self.tree.selection()
        if not selection:
            return
        
        children = self.tree.get_children()
        try:
            index = children.index(selection[0])
            if index > 0:
                # Меняем местами правила
                self.replacement_rules[index], self.replacement_rules[index-1] = \
                    self.replacement_rules[index-1], self.replacement_rules[index]
                self.refresh_tree()
                # Восстанавливаем выделение
                new_item = self.tree.get_children()[index-1]
                self.tree.selection_set(new_item)
        except (ValueError, IndexError):
            pass
    
    def move_down(self):
        selection = self.tree.selection()
        if not selection:
            return
        
        children = self.tree.get_children()
        try:
            index = children.index(selection[0])
            if index < len(self.replacement_rules) - 1:
                # Меняем местами правила
                self.replacement_rules[index], self.replacement_rules[index+1] = \
                    self.replacement_rules[index+1], self.replacement_rules[index]
                self.refresh_tree()
                # Восстанавливаем выделение
                new_item = self.tree.get_children()[index+1]
                self.tree.selection_set(new_item)
        except (ValueError, IndexError):
            pass
    
    def clear_all(self):
        if messagebox.askyesno("Подтверждение", "Удалить все правила?"):
            self.replacement_rules.clear()
            self.refresh_tree()
    
    def test_rules(self):
        test_string = self.test_entry.get()
        if not test_string:
            return
        
        result = test_string
        applied_rules = []
        
        for rule in self.replacement_rules:
            if not rule.get('active', True):
                continue
            
            try:
                old_result = result
                result = re.sub(rule['pattern'], rule['replacement'], result)
                if result != old_result:
                    applied_rules.append(rule['name'])
            except re.error:
                continue
        
        # Показываем результат
        result_dialog = Toplevel(self.top)
        result_dialog.title("Результат тестирования")
        result_dialog.geometry("500x300")
        
        Label(result_dialog, text="Исходная строка:", font=("Arial", 10, "bold")).pack(anchor=W, padx=10, pady=(10,0))
        Label(result_dialog, text=test_string, wraplength=450, justify=LEFT).pack(anchor=W, padx=20)
        
        Label(result_dialog, text="Результат:", font=("Arial", 10, "bold")).pack(anchor=W, padx=10, pady=(10,0))
        Label(result_dialog, text=result, wraplength=450, justify=LEFT).pack(anchor=W, padx=20)
        
        if applied_rules:
            Label(result_dialog, text="Примененные правила:", font=("Arial", 10, "bold")).pack(anchor=W, padx=10, pady=(10,0))
            for rule_name in applied_rules:
                Label(result_dialog, text=f"• {rule_name}").pack(anchor=W, padx=20)
        else:
            Label(result_dialog, text="Правила не применены", font=("Arial", 10, "bold")).pack(anchor=W, padx=10, pady=(10,0))
        
        Button(result_dialog, text="Закрыть", command=result_dialog.destroy).pack(pady=10)
    
    def import_rules(self):
        """Импорт правил из JSON файла"""
        try:
            from tkinter import filedialog
            
            filename = filedialog.askopenfilename(
                title="Выберите файл с правилами",
                filetypes=[("JSON файлы", "*.json"), ("Все файлы", "*.*")]
            )
            
            if not filename:
                return
            
            with open(filename, 'r', encoding='utf-8') as f:
                imported_rules = json.load(f)
            
            if not isinstance(imported_rules, list):
                messagebox.showerror("Ошибка", "Неверный формат файла. Ожидается список правил.")
                return
            
            # Проверяем структуру правил
            valid_rules = []
            for rule in imported_rules:
                if isinstance(rule, dict) and 'name' in rule and 'pattern' in rule and 'replacement' in rule:
                    # Проверяем валидность regex
                    try:
                        re.compile(rule['pattern'])
                        valid_rules.append({
                            'name': rule['name'],
                            'pattern': rule['pattern'],
                            'replacement': rule['replacement'],
                            'active': rule.get('active', True)
                        })
                    except re.error:
                        print(f"Пропущено невалидное правило: {rule['name']}")
            
            if not valid_rules:
                messagebox.showwarning("Предупреждение", "В файле не найдено валидных правил")
                return
            
            # Показываем диалог выбора правил для импорта
            self.show_import_dialog(valid_rules)
            
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка при импорте правил: {e}")
    
    def show_import_dialog(self, rules_to_import):
        """Показывает диалог выбора правил для импорта"""
        dialog = Toplevel(self.top)
        dialog.title("Импорт правил")
        dialog.geometry("700x500")
        dialog.transient(self.top)
        dialog.grab_set()
        
        # Центрируем диалог
        dialog.update_idletasks()
        x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2)
        y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2)
        dialog.geometry(f"+{x}+{y}")
        
        Label(dialog, text=f"Найдено {len(rules_to_import)} правил для импорта", 
              font=("Arial", 12, "bold")).pack(pady=10)
        
        # Фрейм со списком
        list_frame = Frame(dialog)
        list_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        # Список правил с чекбоксами
        canvas = Canvas(list_frame)
        scrollbar = Scrollbar(list_frame, orient="vertical", command=canvas.yview)
        scrollable_frame = Frame(canvas)
        
        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        # Переменные для чекбоксов
        check_vars = []
        for i, rule in enumerate(rules_to_import):
            var = BooleanVar(value=True)
            check_vars.append(var)
            
            frame = Frame(scrollable_frame)
            frame.pack(fill=X, pady=2)
            
            Checkbutton(frame, variable=var).pack(side=LEFT)
            Label(frame, text=f"{rule['name']}: {rule['pattern']} → {rule['replacement']}", 
                  anchor="w").pack(side=LEFT, fill=X, expand=True)
        
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # Кнопки
        btn_frame = Frame(dialog)
        btn_frame.pack(fill=X, padx=10, pady=10)
        
        def import_selected():
            imported_count = 0
            for i, var in enumerate(check_vars):
                if var.get():
                    rule = rules_to_import[i]
                    # Проверяем, не существует ли уже такое правило
                    existing = any(r['name'] == rule['name'] for r in self.replacement_rules)
                    if not existing:
                        self.replacement_rules.append(rule)
                        imported_count += 1
            
            if imported_count > 0:
                self.refresh_tree()
                messagebox.showinfo("Успех", f"Импортировано {imported_count} правил")
            else:
                messagebox.showinfo("Информация", "Не импортировано ни одного правила")
            
            dialog.destroy()
        
        Button(btn_frame, text="✓ Выбрать все", 
               command=lambda: [var.set(True) for var in check_vars]).pack(side=LEFT, padx=5)
        Button(btn_frame, text="✗ Снять все", 
               command=lambda: [var.set(False) for var in check_vars]).pack(side=LEFT, padx=5)
        Button(btn_frame, text="📥 Импортировать выбранные", command=import_selected).pack(side=RIGHT, padx=5)
        Button(btn_frame, text="❌ Отмена", command=dialog.destroy).pack(side=RIGHT, padx=5)
    
    def export_rules(self):
        """Экспорт правил в JSON файл"""
        try:
            from tkinter import filedialog
            
            if not self.replacement_rules:
                messagebox.showwarning("Предупреждение", "Нет правил для экспорта")
                return
            
            filename = filedialog.asksaveasfilename(
                title="Сохранить правила",
                defaultextension=".json",
                filetypes=[("JSON файлы", "*.json"), ("Все файлы", "*.*")]
            )
            
            if not filename:
                return
            
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(self.replacement_rules, f, ensure_ascii=False, indent=2)
            
            messagebox.showinfo("Успех", f"Правила экспортированы в файл:\\n{filename}")
            
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка при экспорте правил: {e}")
    
    def save_and_close(self):
        self.save_callback(self.replacement_rules)
        self.top.destroy()


class AttributeEditor:
    MAPPINGS_FILE = 'mappings.json'
    EXCLUSIONS_FILE = 'exclusions.json'
    PROTECTED_VALUES_FILE = 'protected_values.json'  # 🆕 Защищенные значения от разделения
    SETTINGS_FILE = 'settings.json'
    CASE_NORM_FILE = 'case_normalization.json'
    REPLACEMENT_RULES_FILE = 'replacement_rules.json'
    PARAM_MERGE_SETTINGS_FILE = 'param_merge_settings.json'
    VALUE_MERGE_SETTINGS_FILE = 'value_merge_settings.json'

    def __init__(self, root):
        self.root = root
        self.root.title("🆕 Редактор атрибутов товаров v4.9 - Система динамических обновлений")
        self.root.geometry("1200x700")  # Уменьшили размер окна благодаря компактности
        self.original_df = None
        self.param_mapping = {}
        self.original_values = {}
        self.manual_split_values = {}
        self.exclusions = []
        self.mappings = {}
        self.sort_directions = {}
        self.edit_entry = None
        self.edit_undo_stack = []
        self.similar_groups = {}
        self.SIMILARITY_THRESHOLD = 85
        self.df_columns = []

        self.mapping_sort_reverse = False
        self._mapping_items_cache = []

        self.settings = {
            "features_col": "",
            "extra_cols": [],
            "manual_split_values": {},
            "source_file_path": "",
            "output_folder_path": ""
        }

        self.case_normalization = {}
        self.manual_split_results = {}
        self.replacement_rules = []
        
        # 🆕 ЗАЩИЩЕННЫЕ ЗНАЧЕНИЯ: не разделяются, даже если содержат символ разделителя
        self.protected_values = []  # Список значений типа "B/W", "I/O", "AC/DC"

        # Настройки объединения - автосохранение/загрузка
        self.param_merge_settings = {}
        self.value_merge_settings = {}
        
        # 🆕 НОВЫЕ ВОЗМОЖНОСТИ v4.0
        # Менеджер синонимов значений
        self.synonyms_manager = ValueSynonymsManager()
        
        # 🆕 СИСТЕМА ДИНАМИЧЕСКИХ ОБНОВЛЕНИЙ v4.9
        # Регистрируем callback для автоматического обновления при изменении синонимов
        self.synonyms_manager.add_callback(self.dynamic_update_main_table)
        
        # 🆕 НОВЫЕ ВОЗМОЖНОСТИ v4.7
        # Система переводов значений и единиц измерения
        self.value_translation_rules = []
        self.load_value_translation_rules()
        
        # 🌐 РАСШИРЕННАЯ СИСТЕМА ЕДИНИЦ ИЗМЕРЕНИЙ IP-ВИДЕОНАБЛЮДЕНИЯ
        self.ip_surveillance_units = {}
        self.load_ip_surveillance_units()
        
        # 🌐 ИНИЦИАЛИЗАЦИЯ СИСТЕМЫ ПЕРЕВОДОВ
        self.translation_available = TRANSLATION_AVAILABLE  # Сохраняем как атрибут класса
        # DEBUG: Инициализация переводов завершена
        if self.translation_available:
            self.translation_system = TranslationSystem()
            print("✅ Система переводов инициализирована")
        else:
            self.translation_system = None
            print("⚠️ Работа без системы переводов")
        
        # Цвета для выделения похожих параметров
        self.similar_param_colors = [
            "#FFE6E6",  # Светло-розовый
            "#E6F3FF",  # Светло-синий  
            "#E6FFE6",  # Светло-зеленый
            "#FFF0E6",  # Светло-оранжевый
            "#F0E6FF",  # Светло-фиолетовый
            "#FFFFE6",  # Светло-желтый
            "#E6FFFF",  # Светло-бирюзовый
            "#FFE6F0",  # Светло-магента
        ]
        self.param_color_map = {}  # {param_name: color}
        self.similar_param_groups = {}  # {representative: [similar_params]}

        self.context_menu = Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="Копировать", command=self.copy_selected_value)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="Объединить выделенные параметры", command=self.merge_selected_similar_params)
        self.context_menu.add_command(label="Объединить параметры и их значения", command=self.merge_selected_params_and_values)
        
        # 🆕 Контекстное меню для вкладки "Объединение параметров"
        self.params_context_menu = Menu(self.root, tearoff=0)
        self.params_context_menu.add_command(label="📝 Редактировать группу", command=self.edit_param_group)
        self.params_context_menu.add_command(label="🗑️ Удалить из группы", command=self.remove_from_param_group)
        self.params_context_menu.add_separator()
        self.params_context_menu.add_command(label="❌ Удалить всю группу", command=self.delete_param_group)
        
        # Для отслеживания частоты использования параметров
        self.param_frequency = {}

        self.load_settings()
        self.load_mappings()
        self.load_excl()
        self.load_protected_values()  # 🆕 Загрузка защищенных значений
        self.load_case_normalization()
        self.load_replacement_rules()
        self.load_merge_settings()
        self.setup_ui()
        
        # Инициализируем кеш маппингов
        self.refresh_mapping_table()

        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

    def setup_ui(self):
        """Создание компактного и эргономичного интерфейса"""
        # DEBUG: Настройка интерфейса
        # Горячие клавиши
        self.root.bind_all("<Control-c>", self.global_ctrl_c)
        self.root.bind_all("<Control-v>", self.global_ctrl_v)
        self.root.bind_all("<Control-z>", self.global_ctrl_z)
        self.root.bind_all("<Control-a>", self.global_ctrl_a)
        self.root.bind_all("<Delete>", self.global_delete)
        
        # Создаем расширенное меню
        self.create_compact_menu()
        
        # Главный контейнер
        main_frame = Frame(self.root)
        main_frame.pack(fill=BOTH, expand=True, padx=5, pady=2)

        # 📁 ГРУППА 1: Файлы и настройки (компактно в одной строке)
        files_frame = LabelFrame(main_frame, text="📁 Файлы и настройки", font=("Arial", 9, "bold"))
        files_frame.pack(fill=X, pady=(0, 2))
        
        # Строка 1: Исходный файл
        file_row1 = Frame(files_frame)
        file_row1.pack(fill=X, padx=5, pady=2)
        
        Label(file_row1, text="Файл:", width=6).pack(side=LEFT)
        self.source_file_var = StringVar(value=self.settings.get("source_file_path", ""))
        source_entry = Entry(file_row1, textvariable=self.source_file_var, width=35)
        source_entry.pack(side=LEFT, fill=X, expand=True, padx=2)
        source_entry.bind('<FocusOut>', lambda e: self.save_settings())
        Button(file_row1, text="📂", width=3, command=self.browse_source_file).pack(side=LEFT, padx=2)
        ToolTip(source_entry, "Путь к исходному Excel файлу")
        
        # Вторая часть строки: основные настройки
        Label(file_row1, text="Столбец:", width=8).pack(side=LEFT, padx=(15, 2))
        self.features_col = Entry(file_row1, width=4)
        self.features_col.pack(side=LEFT)
        self.features_col.insert(0, self.settings.get("features_col", ""))
        ToolTip(self.features_col, "Номер столбца с HTML-характеристиками")
        
        Label(file_row1, text="Доп.столбцы:", width=10).pack(side=LEFT, padx=(15, 2))
        self.extra_cols_label = Label(file_row1, text="", width=12, anchor=W, relief=SUNKEN, bg="white")
        self.extra_cols_label.pack(side=LEFT, padx=2)
        Button(file_row1, text="⚙️", width=3, command=self.show_extra_cols_dialog).pack(side=LEFT, padx=2)
        ToolTip(self.extra_cols_label, "Дополнительные столбцы для сохранения")
        
        # Строка 2: Папка сохранения (более компактно)
        file_row2 = Frame(files_frame)
        file_row2.pack(fill=X, padx=5, pady=(0, 2))
        
        Label(file_row2, text="Папка:", width=6).pack(side=LEFT)
        self.output_folder_var = StringVar(value=self.settings.get("output_folder_path", ""))
        output_entry = Entry(file_row2, textvariable=self.output_folder_var, width=35)
        output_entry.pack(side=LEFT, fill=X, expand=True, padx=2)
        output_entry.bind('<FocusOut>', lambda e: self.save_settings())
        Button(file_row2, text="📂", width=3, command=self.browse_output_folder).pack(side=LEFT, padx=2)
        ToolTip(output_entry, "Папка для сохранения результатов")
        
        # 🔧 ГРУППА 2: Панель инструментов (компактная)
        toolbar_frame = LabelFrame(main_frame, text="🔧 Действия", font=("Arial", 9, "bold"))
        toolbar_frame.pack(fill=X, pady=(0, 2))
        
        toolbar = Frame(toolbar_frame)
        toolbar.pack(fill=X, padx=5, pady=3)
        
        # Основные действия (крупные кнопки)
        actions_frame = Frame(toolbar)
        actions_frame.pack(side=LEFT)
        
        self.btn_load = Button(actions_frame, text="📥 Загрузить", command=self.load_file, 
                              bg="#E8F5E8", font=("Arial", 9, "bold"), width=12)
        self.btn_load.pack(side=LEFT, padx=2)
        ToolTip(self.btn_load, "Загрузить Excel файл")
        
        self.btn_process = Button(actions_frame, text="⚙️ Обработать", command=self.process_table,
                                 bg="#FFF4E6", font=("Arial", 9, "bold"), width=12)
        self.btn_process.pack(side=LEFT, padx=2)
        ToolTip(self.btn_process, "Обработать таблицу и извлечь параметры")
        
        self.btn_save = Button(actions_frame, text="💾 Сохранить", command=self.save_results,
                              bg="#E6F3FF", font=("Arial", 9, "bold"), width=12)
        self.btn_save.pack(side=LEFT, padx=2)
        ToolTip(self.btn_save, "Сохранить результат в Excel")
        
        # 🌐 КНОПКА ПЕРЕВОДОВ (если доступна)
        if self.translation_available:
            self.btn_translate = Button(actions_frame, text="🌐 Переводы", command=self.open_translation_dialog,
                                      bg="#F0E6FF", font=("Arial", 9, "bold"), width=12)
            self.btn_translate.pack(side=LEFT, padx=2)
            try:
                ToolTip(self.btn_translate, "Функциональное меню переводов:\n• Перевести параметры\n• Перевести значения\n• Настройки переводов\n• Импорт/экспорт")
            except Exception as e:
                print(f"Ошибка создания ToolTip для кнопки переводов: {e}")
        
        # Быстрые настройки
        # Быстрые настройки (мелкие кнопки)
        try:
            settings_frame = Frame(toolbar)
            settings_frame.pack(side=RIGHT)
            print("🔍 DEBUG: settings_frame создан")
        except Exception as e:
            print(f"🔍 DEBUG: Ошибка создания settings_frame: {e}")
        
        try:
            Label(settings_frame, text="Настройки:", font=("Arial", 8)).pack(side=LEFT, padx=(10, 5))
            print("🔍 DEBUG: Label 'Настройки' создан")
        except Exception as e:
            print(f"🔍 DEBUG: Ошибка Label 'Настройки': {e}")
        
        try:
            self.btn_replacement_rules = Button(settings_frame, text="🔄", width=3, command=self.open_replacement_rules_editor,
                   bg="#FFE6E6")
            self.btn_replacement_rules.pack(side=LEFT, padx=1)
            ToolTip(self.btn_replacement_rules, "Правила замен")
            print("🔍 DEBUG: btn_replacement_rules создана")
        except Exception as e:
            print(f"🔍 DEBUG: Ошибка btn_replacement_rules: {e}")
        
        self.btn_case_norm = Button(settings_frame, text="Aa", width=3, command=self.open_case_normalization_dialog,
               bg="#E6FFE6")
        self.btn_case_norm.pack(side=LEFT, padx=1)
        ToolTip(self.btn_case_norm, "Нормализация регистра")
        
        self.btn_synonyms = Button(settings_frame, text="≈", width=3, command=self.open_value_synonyms_dialog,
               bg="#E6F7FF")
        self.btn_synonyms.pack(side=LEFT, padx=1)
        ToolTip(self.btn_synonyms, "Синонимы значений")
        
        self.btn_protected_values = Button(settings_frame, text="🛡️", width=3, command=self.open_protected_values_dialog,
               bg="#FFE6CC")
        self.btn_protected_values.pack(side=LEFT, padx=1)
        ToolTip(self.btn_protected_values, "Защищенные значения")
        
        # Восстанавливаем выбранные столбцы
        if self.settings.get("extra_cols"):
            col_count = len(self.settings['extra_cols'])
            self.extra_cols_label.config(text=f"({col_count})")
            self.extra_cols_label.config(bg="lightgreen")
            ToolTip(self.extra_cols_label, f"Выбрано {col_count} столбцов")

        # 📊 ОСНОВНАЯ РАБОЧАЯ ОБЛАСТЬ (вкладки)
        self.notebook = ttk.Notebook(main_frame)
        self.notebook.pack(fill=BOTH, expand=True, pady=2)

        # Вкладка 1: Основная таблица
        self.main_tab = Frame(self.notebook)
        self.notebook.add(self.main_tab, text="📋 Основная таблица")
        self.setup_main_tab()

        # Вкладка 2: Объединение параметров
        self.params_merge_tab = Frame(self.notebook)
        self.notebook.add(self.params_merge_tab, text="🔄 Объединение параметров")
        self.setup_params_merge_tab()

        # 🆕 Вкладка 3: Переводы значений и единиц измерения
        self.values_translation_tab = Frame(self.notebook)
        self.notebook.add(self.values_translation_tab, text="🌐 Переводы значений")
        self.setup_values_translation_tab()

        # Информационная панель внизу (компактная)
        self.info_label = Label(main_frame, text="✅ Готов к работе", relief=SUNKEN, anchor=W, 
                               font=("Arial", 8), bg="#F0F0F0")
        self.info_label.pack(fill=X, pady=(2, 0))

    def create_compact_menu(self):
        """Создает компактное меню с перенесенными функциями"""
        menubar = Menu(self.root)
        self.root.config(menu=menubar)
        
        # Меню "Файл"
        file_menu = Menu(menubar, tearoff=0)
        menubar.add_cascade(label="📁 Файл", menu=file_menu)
        file_menu.add_command(label="📥 Загрузить файл", command=self.load_file, accelerator="Ctrl+O")
        file_menu.add_command(label="⚙️ Обработать таблицу", command=self.process_table, accelerator="Ctrl+P")
        file_menu.add_separator()
        file_menu.add_command(label="💾 Сохранить результат", command=self.save_results, accelerator="Ctrl+S")
        file_menu.add_separator()
        file_menu.add_command(label="🚪 Выход", command=self.on_close, accelerator="Ctrl+Q")
        
        # Меню "Настройки" (перенесенные функции)
        settings_menu = Menu(menubar, tearoff=0)
        menubar.add_cascade(label="⚙️ Настройки", menu=settings_menu)
        settings_menu.add_command(label="🔄 Правила замен", command=self.open_replacement_rules_editor)
        settings_menu.add_command(label="Aa Нормализация регистра", command=self.open_case_normalization_dialog)
        settings_menu.add_command(label="≈ Синонимы значений", command=self.open_value_synonyms_dialog)
        settings_menu.add_command(label="🛡️ Защищенные значения", command=self.open_protected_values_dialog)
        settings_menu.add_separator()
        settings_menu.add_command(label="📋 Управление исключениями", command=self.open_exclusions_dialog)
        
        # 🌐 МЕНЮ "ПЕРЕВОДЫ" (если доступна система)
        print(f"🔍 DEBUG: Проверка меню - self.translation_available = {self.translation_available}")
        if self.translation_available:
            print("🔍 DEBUG: СОЗДАЕМ меню переводов!")
            translation_menu = Menu(menubar, tearoff=0)
            menubar.add_cascade(label="🌐 Переводы", menu=translation_menu)
            translation_menu.add_command(label="⚙️ Настройки переводов", command=self.open_translation_dialog)
            translation_menu.add_separator()
            translation_menu.add_command(label="🔄 Перевести текущие параметры", command=self.translate_current_parameters)
            translation_menu.add_command(label="🔄 Перевести текущие значения", command=self.translate_current_values)
            translation_menu.add_separator()
            translation_menu.add_command(label="💾 Экспорт переводов", command=self.export_translations)
            translation_menu.add_command(label="📂 Импорт переводов", command=self.import_translations)
            print("🔍 DEBUG: Меню переводов создано!")
        else:
            print("🔍 DEBUG: НЕ создаем меню - условие False")
        
        # Меню "Инструменты"
        tools_menu = Menu(menubar, tearoff=0)
        menubar.add_cascade(label="🔧 Инструменты", menu=tools_menu)
        tools_menu.add_command(label="🔍 Найти похожие параметры", command=self.detect_similar_parameters)
        tools_menu.add_command(label="📤 Экспорт маппингов", command=self.export_mappings)
        tools_menu.add_command(label="📥 Импорт маппингов", command=self.import_mappings)
        tools_menu.add_separator()
        tools_menu.add_command(label="🧹 Нормализация значений", command=self.normalize_all_values_format)
        
        # Меню "Помощь"
        help_menu = Menu(menubar, tearoff=0)
        menubar.add_cascade(label="❓ Помощь", menu=help_menu)
        help_menu.add_command(label="📚 Справка", command=self.show_help_dialog, accelerator="F1")
        help_menu.add_separator()
        help_menu.add_command(label="ℹ️ О программе", command=self.show_about_dialog)
        help_menu.add_command(label="⌨️ Горячие клавиши", command=self.show_hotkeys_dialog)

        # Горячие клавиши
        self.root.bind('<Control-o>', lambda e: self.load_file())
        self.root.bind('<Control-p>', lambda e: self.process_table())
        self.root.bind('<Control-s>', lambda e: self.save_results())
        self.root.bind('<Control-q>', lambda e: self.on_close())
        self.root.bind('<F1>', lambda e: self.show_help_dialog())

    def setup_main_tab(self):
        """Настройка основной вкладки с таблицей"""
        tree_frame = Frame(self.main_tab)
        tree_frame.pack(fill=BOTH, expand=True, pady=5)

        vsb = Scrollbar(tree_frame, orient="vertical")
        vsb.pack(side=RIGHT, fill=Y)

        columns = ('rownum', 'orig_param', 'orig_values', 'new_param', 'delimiter', 'split_result')
        self.tree = ttk.Treeview(tree_frame, columns=columns, show='headings', height=15, yscrollcommand=vsb.set)
        self.tree.pack(side=LEFT, fill=BOTH, expand=True)
        vsb.config(command=self.tree.yview)

        self.tree.heading('rownum', text='№')
        self.tree.heading('orig_param', text='Исходный параметр')
        self.tree.heading('orig_values', text='Исходные значения')
        self.tree.heading('new_param', text='Новый параметр')
        self.tree.heading('delimiter', text='Разделитель')
        self.tree.heading('split_result', text='Результат разделения')

        self.tree.column('rownum', width=40, anchor=CENTER)
        self.tree.column('orig_param', width=130)
        self.tree.column('orig_values', width=250)
        self.tree.column('new_param', width=130)
        self.tree.column('delimiter', width=70)
        self.tree.column('split_result', width=350)

        self.tree.bind("<Double-1>", self.on_double_click)
        self.tree.bind("<Button-3>", self.show_context_menu)
        self.tree.bind("<Button-1>", self.sort_column, True)
        self.tree.bind("<Control-c>", self.copy_selected_value)
        self.tree.bind("<Control-a>", self.select_all_rows)

        # Таблица сопоставлений
        mapping_frame = LabelFrame(self.main_tab, text="Таблица сопоставлений и поиск")
        mapping_frame.pack(fill=X, pady=5)

        search_frame = Frame(mapping_frame)
        search_frame.pack(fill=X, padx=5, pady=2)
        Label(search_frame, text="Поиск по Исходным значениям:").pack(side=LEFT)
        self.search_var = StringVar()
        self.search_var.trace_add('write', self.filter_mapping_table)
        self.search_entry = Entry(search_frame, textvariable=self.search_var)
        self.search_entry.pack(side=LEFT, fill=X, expand=True, padx=5)

        mapping_vsb = Scrollbar(mapping_frame, orient="vertical")
        mapping_vsb.pack(side=RIGHT, fill=Y)

        self.mapping_tree = ttk.Treeview(mapping_frame, columns=('original', 'replacement'), show='headings', height=6,
                                         selectmode='extended',
                                         yscrollcommand=mapping_vsb.set)
        self.mapping_tree.heading('original', text='Исходное значение', command=self.sort_mapping_tree_by_original_col)
        self.mapping_tree.heading('replacement', text='Замена')
        self.mapping_tree.column('original', width=250)
        self.mapping_tree.column('replacement', width=250)
        self.mapping_tree.pack(side=LEFT, fill=BOTH, expand=True, padx=5)
        mapping_vsb.config(command=self.mapping_tree.yview)

        self.mapping_tree.bind("<Double-1>", self.on_mapping_double_click)
        self.mapping_tree.bind("<Control-a>", self.select_all_mappings)
        self.mapping_tree.bind("<Delete>", self.delete_selected_mappings)
        self.mapping_tree.bind("<Control-c>", self.copy_mapping_selected_value)

        mapping_buttons = Frame(mapping_frame)
        mapping_buttons.pack(side=LEFT, padx=5, fill=Y)

        btn_add = Button(mapping_buttons, text="Добавить", command=self.add_mapping)
        btn_add.pack(fill=X, pady=2)
        btn_del = Button(mapping_buttons, text="Удалить", command=self.delete_mapping)
        btn_del.pack(fill=X, pady=2)
        btn_select_all = Button(mapping_buttons, text="Выделить все", command=self.select_all_mappings)
        btn_select_all.pack(fill=X, pady=2)
        ToolTip(btn_select_all, "Выделить все сопоставления в таблице")
        
        btn_delete_all = Button(mapping_buttons, text="Удалить все", command=self.delete_all_mappings)
        btn_delete_all.pack(fill=X, pady=2)
        ToolTip(btn_delete_all, "Удалить все сопоставления из таблицы")
        
        btn_import = Button(mapping_buttons, text="Импорт", command=self.import_mappings)
        btn_import.pack(fill=X, pady=2)
        btn_export = Button(mapping_buttons, text="Экспорт", command=self.export_mappings)
        btn_export.pack(fill=X, pady=2)

        # Список исключений
        exclusions_frame = LabelFrame(self.main_tab, text="Список исключений (двойной клик - редактирование)")
        exclusions_frame.pack(fill=X, pady=5)

        self.exclusions_tree = ttk.Treeview(exclusions_frame, columns=('exclusion',), show='headings', height=3)
        self.exclusions_tree.heading('exclusion', text='Исключение')
        self.exclusions_tree.column('exclusion', width=400)
        self.exclusions_tree.pack(side=LEFT, fill=BOTH, expand=True)
        self.exclusions_tree.bind("<Double-1>", self.on_exclusion_double_click)
        self.exclusions_tree.bind("<Control-c>", self.copy_exclusion_selected_value)

        exclusions_buttons = Frame(exclusions_frame)
        exclusions_buttons.pack(side=LEFT, padx=5, fill=Y)

        btn_add_exclusion = Button(exclusions_buttons, text="Добавить", command=self.add_exclusion)
        btn_add_exclusion.pack(fill=X, pady=2)
        btn_del_exclusion = Button(exclusions_buttons, text="Удалить", command=self.delete_exclusion)
        btn_del_exclusion.pack(fill=X, pady=2)

        self.update_exclusions_tree()

    def setup_params_merge_tab(self):
        """Настройка вкладки объединения параметров"""
        # Инструкция
        info_frame = LabelFrame(self.params_merge_tab, text="Информация")
        info_frame.pack(fill=X, padx=5, pady=5)
        
        info_text = """Эта вкладка позволяет найти и объединить похожие параметры для исправления опечаток.
Обнаруженные группы похожих параметров будут автоматически сохранены в настройках.
При следующем запуске приложения настройки будут восстановлены."""
        
        Label(info_frame, text=info_text, justify=LEFT, wraplength=800).pack(padx=10, pady=5)

        # Кнопки управления
        control_frame = Frame(self.params_merge_tab)
        control_frame.pack(fill=X, padx=5, pady=5)

        self.btn_find_similar_params = Button(control_frame, text="Найти похожие параметры", 
                                             command=self.find_similar_parameters_for_tab)
        self.btn_find_similar_params.pack(side=LEFT, padx=5)
        
        self.btn_apply_param_merges = Button(control_frame, text="Применить выбранные объединения", 
                                           command=self.apply_param_merges_from_tab)
        self.btn_apply_param_merges.pack(side=LEFT, padx=5)
        
        self.btn_clear_param_settings = Button(control_frame, text="Очистить настройки", 
                                              command=self.clear_param_merge_settings)
        self.btn_clear_param_settings.pack(side=LEFT, padx=15)

        # Таблица похожих параметров
        table_frame = LabelFrame(self.params_merge_tab, text="Похожие параметры")
        table_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)

        tree_frame = Frame(table_frame)
        tree_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)

        columns = ('merge', 'representative', 'similar_params', 'frequency')
        self.params_tree = ttk.Treeview(tree_frame, columns=columns, show='headings')
        self.params_tree.pack(side=LEFT, fill=BOTH, expand=True)

        params_vsb = Scrollbar(tree_frame, orient="vertical", command=self.params_tree.yview)
        params_vsb.pack(side=RIGHT, fill=Y)
        self.params_tree.configure(yscrollcommand=params_vsb.set)

        # 🆕 СОРТИРОВКА ПО ЗАГОЛОВКАМ: Настраиваем заголовки с возможностью сортировки
        self.params_tree.heading('merge', text='Объединить')
        self.params_tree.heading('representative', text='Основной параметр ↕', 
                                 command=lambda: self.sort_params_tree('representative'))
        self.params_tree.heading('similar_params', text='Похожие параметры ↕', 
                                 command=lambda: self.sort_params_tree('similar_params'))
        self.params_tree.heading('frequency', text='Частота ↕', 
                                 command=lambda: self.sort_params_tree('frequency'))

        self.params_tree.column('merge', width=80, anchor=CENTER)
        self.params_tree.column('representative', width=250, anchor=W)
        self.params_tree.column('similar_params', width=350, anchor=W)
        self.params_tree.column('frequency', width=100, anchor=CENTER)

        # Переменная для отслеживания направления сортировки
        self.params_tree_sort_direction = {}  # {column: 'asc'/'desc'}
        
        self.params_tree.bind('<Button-1>', self.on_params_tree_click)
        self.params_tree.bind('<Button-3>', self.show_params_context_menu)  # 🆕 ПКМ для редактирования

        # Кнопки выбора
        btn_frame = Frame(table_frame)
        btn_frame.pack(fill=X, padx=5, pady=5)

        Button(btn_frame, text="Выбрать все", command=self.select_all_params).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Снять все", command=self.deselect_all_params).pack(side=LEFT, padx=5)

    def setup_values_merge_tab(self):
        """Настройка вкладки объединения значений"""
        # Инструкция
        info_frame = LabelFrame(self.values_merge_tab, text="Информация")
        info_frame.pack(fill=X, padx=5, pady=5)
        
        info_text = """Эта вкладка позволяет найти и объединить похожие значения внутри параметров.
Например: "Support"/"Supported", "Yes"/"YES", "Нет"/"No" и т.д.
Обнаруженные группы похожих значений будут автоматически сохранены в настройках."""
        
        Label(info_frame, text=info_text, justify=LEFT, wraplength=800).pack(padx=10, pady=5)

        # Кнопки управления
        control_frame = Frame(self.values_merge_tab)
        control_frame.pack(fill=X, padx=5, pady=5)

        self.btn_find_similar_values = Button(control_frame, text="Найти похожие значения", 
                                            command=self.find_similar_values_for_tab)
        self.btn_find_similar_values.pack(side=LEFT, padx=5)
        
        self.btn_apply_value_merges = Button(control_frame, text="Применить выбранные объединения", 
                                           command=self.apply_value_merges_from_tab)
        self.btn_apply_value_merges.pack(side=LEFT, padx=5)
        
        self.btn_clear_value_settings = Button(control_frame, text="Очистить настройки", 
                                              command=self.clear_value_merge_settings)
        self.btn_clear_value_settings.pack(side=LEFT, padx=15)

        # Таблица похожих значений
        table_frame = LabelFrame(self.values_merge_tab, text="Похожие значения")
        table_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)

        tree_frame = Frame(table_frame)
        tree_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)

        columns = ('merge', 'parameter', 'representative', 'similar_values', 'frequency')
        self.values_tree = ttk.Treeview(tree_frame, columns=columns, show='headings')
        self.values_tree.pack(side=LEFT, fill=BOTH, expand=True)

        values_vsb = Scrollbar(tree_frame, orient="vertical", command=self.values_tree.yview)
        values_vsb.pack(side=RIGHT, fill=Y)
        self.values_tree.configure(yscrollcommand=values_vsb.set)

        self.values_tree.heading('merge', text='Объединить')
        self.values_tree.heading('parameter', text='Параметр')
        self.values_tree.heading('representative', text='Основное значение')
        self.values_tree.heading('similar_values', text='Похожие значения')
        self.values_tree.heading('frequency', text='Частота')

        self.values_tree.column('merge', width=80, anchor=CENTER)
        self.values_tree.column('parameter', width=200, anchor=W)
        self.values_tree.column('representative', width=150, anchor=W)
        self.values_tree.column('similar_values', width=300, anchor=W)
        self.values_tree.column('frequency', width=100, anchor=CENTER)

        self.values_tree.bind('<Button-1>', self.on_values_tree_click)

        # Кнопки выбора
        btn_frame = Frame(table_frame)
        btn_frame.pack(fill=X, padx=5, pady=5)

        Button(btn_frame, text="Выбрать все", command=self.select_all_values).pack(side=LEFT, padx=5)
        Button(btn_frame, text="Снять все", command=self.deselect_all_values).pack(side=LEFT, padx=5)

    def setup_values_translation_tab(self):
        """🆕 Настройка вкладки переводов значений и единиц измерения"""
        # Инициализируем правила перевода значений если их нет
        if not hasattr(self, 'value_translation_rules'):
            self.value_translation_rules = []
            self.load_value_translation_rules()
        
        # Заголовок с описанием
        header_frame = LabelFrame(self.values_translation_tab, text="ℹ️ Информация", font=("Arial", 10, "bold"))
        header_frame.pack(fill=X, padx=5, pady=5)
        
        info_text = """🌐 Переводы значений и единиц измерения
        
Эта вкладка позволяет настроить правила перевода значений:
• Единицы измерения: mm → мм, kg → кг, Wh → Вт⋅ч
• Технические термины: Support → Поддержка, Auto → Автоматически  
• Любые другие значения: Yes → Да, No → Нет

При нажатии "Обработать" система автоматически найдет значения с единицами измерения 
и предложит создать правила перевода."""
        
        Label(header_frame, text=info_text, justify=LEFT, wraplength=900, font=("Arial", 9)).pack(padx=10, pady=5)
        
        # Панель управления
        control_frame = LabelFrame(self.values_translation_tab, text="🔧 Управление", font=("Arial", 10, "bold"))
        control_frame.pack(fill=X, padx=5, pady=5)
        
        buttons_frame = Frame(control_frame)
        buttons_frame.pack(fill=X, padx=5, pady=5)
        
        # Кнопки управления
        Button(buttons_frame, text="➕ Добавить правило", command=self.add_value_translation_rule,
               bg="#E8F5E8", font=("Arial", 9)).pack(side=LEFT, padx=5)
        
        Button(buttons_frame, text="✏️ Редактировать", command=self.edit_value_translation_rule,
               bg="#FFF4E6", font=("Arial", 9)).pack(side=LEFT, padx=5)
        
        Button(buttons_frame, text="🗑️ Удалить", command=self.delete_value_translation_rule,
               bg="#FFE6E6", font=("Arial", 9)).pack(side=LEFT, padx=5)
        
        Button(buttons_frame, text="🔍 Найти единицы", command=self.auto_detect_units,
               bg="#E6F3FF", font=("Arial", 9, "bold")).pack(side=LEFT, padx=15)
        
        Button(buttons_frame, text="💾 Экспорт", command=self.export_value_translations,
               bg="#F0E6FF", font=("Arial", 9)).pack(side=LEFT, padx=5)
        
        Button(buttons_frame, text="📥 Импорт", command=self.import_value_translations,
               bg="#F0E6FF", font=("Arial", 9)).pack(side=LEFT, padx=5)
        
        # Таблица правил перевода
        table_frame = LabelFrame(self.values_translation_tab, text="📋 Правила перевода значений", font=("Arial", 10, "bold"))
        table_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
        
        # Создаем фрейм для таблицы и скроллбара
        tree_frame = Frame(table_frame)
        tree_frame.pack(fill=BOTH, expand=True, padx=5, pady=5)
        
        # Настройка колонок
        columns = ('active', 'original', 'translation', 'category', 'description', 'examples')
        self.translation_tree = ttk.Treeview(tree_frame, columns=columns, show='headings', height=15)
        
        # Настройка заголовков
        self.translation_tree.heading('active', text='✓')
        self.translation_tree.heading('original', text='Исходное значение')  
        self.translation_tree.heading('translation', text='Перевод')
        self.translation_tree.heading('category', text='Категория')
        self.translation_tree.heading('description', text='Описание')
        self.translation_tree.heading('examples', text='Примеры')
        
        # Настройка ширины колонок
        self.translation_tree.column('active', width=30, anchor=CENTER)
        self.translation_tree.column('original', width=150, anchor=W)
        self.translation_tree.column('translation', width=150, anchor=W)
        self.translation_tree.column('category', width=100, anchor=W)
        self.translation_tree.column('description', width=200, anchor=W)
        self.translation_tree.column('examples', width=200, anchor=W)
        
        # Скроллбары
        v_scrollbar = Scrollbar(tree_frame, orient=VERTICAL, command=self.translation_tree.yview)
        h_scrollbar = Scrollbar(tree_frame, orient=HORIZONTAL, command=self.translation_tree.xview)
        self.translation_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
        
        # Размещение элементов
        self.translation_tree.pack(side=LEFT, fill=BOTH, expand=True)
        v_scrollbar.pack(side=RIGHT, fill=Y)
        h_scrollbar.pack(side=BOTTOM, fill=X)
        
        # Привязка событий v4.8
        self.translation_tree.bind('<Double-1>', lambda e: self.edit_value_translation_rule())
        self.translation_tree.bind('<Button-3>', self.show_translation_context_menu)
        self.translation_tree.bind('<Button-1>', self.on_translation_tree_click)
        
        # Кнопки выбора v4.8
        selection_frame = Frame(table_frame)
        selection_frame.pack(fill=X, padx=5, pady=5)
        
        # 🆕 Массовые операции с выбранными правилами v4.8
        Button(selection_frame, text="✓ Активировать выбранные", command=self.activate_selected_rules,
               bg="#E8F5E8", font=("Arial", 9)).pack(side=LEFT, padx=2)
        Button(selection_frame, text="✗ Деактивировать выбранные", command=self.deactivate_selected_rules,
               bg="#FFE6E6", font=("Arial", 9)).pack(side=LEFT, padx=2)
        
        # Разделитель
        Label(selection_frame, text=" | ", font=("Arial", 9)).pack(side=LEFT, padx=5)
        
        Button(selection_frame, text="✓ Активировать все", command=self.activate_all_translation_rules,
               font=("Arial", 8)).pack(side=LEFT, padx=2)
        Button(selection_frame, text="✗ Деактивировать все", command=self.deactivate_all_translation_rules,
               font=("Arial", 8)).pack(side=LEFT, padx=2)
        
        # Статистика v4.8
        stats_right = Frame(selection_frame)
        stats_right.pack(side=RIGHT, padx=5)
        
        self.translation_stats_label = Label(stats_right, text="Правил: 0 | Активных: 0", 
                                           font=("Arial", 9), fg="blue")
        self.translation_stats_label.pack(side=TOP)
        
        self.selected_count_label = Label(stats_right, text="Выбрано: 0", 
                                        font=("Arial", 9), fg="green")
        self.selected_count_label.pack(side=TOP)
        
        # Загружаем и отображаем правила
        self.refresh_translation_rules_display()

    def load_value_translation_rules(self):
        """Загружает правила перевода значений из файла"""
        try:
            if os.path.exists("value_translation_rules.json"):
                with open("value_translation_rules.json", "r", encoding="utf-8") as f:
                    self.value_translation_rules = json.load(f)
                print(f"✅ Загружено {len(self.value_translation_rules)} правил перевода значений")
            else:
                # Создаем правила по умолчанию
                self.value_translation_rules = self.get_default_translation_rules()
                self.save_value_translation_rules()
                print(f"✅ Созданы правила перевода по умолчанию: {len(self.value_translation_rules)} записей")
        except Exception as e:
            print(f"❌ Ошибка при загрузке правил перевода: {e}")
            self.value_translation_rules = self.get_default_translation_rules()

    def get_default_translation_rules(self):
        """Возвращает набор правил перевода по умолчанию"""
        return [
            # Единицы измерения - длина
            {"active": True, "original": "mm", "translation": "мм", "category": "Длина", 
             "description": "Миллиметры", "examples": "5mm → 5 мм"},
            {"active": True, "original": "cm", "translation": "см", "category": "Длина",
             "description": "Сантиметры", "examples": "10cm → 10 см"},
            {"active": True, "original": "m", "translation": "м", "category": "Длина",
             "description": "Метры", "examples": "1.5m → 1.5 м"},
            {"active": True, "original": "km", "translation": "км", "category": "Длина",
             "description": "Километры", "examples": "5km → 5 км"},
            {"active": True, "original": "in", "translation": "дюйм", "category": "Длина",
             "description": "Дюймы", "examples": '24in → 24"'},
            
            # Единицы измерения - вес
            {"active": True, "original": "mg", "translation": "мг", "category": "Вес",
             "description": "Миллиграммы", "examples": "500mg → 500 мг"},
            {"active": True, "original": "g", "translation": "г", "category": "Вес",
             "description": "Граммы", "examples": "250g → 250 г"},
            {"active": True, "original": "kg", "translation": "кг", "category": "Вес",
             "description": "Килограммы", "examples": "1.5kg → 1.5 кг"},
            
            # Единицы измерения - электричество
            {"active": True, "original": "V", "translation": "В", "category": "Электричество",
             "description": "Вольты", "examples": "220V → 220 В"},
            {"active": True, "original": "A", "translation": "А", "category": "Электричество",
             "description": "Амперы", "examples": "2A → 2 А"},
            {"active": True, "original": "W", "translation": "Вт", "category": "Электричество",
             "description": "Ватты", "examples": "100W → 100 Вт"},
            {"active": True, "original": "Wh", "translation": "Вт⋅ч", "category": "Электричество",
             "description": "Ватт-часы", "examples": "5000Wh → 5000 Вт⋅ч"},
            {"active": True, "original": "kWh", "translation": "кВт⋅ч", "category": "Электричество",
             "description": "Киловатт-часы", "examples": "10kWh → 10 кВт⋅ч"},
            
            # Единицы измерения - частота
            {"active": True, "original": "Hz", "translation": "Гц", "category": "Частота",
             "description": "Герцы", "examples": "50Hz → 50 Гц"},
            {"active": True, "original": "kHz", "translation": "кГц", "category": "Частота",
             "description": "Килогерцы", "examples": "20kHz → 20 кГц"},
            {"active": True, "original": "MHz", "translation": "МГц", "category": "Частота",
             "description": "Мегагерцы", "examples": "2.4MHz → 2.4 МГц"},
            {"active": True, "original": "GHz", "translation": "ГГц", "category": "Частота",
             "description": "Гигагерцы", "examples": "2.4GHz → 2.4 ГГц"},
            
            # 🆕 ИСПРАВЛЕНО: Добавлены единицы скорости передачи данных
            {"active": True, "original": "bps", "translation": "бит/с", "category": "Скорость",
             "description": "Биты в секунду", "examples": "1000bps → 1000 бит/с"},
            {"active": True, "original": "kbps", "translation": "кбит/с", "category": "Скорость",
             "description": "Килобиты в секунду", "examples": "32kbps → 32 кбит/с"},
            {"active": True, "original": "Mbps", "translation": "Мбит/с", "category": "Скорость",
             "description": "Мегабиты в секунду", "examples": "16Mbps → 16 Мбит/с"},
            {"active": True, "original": "Gbps", "translation": "Гбит/с", "category": "Скорость",
             "description": "Гигабиты в секунду", "examples": "1Gbps → 1 Гбит/с"},
            {"active": True, "original": "fps", "translation": "к/с", "category": "Скорость",
             "description": "Кадры в секунду", "examples": "30fps → 30 к/с"},
            
            # Единицы измерения - освещенность
            {"active": True, "original": "lux", "translation": "лк", "category": "Освещенность",
             "description": "Люксы", "examples": "500lux → 500 лк"},
            {"active": True, "original": "lm", "translation": "лм", "category": "Освещенность",
             "description": "Люмены", "examples": "800lm → 800 лм"},
            
            # Технические термины
            {"active": True, "original": "Support", "translation": "Поддержка", "category": "Термины",
             "description": "Поддержка функции", "examples": "Support → Поддержка"},
            {"active": True, "original": "Supported", "translation": "Поддерживается", "category": "Термины",
             "description": "Поддерживается", "examples": "Supported → Поддерживается"},
            {"active": True, "original": "Auto", "translation": "Автоматически", "category": "Термины",
             "description": "Автоматический режим", "examples": "Auto → Автоматически"},
            {"active": True, "original": "Manual", "translation": "Вручную", "category": "Термины",
             "description": "Ручной режим", "examples": "Manual → Вручную"},
            
            # Да/Нет
            {"active": True, "original": "Yes", "translation": "Да", "category": "Логические",
             "description": "Утвердительный ответ", "examples": "Yes → Да"},
            {"active": True, "original": "No", "translation": "Нет", "category": "Логические",
             "description": "Отрицательный ответ", "examples": "No → Нет"},
            {"active": True, "original": "True", "translation": "Истина", "category": "Логические",
             "description": "Логическая истина", "examples": "True → Истина"},
            {"active": True, "original": "False", "translation": "Ложь", "category": "Логические",
             "description": "Логическая ложь", "examples": "False → Ложь"},
        ]

    def save_value_translation_rules(self):
        """Сохраняет правила перевода значений в файл"""
        try:
            with open("value_translation_rules.json", "w", encoding="utf-8") as f:
                json.dump(self.value_translation_rules, f, ensure_ascii=False, indent=2)
            print(f"💾 Сохранено {len(self.value_translation_rules)} правил перевода")
            
            # 🆕 ДИНАМИЧЕСКОЕ ОБНОВЛЕНИЕ основной таблицы v4.8
            self.dynamic_update_main_table()
            
        except Exception as e:
            print(f"❌ Ошибка при сохранении правил перевода: {e}")
    
    def dynamic_update_main_table(self):
        """🆕 ДИНАМИЧЕСКОЕ обновление основной таблицы при изменении правил v4.8"""
        try:
            if not hasattr(self, 'tree') or not self.tree.get_children():
                print("ℹ️ Основная таблица пуста - обновление не требуется")
                return
            
            print("🔄 ДИНАМИЧЕСКОЕ ОБНОВЛЕНИЕ основной таблицы...")
            
            updated_count = 0
            
            # Проходим по всем строкам основной таблицы
            for item in self.tree.get_children():
                values = self.tree.item(item, "values")
                if len(values) >= 6:
                    # Получаем исходные значения (индекс 2)
                    original_values = values[2]
                    delimiter = values[4] if values[4] else ";"
                    
                    # 🆕 УЛУЧШЕННЫЙ пересчет "Результат разделения" v4.8.2
                    try:
                        if not original_values:
                            new_split_result = ""
                        else:
                            print(f"    🔄 Пересчитываем: '{original_values}' (разделитель: '{delimiter}')")
                            
                            # Разделяем значения по разделителю
                            parts = [p.strip() for p in original_values.split(delimiter) if p.strip()]
                            print(f"    📋 Разделенные части: {parts}")
                            
                            # Базовая очистка от системных символов
                            clean_parts = []
                            for part in parts:
                                if part and part not in ["/", ";", ",", "-"]:
                                    clean_parts.append(part)
                            print(f"    📋 Очищенные части: {clean_parts}")
                            
                            # Удаление дубликатов с сохранением порядка
                            unique_parts = []
                            seen = set()
                            for part in clean_parts:
                                if part.lower() not in seen:
                                    seen.add(part.lower())
                                    unique_parts.append(part)
                            print(f"    📋 Уникальные части: {unique_parts}")
                            
                            # 🆕 ОБЯЗАТЕЛЬНОЕ применение переводов v4.8.2
                            if hasattr(self, 'value_translation_rules') and self.value_translation_rules:
                                try:
                                    print(f"    🌐 Применяем переводы к: {unique_parts}")
                                    translated_parts = self.apply_value_translation_rules(unique_parts)
                                    print(f"    🌐 После переводов: {translated_parts}")
                                    unique_parts = translated_parts
                                except Exception as translate_error:
                                    print(f"    ❌ Ошибка переводов: {translate_error}")
                            
                            new_split_result = "; ".join(unique_parts)
                            print(f"    ✅ Новый результат: '{new_split_result}'")
                            
                    except Exception as e:
                        print(f"  ❌ Ошибка пересчета для '{original_values}': {e}")
                        new_split_result = values[5]  # Оставляем как было
                    
                    # Проверяем, изменился ли результат
                    if new_split_result != values[5]:
                        # Обновляем строку в таблице
                        new_values = list(values)
                        new_values[5] = new_split_result
                        self.tree.item(item, values=new_values)
                        updated_count += 1
                        print(f"  🔄 Обновлено: '{values[5]}' → '{new_split_result}'")
            
            if updated_count > 0:
                print(f"✅ ДИНАМИЧЕСКОЕ ОБНОВЛЕНИЕ: обновлено {updated_count} строк")
                if hasattr(self, 'info_label'):
                    self.info_label.config(text=f"✅ Динамически обновлено {updated_count} строк")
            else:
                print("ℹ️ ДИНАМИЧЕСКОЕ ОБНОВЛЕНИЕ: изменений не требуется")
                
        except Exception as e:
            print(f"❌ Ошибка динамического обновления: {e}")

    def is_protected_abbreviation(self, text):
        """🆕 Проверяет, является ли текст защищенной аббревиатурой v4.8"""
        if not text:
            return False
        
        # Проверяем точное совпадение
        if text in PROTECTED_ABBREVIATIONS:
            return True
        
        # Проверяем совпадение без учета регистра
        text_upper = text.upper()
        if text_upper in PROTECTED_ABBREVIATIONS:
            return True
        
        return False

    def load_ip_surveillance_units(self):
        """🆕 Загружает расширенную базу единиц измерений IP-видеонаблюдения"""
        try:
            # Попробуем загрузить из data/ папки
            units_files = [
                "data/ip_surveillance_units_compact.json",
                "data/ip_surveillance_units_editor.json",
                "ip_surveillance_units_compact.json",
                "ip_surveillance_units_editor.json"
            ]
            
            loaded = False
            for units_file in units_files:
                if os.path.exists(units_file):
                    with open(units_file, "r", encoding="utf-8") as f:
                        self.ip_surveillance_units = json.load(f)
                    print(f"✅ Загружена расширенная база единиц измерений из {units_file}")
                    
                    # Интегрируем единицы в правила перевода
                    self.integrate_ip_surveillance_units()
                    loaded = True
                    break
            
            if not loaded:
                print("ℹ️ Файлы единиц измерений IP-видеонаблюдения не найдены")
                print("   Будет использована базовая система единиц измерений")
                self.ip_surveillance_units = {}
                
        except Exception as e:
            print(f"❌ Ошибка при загрузке единиц измерений IP-видеонаблюдения: {e}")
            self.ip_surveillance_units = {}

    def integrate_ip_surveillance_units(self):
        """🆕 Интегрирует единицы измерений IP-видеонаблюдения в систему переводов"""
        try:
            if not self.ip_surveillance_units:
                return
            
            added_count = 0
            # Получаем существующие оригинальные значения
            existing_originals = {rule.get('original', '').lower() for rule in self.value_translation_rules}
            
            # Обрабатываем единицы из компактного формата
            if 'units' in self.ip_surveillance_units:
                for original, translation in self.ip_surveillance_units['units'].items():
                    if original.lower() not in existing_originals:
                        # Определяем категорию на основе единицы
                        category = self.determine_unit_category(original)
                        
                        new_rule = {
                            "active": True,
                            "original": original,
                            "translation": translation,
                            "category": category,
                            "description": f"Автоперевод IP-видеонаблюдение: {original}",
                            "examples": f"{original} → {translation}"
                        }
                        self.value_translation_rules.append(new_rule)
                        existing_originals.add(original.lower())
                        added_count += 1
            
            # Обрабатываем единицы из полного формата
            elif 'unit_variants' in self.ip_surveillance_units:
                for unit, data in self.ip_surveillance_units['unit_variants'].items():
                    if unit.lower() not in existing_originals:
                        new_rule = {
                            "active": True,
                            "original": unit,
                            "translation": data.get('russian', unit),
                            "category": data.get('category', 'IP-видеонаблюдение'),
                            "description": f"Автоперевод: {data.get('unit_type', 'general')}",
                            "examples": f"{unit} → {data.get('russian', unit)}"
                        }
                        self.value_translation_rules.append(new_rule)
                        existing_originals.add(unit.lower())
                        added_count += 1
            
            if added_count > 0:
                print(f"🌐 Автоматически добавлено {added_count} правил перевода единиц IP-видеонаблюдения")
                self.save_value_translation_rules()
            else:
                print("ℹ️ Все единицы IP-видеонаблюдения уже присутствуют в правилах перевода")
                
        except Exception as e:
            print(f"❌ Ошибка при интеграции единиц IP-видеонаблюдения: {e}")

    def determine_unit_category(self, unit):
        """🆕 Определяет категорию единицы измерения"""
        unit_lower = unit.lower()
        
        # Электричество
        if any(x in unit_lower for x in ['v', 'volt', 'a', 'amp', 'w', 'watt', 'wh', 'ohm', 'ma', 'mv', 'kw', 'kv']):
            return "Электричество"
        
        # Данные и память
        if any(x in unit_lower for x in ['b', 'byte', 'kb', 'mb', 'gb', 'tb', 'pb', 'bit']):
            return "Данные"
        
        # Скорость передачи
        if any(x in unit_lower for x in ['bps', 'kbps', 'mbps', 'gbps']):
            return "Скорость"
        
        # Время
        if any(x in unit_lower for x in ['s', 'sec', 'ms', 'μs', 'ns', 'min', 'h', 'hour', 'day']):
            return "Время"
        
        # Частота
        if any(x in unit_lower for x in ['hz', 'khz', 'mhz', 'ghz', 'fps']):
            return "Частота"
        
        # Размеры
        if any(x in unit_lower for x in ['mm', 'cm', 'm', 'km', 'in', 'ft', 'μm', 'nm']):
            return "Длина"
        
        # Вес
        if any(x in unit_lower for x in ['g', 'kg', 'mg', 'lb', 'oz']):
            return "Вес"
        
        # Звук
        if any(x in unit_lower for x in ['db', 'dba', 'dbc', 'dbi', 'dbm']):
            return "Звук"
        
        # Освещение
        if any(x in unit_lower for x in ['lux', 'lm', 'cd', 'nit']):
            return "Освещенность"
        
        # Температура
        if any(x in unit_lower for x in ['°c', '°f', 'c', 'k', 'kelvin', 'celsius']):
            return "Температура"
        
        # Изображения
        if any(x in unit_lower for x in ['mp', 'px', 'pixel', 'dpi', 'ppi', 'megapixel']):
            return "Разрешение"
        
        # Давление
        if any(x in unit_lower for x in ['pa', 'kpa', 'mpa', 'bar', 'psi', 'atm', 'mmhg']):
            return "Давление"
        
        # Углы
        if any(x in unit_lower for x in ['°', 'degree', 'fov', 'aov']):
            return "Угол"
        
        return "IP-видеонаблюдение"

    def refresh_translation_rules_display(self):
        """Обновляет отображение правил перевода в таблице"""
        # Очищаем таблицу
        for item in self.translation_tree.get_children():
            self.translation_tree.delete(item)
        
        # Заполняем таблицу
        for rule in self.value_translation_rules:
            active_text = "✓" if rule.get("active", True) else "✗"
            
            self.translation_tree.insert("", "end", values=(
                active_text,
                rule.get("original", ""),
                rule.get("translation", ""),
                rule.get("category", ""),
                rule.get("description", ""),
                rule.get("examples", "")
            ))
        
        # Обновляем статистику
        total_rules = len(self.value_translation_rules)
        active_rules = sum(1 for rule in self.value_translation_rules if rule.get("active", True))
        self.translation_stats_label.config(text=f"Правил: {total_rules} | Активных: {active_rules}")

    def add_value_translation_rule(self):
        """Добавляет новое правило перевода значений"""
        dialog = ValueTranslationRuleDialog(self.root, None)
        if dialog.result:
            self.value_translation_rules.append(dialog.result)
            self.save_value_translation_rules()
            self.refresh_translation_rules_display()

    def edit_value_translation_rule(self):
        """Редактирует выбранное правило перевода"""
        selection = self.translation_tree.selection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Выберите правило для редактирования")
            return
        
        # Получаем индекс выбранного элемента
        item = selection[0]
        index = self.translation_tree.index(item)
        
        if 0 <= index < len(self.value_translation_rules):
            rule = self.value_translation_rules[index]
            dialog = ValueTranslationRuleDialog(self.root, rule)
            if dialog.result:
                self.value_translation_rules[index] = dialog.result
                self.save_value_translation_rules()
                self.refresh_translation_rules_display()

    def delete_value_translation_rule(self):
        """Удаляет выбранное правило перевода"""
        selection = self.translation_tree.selection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Выберите правило для удаления")
            return
        
        item = selection[0]
        index = self.translation_tree.index(item)
        
        if 0 <= index < len(self.value_translation_rules):
            rule = self.value_translation_rules[index]
            if messagebox.askyesno("Подтверждение", 
                                 f"Удалить правило перевода?\n\n'{rule.get('original', '')}' → '{rule.get('translation', '')}'"):
                del self.value_translation_rules[index]
                self.save_value_translation_rules()
                self.refresh_translation_rules_display()

    def auto_detect_units(self):
        """🔍 Автоматически находит единицы измерения в данных"""
        if not hasattr(self, 'tree') or not self.tree.get_children():
            messagebox.showwarning("Предупреждение", "Сначала обработайте данные")
            return
        
        # Собираем все значения из столбца "Результат разделения"
        all_values = []
        for item in self.tree.get_children():
            values = self.tree.item(item, 'values')
            if len(values) >= 6:
                split_results = values[5]
                if split_results:
                    split_values = [v.strip() for v in re.split(r'[;,|]', split_results) if v.strip()]
                    all_values.extend(split_values)
        
        if not all_values:
            messagebox.showinfo("Информация", "Нет данных для анализа")
            return
        
        # Расширенные паттерны для поиска единиц измерения IP-видеонаблюдения
        unit_patterns = {
            # === ФИЗИЧЕСКИЕ РАЗМЕРЫ ===
            r'\b\d+\.?\d*\s*mm\b': ('mm', 'мм', 'Длина'),
            r'\b\d+\.?\d*\s*cm\b': ('cm', 'см', 'Длина'),
            r'\b\d+\.?\d*\s*m\b': ('m', 'м', 'Длина'),
            r'\b\d+\.?\d*\s*km\b': ('km', 'км', 'Длина'),
            r'\b\d+\.?\d*\s*in\b': ('in', 'дюйм', 'Длина'),
            r'\b\d+\.?\d*\s*ft\b': ('ft', 'фут', 'Длина'),
            r'\b\d+\.?\d*\s*μm\b': ('μm', 'мкм', 'Длина'),
            r'\b\d+\.?\d*\s*nm\b': ('nm', 'нм', 'Длина'),
            
            # === ВЕС ===
            r'\b\d+\.?\d*\s*kg\b': ('kg', 'кг', 'Вес'),
            r'\b\d+\.?\d*\s*g\b': ('g', 'г', 'Вес'),
            r'\b\d+\.?\d*\s*lb\b': ('lb', 'фунт', 'Вес'),
            r'\b\d+\.?\d*\s*oz\b': ('oz', 'унция', 'Вес'),
            
            # === ЭЛЕКТРИЧЕСТВО ===
            r'\b\d+\.?\d*\s*V\b': ('V', 'В', 'Электричество'),
            r'\b\d+\.?\d*\s*mV\b': ('mV', 'мВ', 'Электричество'),
            r'\b\d+\.?\d*\s*kV\b': ('kV', 'кВ', 'Электричество'),
            r'\b\d+\.?\d*\s*W\b': ('W', 'Вт', 'Электричество'),
            r'\b\d+\.?\d*\s*mW\b': ('mW', 'мВт', 'Электричество'),
            r'\b\d+\.?\d*\s*kW\b': ('kW', 'кВт', 'Электричество'),
            r'\b\d+\.?\d*\s*Wh\b': ('Wh', 'Вт⋅ч', 'Электричество'),
            r'\b\d+\.?\d*\s*kWh\b': ('kWh', 'кВт⋅ч', 'Электричество'),
            r'\b\d+\.?\d*\s*A\b': ('A', 'А', 'Электричество'),
            r'\b\d+\.?\d*\s*mA\b': ('mA', 'мА', 'Электричество'),
            r'\b\d+\.?\d*\s*μA\b': ('μA', 'мкА', 'Электричество'),
            r'\b\d+\.?\d*\s*mAh\b': ('mAh', 'мАч', 'Электричество'),
            r'\b\d+\.?\d*\s*Ohm\b': ('Ohm', 'Ом', 'Электричество'),
            r'\b\d+\.?\d*\s*kOhm\b': ('kOhm', 'кОм', 'Электричество'),
            
            # === ЧАСТОТА И ВРЕМЯ ===
            r'\b\d+\.?\d*\s*Hz\b': ('Hz', 'Гц', 'Частота'),
            r'\b\d+\.?\d*\s*kHz\b': ('kHz', 'кГц', 'Частота'),
            r'\b\d+\.?\d*\s*MHz\b': ('MHz', 'МГц', 'Частота'),
            r'\b\d+\.?\d*\s*GHz\b': ('GHz', 'ГГц', 'Частота'),
            r'\b\d+\.?\d*\s*fps\b': ('fps', 'кадр/с', 'Видео'),
            r'\b\d+\.?\d*\s*s\b': ('s', 'с', 'Время'),
            r'\b\d+\.?\d*\s*ms\b': ('ms', 'мс', 'Время'),
            r'\b\d+\.?\d*\s*μs\b': ('μs', 'мкс', 'Время'),
            r'\b\d+\.?\d*\s*ns\b': ('ns', 'нс', 'Время'),
            r'\b\d+\.?\d*\s*min\b': ('min', 'мин', 'Время'),
            r'\b\d+\.?\d*\s*h\b': ('h', 'ч', 'Время'),
            
            # === ПАМЯТЬ И ДАННЫЕ ===
            r'\b\d+\.?\d*\s*B\b': ('B', 'Б', 'Данные'),
            r'\b\d+\.?\d*\s*KB\b': ('KB', 'КБ', 'Данные'),
            r'\b\d+\.?\d*\s*MB\b': ('MB', 'МБ', 'Данные'),
            r'\b\d+\.?\d*\s*GB\b': ('GB', 'ГБ', 'Данные'),
            r'\b\d+\.?\d*\s*TB\b': ('TB', 'ТБ', 'Данные'),
            r'\b\d+\.?\d*\s*PB\b': ('PB', 'ПБ', 'Данные'),
            
            # === СКОРОСТЬ ПЕРЕДАЧИ ДАННЫХ ===
            r'\b\d+\.?\d*\s*bps\b': ('bps', 'бит/с', 'Скорость'),
            r'\b\d+\.?\d*\s*kbps\b': ('kbps', 'кбит/с', 'Скорость'),
            r'\b\d+\.?\d*\s*Mbps\b': ('Mbps', 'Мбит/с', 'Скорость'),
            r'\b\d+\.?\d*\s*Gbps\b': ('Gbps', 'Гбит/с', 'Скорость'),
            r'\b\d+\.?\d*\s*Tbps\b': ('Tbps', 'Тбит/с', 'Скорость'),
            
            # === ИЗОБРАЖЕНИЯ И ВИДЕО ===
            r'\b\d+\.?\d*\s*mp\b': ('mp', 'мп', 'Разрешение'),
            r'\b\d+\.?\d*\s*MP\b': ('MP', 'МП', 'Разрешение'),
            r'\b\d+\.?\d*\s*px\b': ('px', 'пикс', 'Разрешение'),
            r'\b\d+\.?\d*\s*dpi\b': ('dpi', 'точек/дюйм', 'Разрешение'),
            r'\b\d+\.?\d*\s*ppi\b': ('ppi', 'пикс/дюйм', 'Разрешение'),
            r'\b\d+\.?\d*\s*lpi\b': ('lpi', 'линий/дюйм', 'Разрешение'),
            
            # === ЗВУК И ОСВЕЩЕННОСТЬ ===
            r'\b\d+\.?\d*\s*dB\b': ('dB', 'дБ', 'Звук'),
            r'\b\d+\.?\d*\s*dBA\b': ('dBA', 'дБА', 'Звук'),
            r'\b\d+\.?\d*\s*dBC\b': ('dBC', 'дБС', 'Звук'),
            r'\b\d+\.?\d*\s*dBi\b': ('dBi', 'дБи', 'Звук'),
            r'\b\d+\.?\d*\s*dBm\b': ('dBm', 'дБм', 'Звук'),
            r'\b\d+\.?\d*\s*dBW\b': ('dBW', 'дБВт', 'Звук'),
            r'\b\d+\.?\d*\s*lux\b': ('lux', 'лк', 'Освещенность'),
            r'\b\d+\.?\d*\s*lm\b': ('lm', 'лм', 'Освещенность'),
            r'\b\d+\.?\d*\s*cd\b': ('cd', 'кд', 'Освещенность'),
            r'\b\d+\.?\d*\s*nit\b': ('nit', 'нит', 'Освещенность'),
            
            # === ТЕМПЕРАТУРА (только Цельсий!) ===
            r'\b\d+\.?\d*\s*°C\b': ('°C', '°C', 'Температура'),
            r'\b\d+\.?\d*\s*C\b': ('C', '°C', 'Температура'),
            r'\b\d+\.?\d*\s*K\b': ('K', 'К', 'Температура'),
            
            # === ДАВЛЕНИЕ И ВЛАЖНОСТЬ ===
            r'\b\d+\.?\d*\s*Pa\b': ('Pa', 'Па', 'Давление'),
            r'\b\d+\.?\d*\s*kPa\b': ('kPa', 'кПа', 'Давление'),
            r'\b\d+\.?\d*\s*MPa\b': ('MPa', 'МПа', 'Давление'),
            r'\b\d+\.?\d*\s*bar\b': ('bar', 'бар', 'Давление'),
            r'\b\d+\.?\d*\s*mbar\b': ('mbar', 'мбар', 'Давление'),
            r'\b\d+\.?\d*\s*psi\b': ('psi', 'фунт/кв.дюйм', 'Давление'),
            r'\b\d+\.?\d*\s*atm\b': ('atm', 'атм', 'Давление'),
            r'\b\d+\.?\d*\s*mmHg\b': ('mmHg', 'мм рт.ст.', 'Давление'),
            r'\b\d+\.?\d*\s*%RH\b': ('%RH', '%ОВ', 'Влажность'),
            r'\b\d+\.?\d*\s*RH\b': ('RH', '%ОВ', 'Влажность'),
            
            # === УГЛЫ ===
            r'\b\d+\.?\d*\s*°\b': ('°', '°', 'Угол'),
            r'\b\d+\.?\d*\s*degrees?\b': ('degree', '°', 'Угол'),
            
            # === СПЕЦИАЛЬНЫЕ ЕДИНИЦЫ ВИДЕОНАБЛЮДЕНИЯ ===
            r'\b\d+\.?\d*\s*FOV\b': ('FOV', 'поле зрения', 'Оптика'),
            r'\b\d+\.?\d*\s*AOV\b': ('AOV', 'угол обзора', 'Оптика'),
            r'\b\d+\.?\d*\s*HFOV\b': ('HFOV', 'гориз. обзор', 'Оптика'),
            r'\b\d+\.?\d*\s*VFOV\b': ('VFOV', 'верт. обзор', 'Оптика'),
            
            # === РАЗРЕШЕНИЯ ВИДЕО ===
            r'\b1920x1080\b': ('1920x1080', 'FullHD', 'Разрешение'),
            r'\b3840x2160\b': ('3840x2160', '4K', 'Разрешение'),
            r'\b1280x720\b': ('1280x720', 'HD', 'Разрешение'),
            r'\b\d+x\d+\b': ('resolution', 'разрешение', 'Разрешение'),
        }
        
        found_units = {}
        examples = {}
        
        # Ищем единицы в значениях
        for value in all_values:
            for pattern, (unit, translation, category) in unit_patterns.items():
                if re.search(pattern, value, re.IGNORECASE):
                    if unit not in found_units:
                        found_units[unit] = (translation, category)
                        examples[unit] = []
                    if value not in examples[unit] and len(examples[unit]) < 3:
                        examples[unit].append(value)
        
        if not found_units:
            messagebox.showinfo("Результат", "Единицы измерения не найдены")
            return
        
        # Проверяем, какие единицы уже есть в правилах
        existing_units = {rule['original'] for rule in self.value_translation_rules}
        new_units = {}
        
        for unit, (translation, category) in found_units.items():
            if unit not in existing_units:
                new_units[unit] = (translation, category, examples[unit])
        
        if not new_units:
            messagebox.showinfo("Результат", 
                              f"Найдено единиц: {len(found_units)}\nВсе найденные единицы уже есть в правилах")
            return
        
        # Показываем диалог с предложением добавить новые правила
        message = f"🔍 Найдено новых единиц измерения: {len(new_units)}\n\n"
        for unit, (translation, category, unit_examples) in new_units.items():
            message += f"• {unit} → {translation} ({category})\n"
            if unit_examples:
                message += f"  Примеры: {', '.join(unit_examples[:2])}\n"
        
        message += f"\nДобавить эти правила перевода?"
        
        if messagebox.askyesno("Найденные единицы измерения", message):
            # Добавляем новые правила
            for unit, (translation, category, unit_examples) in new_units.items():
                new_rule = {
                    "active": True,
                    "original": unit,
                    "translation": translation,
                    "category": category,
                    "description": f"Единица измерения: {translation}",
                    "examples": f"{unit_examples[0] if unit_examples else f'100{unit}'} → {unit_examples[0].replace(unit, translation) if unit_examples else f'100 {translation}'}"
                }
                self.value_translation_rules.append(new_rule)
            
            self.save_value_translation_rules()
            self.refresh_translation_rules_display()
            
            messagebox.showinfo("Успешно", f"Добавлено {len(new_units)} новых правил перевода")

    def activate_all_translation_rules(self):
        """Активирует все правила перевода"""
        for rule in self.value_translation_rules:
            rule["active"] = True
        self.save_value_translation_rules()
        self.refresh_translation_rules_display()

    def deactivate_all_translation_rules(self):
        """Деактивирует все правила перевода"""
        for rule in self.value_translation_rules:
            rule["active"] = False
        self.save_value_translation_rules()
        self.refresh_translation_rules_display()

    def on_translation_tree_click(self, event):
        """🆕 Обработка клика по таблице переводов v4.8"""
        # Обновляем счетчик выбранных элементов
        if hasattr(self, 'selected_count_label'):
            selected_count = len(self.translation_tree.selection())
            self.selected_count_label.config(text=f"Выбрано: {selected_count}")

    def activate_selected_rules(self):
        """🆕 Активирует только выбранные правила v4.8"""
        selection = self.translation_tree.selection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Выберите правила для активации")
            return
        
        activated_count = 0
        for item in selection:
            index = self.translation_tree.index(item)
            if index < len(self.value_translation_rules):
                if not self.value_translation_rules[index].get("active", True):
                    self.value_translation_rules[index]["active"] = True
                    activated_count += 1
        
        if activated_count > 0:
            self.save_value_translation_rules()
            self.refresh_translation_rules_display()
            print(f"✅ Активировано {activated_count} правил")
        else:
            print("ℹ️ Все выбранные правила уже активны")

    def deactivate_selected_rules(self):
        """🆕 Деактивирует только выбранные правила v4.8"""
        selection = self.translation_tree.selection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Выберите правила для деактивации")
            return
        
        deactivated_count = 0
        for item in selection:
            index = self.translation_tree.index(item)
            if index < len(self.value_translation_rules):
                if self.value_translation_rules[index].get("active", True):
                    self.value_translation_rules[index]["active"] = False
                    deactivated_count += 1
        
        if deactivated_count > 0:
            self.save_value_translation_rules()
            self.refresh_translation_rules_display()
            print(f"✅ Деактивировано {deactivated_count} правил")
        else:
            print("ℹ️ Все выбранные правила уже неактивны")

    def export_value_translations(self):
        """Экспортирует правила перевода в файл"""
        filename = filedialog.asksaveasfilename(
            title="Экспорт правил перевода",
            defaultextension=".json",
            filetypes=[("JSON файлы", "*.json"), ("Все файлы", "*.*")]
        )
        
        if filename:
            try:
                with open(filename, "w", encoding="utf-8") as f:
                    json.dump(self.value_translation_rules, f, ensure_ascii=False, indent=2)
                messagebox.showinfo("Успешно", f"Правила экспортированы в:\n{filename}")
            except Exception as e:
                messagebox.showerror("Ошибка", f"Ошибка экспорта:\n{e}")

    def import_value_translations(self):
        """Импортирует правила перевода из файла"""
        filename = filedialog.askopenfilename(
            title="Импорт правил перевода",
            filetypes=[("JSON файлы", "*.json"), ("Все файлы", "*.*")]
        )
        
        if filename:
            try:
                with open(filename, "r", encoding="utf-8") as f:
                    imported_rules = json.load(f)
                
                # Проверяем формат данных
                if not isinstance(imported_rules, list):
                    messagebox.showerror("Ошибка", "Неверный формат файла")
                    return
                
                # Добавляем импортированные правила
                existing_originals = {rule['original'] for rule in self.value_translation_rules}
                added_count = 0
                
                for rule in imported_rules:
                    if isinstance(rule, dict) and 'original' in rule and 'translation' in rule:
                        if rule['original'] not in existing_originals:
                            # Заполняем отсутствующие поля значениями по умолчанию
                            complete_rule = {
                                "active": rule.get("active", True),
                                "original": rule["original"],
                                "translation": rule["translation"],
                                "category": rule.get("category", "Импорт"),
                                "description": rule.get("description", "Импортированное правило"),
                                "examples": rule.get("examples", f"{rule['original']} → {rule['translation']}")
                            }
                            self.value_translation_rules.append(complete_rule)
                            existing_originals.add(rule['original'])
                            added_count += 1
                
                self.save_value_translation_rules()
                self.refresh_translation_rules_display()
                
                messagebox.showinfo("Успешно", f"Импортировано {added_count} новых правил")
                
            except Exception as e:
                messagebox.showerror("Ошибка", f"Ошибка импорта:\n{e}")

    def show_translation_context_menu(self, event):
        """Показывает контекстное меню для правил перевода"""
        # Определяем элемент под курсором
        item = self.translation_tree.identify('item', event.x, event.y)
        if item:
            self.translation_tree.selection_set(item)
            
            # Создаем контекстное меню
            context_menu = Menu(self.root, tearoff=0)
            context_menu.add_command(label="✏️ Редактировать", command=self.edit_value_translation_rule)
            context_menu.add_command(label="🗑️ Удалить", command=self.delete_value_translation_rule)
            context_menu.add_separator()
            
            # Получаем текущее состояние правила
            index = self.translation_tree.index(item)
            if 0 <= index < len(self.value_translation_rules):
                rule = self.value_translation_rules[index]
                is_active = rule.get("active", True)
                
                if is_active:
                    context_menu.add_command(label="✗ Деактивировать", 
                                           command=lambda: self.toggle_translation_rule(index))
                else:
                    context_menu.add_command(label="✓ Активировать", 
                                           command=lambda: self.toggle_translation_rule(index))
            
            context_menu.tk_popup(event.x_root, event.y_root)

    def toggle_translation_rule(self, index):
        """Переключает активность правила перевода"""
        if 0 <= index < len(self.value_translation_rules):
            rule = self.value_translation_rules[index]
            rule["active"] = not rule.get("active", True)
            self.save_value_translation_rules()
            self.refresh_translation_rules_display()

    def load_merge_settings(self):
        """Загрузка настроек объединения"""
        # Загрузка настроек объединения параметров
        if os.path.exists(self.PARAM_MERGE_SETTINGS_FILE):
            try:
                with open(self.PARAM_MERGE_SETTINGS_FILE, "r", encoding="utf-8") as f:
                    self.param_merge_settings = json.load(f)
            except Exception as e:
                print(f"Ошибка загрузки настроек объединения параметров: {e}")
                self.param_merge_settings = {}
        else:
            self.param_merge_settings = {}

        # Загрузка настроек объединения значений
        if os.path.exists(self.VALUE_MERGE_SETTINGS_FILE):
            try:
                with open(self.VALUE_MERGE_SETTINGS_FILE, "r", encoding="utf-8") as f:
                    self.value_merge_settings = json.load(f)
            except Exception as e:
                print(f"Ошибка загрузки настроек объединения значений: {e}")
                self.value_merge_settings = {}
        else:
            self.value_merge_settings = {}

    def save_merge_settings(self):
        """Сохранение настроек объединения"""
        try:
            with open(self.PARAM_MERGE_SETTINGS_FILE, "w", encoding="utf-8") as f:
                json.dump(self.param_merge_settings, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"Ошибка сохранения настроек объединения параметров: {e}")

        try:
            with open(self.VALUE_MERGE_SETTINGS_FILE, "w", encoding="utf-8") as f:
                json.dump(self.value_merge_settings, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"Ошибка сохранения настроек объединения значений: {e}")

    def show_help_dialog(self):
        """Показывает диалог справки с подробной инструкцией"""
        
        # Создаем диалоговое окно
        help_dialog = Toplevel(self.root)
        help_dialog.title("📚 Справка - Инструкция пользователя")
        help_dialog.geometry("800x600")
        help_dialog.transient(self.root)
        help_dialog.grab_set()
        
        # Центрируем диалог
        help_dialog.geometry("+%d+%d" % (self.root.winfo_rootx() + 100, self.root.winfo_rooty() + 50))
        
        # Создаем основной фрейм
        main_frame = Frame(help_dialog)
        main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
        
        # Заголовок
        header_frame = Frame(main_frame)
        header_frame.pack(fill=X, pady=(0, 10))
        
        title_label = Label(header_frame, text="📚 Инструкция пользователя", 
                           font=("Arial", 16, "bold"), fg="#2E7D32")
        title_label.pack()
        
        version_label = Label(header_frame, text="Редактор атрибутов товаров v4.2", 
                             font=("Arial", 10), fg="#666666")
        version_label.pack()
        
        # Навигация (кнопки быстрого перехода)
        nav_frame = LabelFrame(main_frame, text="🧭 Быстрая навигация", font=("Arial", 9, "bold"))
        nav_frame.pack(fill=X, pady=(0, 10))
        
        nav_buttons = Frame(nav_frame)
        nav_buttons.pack(fill=X, padx=5, pady=5)
        
        # Создаем кнопки навигации
        nav_sections = [
            ("🚀 Быстрый старт", "start"),
            ("🔧 Инструкция", "instruction"), 
            ("⚙️ Настройки", "settings"),
            ("💡 Примеры", "examples"),
            ("🔧 Проблемы", "problems"),
            ("⌨️ Клавиши", "hotkeys")
        ]
        
        for i, (text, section) in enumerate(nav_sections):
            btn = Button(nav_buttons, text=text, width=15, 
                        command=lambda s=section, ht=None: self.scroll_to_section_in_dialog(ht, s))
            btn.grid(row=i//3, column=i%3, padx=2, pady=2, sticky=W+E)
        
        # Настраиваем колонки для равномерного распределения
        for i in range(3):
            nav_buttons.grid_columnconfigure(i, weight=1)
        
        # Создаем текстовую область с прокруткой
        text_frame = Frame(main_frame)
        text_frame.pack(fill=BOTH, expand=True)
        
        # Текстовое поле
        help_text = Text(text_frame, wrap=WORD, font=("Arial", 10), 
                        padx=10, pady=10, bg="#FAFAFA")
        
        # Скроллбар
        scrollbar = Scrollbar(text_frame, orient=VERTICAL, command=help_text.yview)
        help_text.configure(yscrollcommand=scrollbar.set)
        
        # Размещение
        help_text.pack(side=LEFT, fill=BOTH, expand=True)
        scrollbar.pack(side=RIGHT, fill=Y)
        
        # Настройка стилей текста
        help_text.tag_configure("title", font=("Arial", 14, "bold"), foreground="#1976D2", spacing1=10, spacing3=5)
        help_text.tag_configure("subtitle", font=("Arial", 12, "bold"), foreground="#388E3C", spacing1=8, spacing3=3)
        help_text.tag_configure("header", font=("Arial", 11, "bold"), foreground="#F57C00", spacing1=6, spacing3=2)
        help_text.tag_configure("code", font=("Courier New", 9), background="#F5F5F5", foreground="#D32F2F")
        help_text.tag_configure("emphasis", font=("Arial", 10, "bold"), foreground="#7B1FA2")
        help_text.tag_configure("success", foreground="#2E7D32")
        help_text.tag_configure("warning", foreground="#F57C00")
        help_text.tag_configure("error", foreground="#D32F2F")
        
        # Словарь позиций секций для навигации
        section_positions = {}
        
        # Загружаем содержимое инструкции
        self.load_help_content_in_dialog(help_text, section_positions)
        
        # Обновляем команды кнопок навигации с правильными ссылками
        for i, (text, section) in enumerate(nav_sections):
            btn = nav_buttons.grid_slaves(row=i//3, column=i%3)[0]
            btn.config(command=lambda s=section: self.scroll_to_section_in_dialog(help_text, section_positions, s))
        
        # Делаем текст только для чтения
        help_text.config(state=DISABLED)
        
        # Кнопка закрытия
        close_frame = Frame(main_frame)
        close_frame.pack(fill=X, pady=(10, 0))
        
        close_btn = Button(close_frame, text="Закрыть", command=help_dialog.destroy, width=15)
        close_btn.pack(side=RIGHT)
        
        # Обработка клавиш
        help_dialog.bind('<Escape>', lambda e: help_dialog.destroy())
        help_dialog.focus_set()

    def scroll_to_section_in_dialog(self, help_text, section_positions, section):
        """Прокрутка к указанной секции в диалоге"""
        if section in section_positions:
            help_text.see(section_positions[section])
            # Подсвечиваем найденную секцию
            help_text.tag_add(SEL, section_positions[section], 
                             f"{section_positions[section]} lineend")

    def load_help_content_in_dialog(self, help_text, section_positions):
        """Загружает содержимое инструкции в диалог"""
        help_text.config(state=NORMAL)
        
        # Очищаем содержимое
        help_text.delete(1.0, END)
        
        content = '''🎯 НАЗНАЧЕНИЕ ПРОГРАММЫ

Редактор атрибутов товаров предназначен для автоматической обработки Excel файлов с HTML-характеристиками товаров и преобразования их в структурированный формат.

Что делает программа:
📥 Загружает Excel файлы с HTML-характеристиками
🔧 Парсит HTML и извлекает параметры товаров  
✂️ Разделяет значения параметров (например: "Красный, Синий" → "Красный; Синий")
🔄 Нормализует названия параметров (маппинг)
💾 Экспортирует результат в новый Excel файл

---

🚀 БЫСТРЫЙ СТАРТ

1. ПОДГОТОВКА ФАЙЛА
Ваш Excel файл должен содержать:
• Столбец с артикулами товаров (обычно первый)
• Столбец с HTML-характеристиками (указываете номер)  
• Дополнительные столбцы для сохранения (опционально)

2. ПРИМЕР ИСХОДНЫХ ДАННЫХ
Артикул | Название  | Характеристики (HTML)
TOV001  | Товар 1   | <tr><td>Цвет</td><td>Красный</td></tr>
TOV002  | Товар 2   | <tr><td>Размер</td><td>XL</td></tr>

3. РЕЗУЛЬТАТ ОБРАБОТКИ  
Артикул | Название  | Цвет     | Размер
TOV001  | Товар 1   | Красный  |
TOV002  | Товар 2   |          | XL

---

🔧 ПОШАГОВАЯ ИНСТРУКЦИЯ

ШАГ 1: Настройка файлов и путей

📁 ФАЙЛЫ И НАСТРОЙКИ:
1. Выберите исходный файл: нажмите 📂 рядом с полем "Файл"
2. Укажите папку для сохранения: нажмите 📂 рядом с полем "Папка"
3. Настройте столбцы:
   • Столбец: Номер столбца с HTML (отсчет с 1)
   • Доп.столбцы: Нажмите ⚙️ для выбора дополнительных столбцов

ШАГ 2: Загрузка и обработка

🔧 ДЕЙСТВИЯ:
1. 📥 Загрузить - загружает файл в память
2. ⚙️ Обработать - извлекает параметры из HTML  
3. 💾 Сохранить - экспортирует результат в Excel

ПРОЦЕСС ОБРАБОТКИ:
1. Нажмите 📥 Загрузить для загрузки файла
2. Нажмите ⚙️ Обработать для извлечения параметров
3. Проверьте результаты на вкладке "📋 Основная таблица"
4. При необходимости настройте маппинги параметров
5. Нажмите 💾 Сохранить для экспорта

---

⚙️ НАСТРОЙКИ И КОНФИГУРАЦИЯ

БЫСТРЫЕ НАСТРОЙКИ (панель инструментов):

🔄 ПРАВИЛА ЗАМЕН
• Настройка regex-правил для обработки текста
• Удаление лишних символов
• Нормализация форматирования
• Пример: Удаление версий \\sv?\\d+(\\.\\d+)*$ → (пустая строка)

Aa НОРМАЛИЗАЦИЯ РЕГИСТРА  
• Приведение к единому регистру
• "support", "Support", "SUPPORT" → "Support"
• Предотвращает дублирование из-за регистра

≈ СИНОНИМЫ ЗНАЧЕНИЙ
• Автоматическая замена синонимов
• "Yes" → "Supported", "No" → "Not Supported"  
• Унификация значений параметров

🛡️ ЗАЩИЩЕННЫЕ ЗНАЧЕНИЯ
• Значения, которые НЕ разделяются
• Например: "B/W", "I/O", "AC/DC"
• Остаются как есть, несмотря на разделители

---

💡 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ

ПРИМЕР 1: Обработка каталога товаров

ИСХОДНЫЕ ДАННЫЕ:
<table>
<tr><td>Бренд</td><td>Samsung</td></tr>
<tr><td>Модель</td><td>Galaxy S21</td></tr>  
<tr><td>Цвет</td><td>Черный, Белый</td></tr>
<tr><td>Память</td><td>128GB, 256GB</td></tr>
</table>

РЕЗУЛЬТАТ:
Артикул | Бренд   | Модель     | Цвет           | Память
PHONE01 | Samsung | Galaxy S21 | Черный; Белый  | 128GB; 256GB

ПРИМЕР 2: Маппинг параметров

ПРОБЛЕМА: Разные названия одного параметра
"Цвет корпуса" → "Красный"
"Color" → "Blue"
"Расцветка" → "Зеленый"

РЕШЕНИЕ: Создать маппинги
1. Цвет корпуса → Цвет
2. Color → Цвет  
3. Расцветка → Цвет

РЕЗУЛЬТАТ: Все в одном столбце "Цвет"

---

🔧 РЕШЕНИЕ ПРОБЛЕМ

ЧАСТЫЕ ОШИБКИ И РЕШЕНИЯ:

"Не удалось загрузить файл"
✅ Проверьте, что файл существует
✅ Убедитесь, что файл не открыт в Excel
✅ Проверьте формат файла (.xlsx, .xls)

"Столбец выходит за пределы таблицы"
✅ Проверьте номер столбца с характеристиками  
✅ Отсчет ведется с 1, а не с 0
✅ Убедитесь, что столбец существует

"Не найдены характеристики"
✅ Проверьте формат HTML в столбце
✅ Должны быть теги <tr> и <td>
✅ Убедитесь, что столбец не пустой

"Ошибка сохранения"
✅ Проверьте права на запись в папку
✅ Убедитесь, что папка существует
✅ Закройте Excel, если открыт результирующий файл

---

⌨️ ГОРЯЧИЕ КЛАВИШИ

ФАЙЛОВЫЕ ОПЕРАЦИИ:
Ctrl+O - Загрузить файл
Ctrl+P - Обработать таблицу
Ctrl+S - Сохранить результат  
Ctrl+Q - Выход из программы

РАБОТА С ДАННЫМИ:
Ctrl+C - Копировать выбранное
Ctrl+V - Вставить
Ctrl+Z - Отмена последнего действия
Ctrl+A - Выделить все
Delete - Удалить выбранное

НАВИГАЦИЯ:
F1 - Справка
Esc - Закрыть диалог
Enter - Подтвердить действие
Tab - Переход между элементами

---

🎯 ЛУЧШИЕ ПРАКТИКИ

ПОДГОТОВКА ДАННЫХ:
1. Очистите исходные данные от явных ошибок
2. Стандартизируйте HTML по возможности
3. Создайте резервную копию исходного файла
4. Проверьте кодировку файла (UTF-8 предпочтительно)

НАСТРОЙКА ОБРАБОТКИ:
1. Начните с малого - обработайте 10-20 товаров для тестирования  
2. Настройте маппинги для основных параметров
3. Добавьте правила замен для очистки данных
4. Создайте словарь синонимов для унификации значений

ПРОВЕРКА РЕЗУЛЬТАТОВ:
1. Проверьте количество товаров в результате
2. Сравните ключевые параметры с исходными данными
3. Убедитесь в корректности разделения значений
4. Проверьте отсутствие потери важных данных

---

🏆 ЗАКЛЮЧЕНИЕ

Редактор атрибутов товаров v4.2 - мощный инструмент для автоматизации обработки товарных характеристик.

ПРЕИМУЩЕСТВА:
⚡ Автоматизация рутинных операций
🎯 Высокая точность обработки данных
🔧 Гибкие настройки под любые задачи
💾 Сохранение времени на обработке каталогов
📈 Масштабируемость для любых объемов данных

РЕЗУЛЬТАТ ИСПОЛЬЗОВАНИЯ:
📊 Структурированные данные вместо HTML
🔄 Унифицированные параметры товаров
💾 Готовые файлы для загрузки в системы
⏰ Экономия времени на ручной обработке

Удачной работы с программой! 🚀

---

Версия инструкции: 1.0
Дата: 2025-06-02
Для программы: Редактор атрибутов товаров v4.2'''

        # Добавляем содержимое с форматированием
        lines = content.split('\n')
        
        for line in lines:
            line = line.rstrip()
            
            # Сохраняем позицию ДО добавления строки для навигации
            current_pos = help_text.index(INSERT)
            
            # Определяем тип строки и применяем соответствующий стиль
            if line.startswith('🎯 ') or line.startswith('🚀 ') or line.startswith('🔧 ') or line.startswith('⚙️ ') or line.startswith('💡 ') or line.startswith('⌨️ '):
                # Сохраняем позицию секции для навигации
                if "быстрый" in line.lower():
                    section_positions["start"] = current_pos
                elif "инструкция" in line.lower():
                    section_positions["instruction"] = current_pos
                elif "настройки" in line.lower():
                    section_positions["settings"] = current_pos
                elif "примеры" in line.lower():
                    section_positions["examples"] = current_pos
                elif "проблем" in line.lower():
                    section_positions["problems"] = current_pos
                elif "клавиши" in line.lower():
                    section_positions["hotkeys"] = current_pos
                
                help_text.insert(END, line + '\n', "title")
                    
            elif line.startswith('SHAГ ') or line.startswith('ПРИМЕР ') or line.startswith('ПРОБЛЕМА:') or line.startswith('РЕШЕНИЕ:'):
                help_text.insert(END, line + '\n', "subtitle")
            elif line.isupper() and len(line) > 3 and not line.startswith('TOV') and not line.startswith('PHONE'):
                help_text.insert(END, line + '\n', "header")
            elif line.startswith('✅') or line.startswith('•'):
                help_text.insert(END, line + '\n', "success")
            elif line.startswith('❌') or line.startswith('⚠️'):
                help_text.insert(END, line + '\n', "warning")
            elif line.startswith('Ctrl+') or line.startswith('F1') or line.startswith('Esc'):
                help_text.insert(END, line + '\n', "code")
            elif '→' in line or '<tr>' in line or '<td>' in line:
                help_text.insert(END, line + '\n', "code")
            elif line.strip() == '---':
                help_text.insert(END, '\n' + '─' * 80 + '\n\n', "emphasis")
            else:
                help_text.insert(END, line + '\n')
        
        help_text.config(state=DISABLED)

    def find_similar_parameters_for_tab(self):
        """🚨 ЭКСТРА-ИСПРАВЛЕННЫЙ поиск похожих параметров (v4.4)"""
        if not self.original_values:
            messagebox.showwarning("Ошибка", "Сначала обработайте таблицу")
            return

        print("\n🔍 ЗАПУСК ПОИСКА ПОХОЖИХ ПАРАМЕТРОВ (v4.8)")
        
        # 🚨 ПРИНУДИТЕЛЬНАЯ ОЧИСТКА ВСЕХ СТАРЫХ ДАННЫХ
        self.param_merge_settings.clear()
        print("🧹 Очищены старые настройки param_merge_settings")
        
        # 🚨 УДАЛЯЕМ ФАЙЛ НАСТРОЕК ЕСЛИ СУЩЕСТВУЕТ
        if os.path.exists(self.PARAM_MERGE_SETTINGS_FILE):
            try:
                os.remove(self.PARAM_MERGE_SETTINGS_FILE)
                print(f"🗑️ Удален файл старых настроек: {self.PARAM_MERGE_SETTINGS_FILE}")
            except Exception as e:
                print(f"⚠️ Не удалось удалить файл настроек: {e}")
        
        # 🚨 ПРИНУДИТЕЛЬНО ОЧИЩАЕМ ТАБЛИЦУ
        for item in self.params_tree.get_children():
            self.params_tree.delete(item)
        print("🧹 Очищена таблица параметров")
        
        # Собираем все параметры
        all_params = set()
        for sku_params in self.original_values.values():
            all_params.update(sku_params.keys())
        
        all_params = list(all_params)
        print(f"📋 Всего уникальных параметров: {len(all_params)}")
        
        # Обновляем частоту использования параметров
        self.update_param_frequency(all_params)
        
        print("📊 Топ-10 самых частых параметров:")
        sorted_params = sorted(self.param_frequency.items(), key=lambda x: x[1], reverse=True)[:10]
        for param, freq in sorted_params:
            print(f"  • {param}: {freq} раз")
        
        # 🔥 ТЕСТИРУЕМ ПРОБЛЕМНЫЕ ПАРЫ ДО ПОИСКА
        print("\n🧪 ТЕСТИРОВАНИЕ ПРОБЛЕМНЫХ ПАР:")
        test_pairs = [
            ("Caption", "Stream Authentication"),
            ("Weight", "Night"),
            ("IR Distance", "Night Vision"),
            ("Operating Humidity", "Mounting Bracket")
        ]
        
        for param1, param2 in test_pairs:
            if param1 in all_params and param2 in all_params:
                is_similar = self.are_parameters_truly_similar(param1, param2)
                print(f"   🔍 {param1} ↔ {param2}: {'❌ ОТКЛОНЕНО' if not is_similar else '✅ ПРИНЯТО'}")
        
        # 🔥 НАХОДИМ ПОХОЖИЕ ПАРАМЕТРЫ С НОВЫМ АЛГОРИТМОМ
        print("\n🔍 ЗАПУСК АЛГОРИТМА ПОИСКА...")
        similar_groups = self.find_similar_parameters_advanced(all_params)
        
        if not similar_groups:
            print("❌ Похожие параметры НЕ НАЙДЕНЫ")
            messagebox.showinfo("Информация", "✅ Похожие параметры не найдены с текущими критериями.")
            
            # Добавляем информационную строку в таблицу
            self.params_tree.insert('', END, values=(
                "", "Похожие параметры не найдены", "Попробуйте снизить критерии", ""
            ))
            return

        print(f"\n✅ НАЙДЕНО ГРУПП: {len(similar_groups)}")
        
        # 🔥 СОХРАНЯЕМ НОВЫЕ РЕЗУЛЬТАТЫ
        for representative, similar_list in similar_groups.items():
            # 🔥 ЗАЩИТА: Убираем представителя если случайно попал
            clean_similar_list = [p for p in similar_list if p != representative]
            
            if clean_similar_list:  # Если есть похожие параметры (без представителя)
                self.param_merge_settings[representative] = {
                    'similar_params': clean_similar_list,
                    'selected': False  # По умолчанию не выбрано
                }
                
                freq = self.param_frequency.get(representative, 0)
                print(f"✅ СОХРАНЕНО: {representative} (частота: {freq}) → {len(clean_similar_list)} похожих")

        # 🔥 СОХРАНЯЕМ ДАННЫЕ
        print(f"\n💾 СОХРАНЕНИЕ ДАННЫХ...")
        self.save_merge_settings()
        
        print(f"🎯 ОБНОВЛЕНИЕ ТАБЛИЦЫ...")
        self.update_params_tree()
        
        # Показываем результат пользователю
        message = f"✅ Найдено {len(similar_groups)} групп похожих параметров.\n"
        message += "📊 Основные параметры выбраны по количеству значений (параметр с наибольшим числом значений становится главным).\n"
        message += "✔️ Проверьте результаты на вкладке 'Объединение параметров'."
        messagebox.showinfo("Поиск завершен", message)

    def update_params_tree(self):
        """УЛУЧШЕННОЕ обновление таблицы похожих параметров с автоматической сортировкой"""
        print("🎯 ОБНОВЛЕНИЕ ТАБЛИЦЫ ПОХОЖИХ ПАРАМЕТРОВ")
        
        if not self.param_merge_settings:
            # Очищаем таблицу
            for item in self.params_tree.get_children():
                self.params_tree.delete(item)
            
            # Добавляем информационную строку
            self.params_tree.insert('', END, values=(
                "", "Похожие параметры не найдены", "Попробуйте снизить критерии", ""
            ))
            return
        
        # Сбрасываем пользовательскую сортировку и применяем сортировку по умолчанию (алфавит)
        self.params_tree_sort_direction = {}
        
        # Обновляем заголовки (убираем стрелки сортировки)
        self.update_params_tree_headers('', '')
        
        # Применяем сортировку по алфавиту (по умолчанию)
        print("📋 Применение сортировки по алфавиту (по умолчанию)...")
        # Устанавливаем направление для показа стрелки
        self.params_tree_sort_direction['representative'] = 'asc'
        self.sort_params_tree('representative')

    def on_params_tree_click(self, event):
        """Обработка клика по таблице параметров"""
        region = self.params_tree.identify("region", event.x, event.y)
        if region != "cell":
            return
        col = self.params_tree.identify_column(event.x)
        row = self.params_tree.identify_row(event.y)
        
        if not row or col != '#1':  # Колонка "Объединить"
            return
        
        values = list(self.params_tree.item(row, 'values'))
        representative = values[1]
        
        # Переключаем состояние
        if representative in self.param_merge_settings:
            current_state = self.param_merge_settings[representative].get('selected', False)
            self.param_merge_settings[representative]['selected'] = not current_state
            self.save_merge_settings()
            self.update_params_tree()

    def select_all_params(self):
        """Выбрать все параметры для объединения"""
        for representative in self.param_merge_settings:
            self.param_merge_settings[representative]['selected'] = True
        self.save_merge_settings()
        self.update_params_tree()

    def deselect_all_params(self):
        """Снять выбор со всех параметров"""
        for representative in self.param_merge_settings:
            self.param_merge_settings[representative]['selected'] = False
        self.save_merge_settings()
        self.update_params_tree()
    
    def sort_params_tree(self, column):
        """🆕 СОРТИРОВКА ПО СТОЛБЦАМ: Сортировка таблицы параметров по выбранному столбцу"""
        print(f"🔄 Сортировка по столбцу: {column}")
        
        # Определяем направление сортировки
        current_direction = self.params_tree_sort_direction.get(column, 'asc')
        new_direction = 'desc' if current_direction == 'asc' else 'asc'
        self.params_tree_sort_direction[column] = new_direction
        
        # Получаем все элементы для сортировки
        items_to_sort = []
        for representative, data in self.param_merge_settings.items():
            similar_list = data.get('similar_params', [])
            selected = data.get('selected', False)
            

            
            # 🔥 ИСПРАВЛЕНИЕ: similar_list уже НЕ содержит представителя
            if similar_list:  # Если есть похожие параметры
                # similar_list уже содержит только похожие параметры (без представителя)
                other_params_sorted = sorted(similar_list, key=lambda x: x.lower())
                similar_str = "; ".join(other_params_sorted)
                frequency = self.param_frequency.get(representative, 0)
                
                # Формируем кортеж для сортировки (значение_для_сортировки, все_данные)
                if column == 'representative':
                    sort_value = representative.lower()
                elif column == 'similar_params':
                    sort_value = similar_str.lower()
                elif column == 'frequency':
                    sort_value = frequency
                else:
                    sort_value = representative.lower()
                
                items_to_sort.append((sort_value, {
                    'representative': representative,
                    'similar_str': similar_str,
                    'selected': selected,
                    'frequency': frequency,
                    'other_params': similar_list  # 🔥 ИСПРАВЛЕНИЕ: используем similar_list
                }))
        

        
        # Сортируем
        reverse_order = (new_direction == 'desc')
        items_to_sort.sort(key=lambda x: x[0], reverse=reverse_order)
        
        # Очищаем и заполняем таблицу в новом порядке
        for item in self.params_tree.get_children():
            self.params_tree.delete(item)
        
        direction_symbol = "↓" if new_direction == 'desc' else "↑"
        print(f"📊 Сортировка {len(items_to_sort)} групп по {column} {direction_symbol}")
        
        # Обновляем заголовки для показа текущей сортировки
        self.update_params_tree_headers(column, direction_symbol)
        
        for sort_value, item_data in items_to_sort:
            merge_symbol = "☑" if item_data['selected'] else "☐"
            freq_info = f"{item_data['frequency']}"
            
            if len(item_data['other_params']) > 0:
                other_freqs = [self.param_frequency.get(p, 0) for p in item_data['other_params']]
                max_other_freq = max(other_freqs) if other_freqs else 0
                freq_info += f" (макс.др: {max_other_freq})"
            
            self.params_tree.insert('', END, values=(
                merge_symbol, item_data['representative'], item_data['similar_str'], freq_info
            ))
        
        print(f"✅ Сортировка завершена: {new_direction} по {column}")
    
    def update_params_tree_headers(self, sorted_column, direction_symbol):
        """Обновляет заголовки столбцов с индикацией текущей сортировки"""
        # Сбрасываем все заголовки
        self.params_tree.heading('merge', text='Объединить')
        self.params_tree.heading('representative', 
                                 text=f'Основной параметр {"" if sorted_column != "representative" else direction_symbol}', 
                                 command=lambda: self.sort_params_tree('representative'))
        self.params_tree.heading('similar_params', 
                                 text=f'Похожие параметры {"" if sorted_column != "similar_params" else direction_symbol}', 
                                 command=lambda: self.sort_params_tree('similar_params'))
        self.params_tree.heading('frequency', 
                                 text=f'Частота {"" if sorted_column != "frequency" else direction_symbol}', 
                                 command=lambda: self.sort_params_tree('frequency'))

    def apply_param_merges_from_tab(self):
        """Применение выбранных объединений параметров"""
        merge_groups = {}
        for representative, data in self.param_merge_settings.items():
            if data.get('selected', False):
                merge_groups[representative] = data['similar_params']
        
        if not merge_groups:
            messagebox.showinfo("Информация", "Не выбрано ни одного объединения")
            return

        self.apply_parameter_merges(merge_groups)
        messagebox.showinfo("Успешно", f"Применено {len(merge_groups)} объединений параметров")

    def clear_param_merge_settings(self):
        """Очистка настроек объединения параметров"""
        if messagebox.askyesno("Подтверждение", "Очистить все настройки объединения параметров?"):
            self.param_merge_settings.clear()
            self.save_merge_settings()
            self.update_params_tree()

    def find_similar_values_for_tab(self):
        """Поиск похожих значений для вкладки"""
        if not self.original_values:
            messagebox.showwarning("Ошибка", "Сначала обработайте таблицу")
            return

        similar_values_data = self.find_similar_values_in_all_parameters()
        
        if not similar_values_data:
            messagebox.showinfo("Информация", "Похожие значения не найдены")
            return

        # Сохраняем найденные группы в настройки
        for param_name, groups in similar_values_data.items():
            if param_name not in self.value_merge_settings:
                self.value_merge_settings[param_name] = {}
            
            for representative, similar_list in groups.items():
                if len(similar_list) > 1:
                    self.value_merge_settings[param_name][representative] = {
                        'similar_values': similar_list,
                        'selected': False
                    }

        self.save_merge_settings()
        self.update_values_tree()

    def update_values_tree(self):
        """Обновление таблицы похожих значений"""
        # Очищаем таблицу
        for item in self.values_tree.get_children():
            self.values_tree.delete(item)

        # Заполняем данными
        for param_name, groups in self.value_merge_settings.items():
            for representative, data in groups.items():
                similar_list = data.get('similar_values', [])
                selected = data.get('selected', False)
                
                if len(similar_list) > 1:
                    similar_str = "; ".join(similar_list)
                    merge_symbol = "☑" if selected else "☐"
                    frequency = len(similar_list)
                    
                    self.values_tree.insert('', END, values=(
                        merge_symbol, param_name, representative, similar_str, frequency
                    ))

    def on_values_tree_click(self, event):
        """Обработка клика по таблице значений"""
        region = self.values_tree.identify("region", event.x, event.y)
        if region != "cell":
            return
        col = self.values_tree.identify_column(event.x)
        row = self.values_tree.identify_row(event.y)
        
        if not row or col != '#1':  # Колонка "Объединить"
            return
        
        values = list(self.values_tree.item(row, 'values'))
        param_name = values[1]
        representative = values[2]
        
        # Переключаем состояние
        if param_name in self.value_merge_settings and representative in self.value_merge_settings[param_name]:
            current_state = self.value_merge_settings[param_name][representative].get('selected', False)
            self.value_merge_settings[param_name][representative]['selected'] = not current_state
            self.save_merge_settings()
            self.update_values_tree()

    def select_all_values(self):
        """Выбрать все значения для объединения"""
        for param_name in self.value_merge_settings:
            for representative in self.value_merge_settings[param_name]:
                self.value_merge_settings[param_name][representative]['selected'] = True
        self.save_merge_settings()
        self.update_values_tree()

    def deselect_all_values(self):
        """Снять выбор со всех значений"""
        for param_name in self.value_merge_settings:
            for representative in self.value_merge_settings[param_name]:
                self.value_merge_settings[param_name][representative]['selected'] = False
        self.save_merge_settings()
        self.update_values_tree()

    def apply_value_merges_from_tab(self):
        """Применение выбранных объединений значений"""
        merge_data = {}
        total_merges = 0
        
        for param_name, groups in self.value_merge_settings.items():
            param_merges = {}
            for representative, data in groups.items():
                if data.get('selected', False):
                    param_merges[representative] = data['similar_values']
                    total_merges += 1
            
            if param_merges:
                merge_data[param_name] = param_merges
        
        if not merge_data:
            messagebox.showinfo("Информация", "Не выбрано ни одного объединения")
            return

        self.apply_value_merges(merge_data)
        messagebox.showinfo("Успешно", f"Применено {total_merges} объединений значений")

    def clear_value_merge_settings(self):
        """Очистка настроек объединения значений"""
        if messagebox.askyesno("Подтверждение", "Очистить все настройки объединения значений?"):
            self.value_merge_settings.clear()
            self.save_merge_settings()
            self.update_values_tree()

    def load_settings(self):
        if os.path.exists(self.SETTINGS_FILE):
            try:
                with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f:
                    self.settings = json.load(f)
            except Exception as e:
                messagebox.showwarning("Ошибка загрузки настроек", f"Не удалось загрузить настройки: {str(e)}")
                self.settings = {}
        else:
            self.settings = {}
        
        # Убеждаемся, что все необходимые ключи присутствуют
        if "extra_cols" not in self.settings:
            self.settings["extra_cols"] = []
        if "features_col" not in self.settings:
            self.settings["features_col"] = ""
        if "source_file_path" not in self.settings:
            self.settings["source_file_path"] = ""
        if "output_folder_path" not in self.settings:
            self.settings["output_folder_path"] = ""

    def save_settings(self):
        self.settings["features_col"] = self.features_col.get()
        # extra_cols уже должны быть установлены в settings, не парсим из лейбла
        if "extra_cols" not in self.settings:
            # Fallback: если по какой-то причине extra_cols нет в settings, берем из лейбла
            label_text = self.extra_cols_label.cget("text")
            self.settings["extra_cols"] = [col.strip() for col in label_text.split(",") if col.strip()] if label_text else []
        self.settings["manual_split_values"] = self.manual_split_values
        self.settings["source_file_path"] = self.source_file_var.get()
        self.settings["output_folder_path"] = self.output_folder_var.get()

        try:
            with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
                json.dump(self.settings, f, ensure_ascii=False, indent=2)
        except Exception as e:
            messagebox.showerror("Ошибка сохранения настроек", f"Не удалось сохранить настройки: {str(e)}")

    def browse_source_file(self):
        """
        Открывает диалог выбора исходного файла
        """
        current_path = self.source_file_var.get()
        initial_dir = os.path.dirname(current_path) if current_path and os.path.exists(current_path) else os.getcwd()
        
        filename = filedialog.askopenfilename(
            title="Выберите Excel файл",
            initialdir=initial_dir,
            filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
        )
        
        if filename:
            self.source_file_var.set(filename)
            self.save_settings()

    def browse_output_folder(self):
        """
        Открывает диалог выбора папки для сохранения
        """
        current_path = self.output_folder_var.get()
        initial_dir = current_path if current_path and os.path.exists(current_path) else os.getcwd()
        
        folder = filedialog.askdirectory(
            title="Выберите папку для сохранения",
            initialdir=initial_dir
        )
        
        if folder:
            self.output_folder_var.set(folder)
            self.save_settings()

    def load_file_from_settings(self):
        """
        Быстрая загрузка файла из настроек
        """
        source_path = self.settings.get("source_file_path", "")
        
        if not source_path:
            messagebox.showwarning("Ошибка", "В настройках не указан путь к исходному файлу")
            return
        
        if not os.path.exists(source_path):
            messagebox.showerror("Ошибка", f"Файл не найден: {source_path}")
            return
        
        try:
            self.load_excel_file(source_path)
            messagebox.showinfo("Успешно", f"Файл загружен: {os.path.basename(source_path)}")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось загрузить файл: {str(e)}")

    def show_extra_cols_dialog(self):
        if not hasattr(self, 'df_columns') or not self.df_columns:
            messagebox.showwarning("Ошибка", "Сначала загрузите файл")
            return
        
        # Получаем текущие выбранные столбцы
        current_selection = self.settings.get("extra_cols", [])
        
        dialog = ColumnSelectorDialog(self.root, self.df_columns, current_selection)
        self.root.wait_window(dialog.dialog)
        
        if dialog.result is not None:
            # Сохраняем выбранные столбцы в настройки
            self.settings["extra_cols"] = dialog.result
            
            # Обновляем отображение в интерфейсе
            display_text = ", ".join(dialog.result) if dialog.result else ""
            self.extra_cols_label.config(text=display_text)
            
            # Визуальная индикация
            col_count = len(dialog.result)
            if col_count > 0:
                self.extra_cols_label.config(bg="lightgreen")
                ToolTip(self.extra_cols_label, f"Выбрано {col_count} столбцов")
            else:
                self.extra_cols_label.config(bg="SystemButtonFace")
            
            # Сохраняем настройки в файл
            self.save_settings()
            
            # Обновляем статус
            self.info_label.config(text=f"Выбрано дополнительных столбцов: {col_count}")

    def load_file(self):
        # Сначала проверяем, есть ли путь в поле исходного файла
        source_path = self.source_file_var.get().strip()
        
        if source_path and os.path.exists(source_path):
            # Загружаем файл из поля
            try:
                self.load_excel_file(source_path)
                messagebox.showinfo("Успешно", f"Файл загружен: {os.path.basename(source_path)}")
                return
            except Exception as e:
                messagebox.showerror("Ошибка", f"Не удалось загрузить файл из настроек: {str(e)}")
        
        # Если файла нет или ошибка загрузки - открываем диалог
        filename = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx *.xls")])
        if filename:
            try:
                self.load_excel_file(filename)
                # Обновляем поле исходного файла
                self.source_file_var.set(filename)
                self.save_settings()
                messagebox.showinfo("Успешно", f"Файл загружен: {os.path.basename(filename)}")
            except Exception as e:
                messagebox.showerror("Ошибка", f"Не удалось загрузить файл: {str(e)}")

    def load_excel_file(self, filename):
        df = pd.read_excel(filename)
        self.original_df = df.copy()
        self.df_columns = df.columns.tolist()
        
        # Обновляем подсказку для поля столбца характеристик
        if hasattr(self, 'features_col'):
            tooltip_text = f"Номер столбца с HTML-характеристиками (отсчёт с 1)\nДоступные столбцы: {', '.join([f'{i+1}. {col}' for i, col in enumerate(self.df_columns)])}"
            ToolTip(self.features_col, tooltip_text)

    def process_table(self):
        if self.original_df is None:
            messagebox.showwarning("Ошибка", "Сначала загрузите файл")
            return

        try:
            col_idx = int(self.features_col.get()) - 1
            html_col = self.original_df.columns[col_idx]
        except Exception:
            messagebox.showwarning("Ошибка", "Некорректный номер столбца характеристик")
            return

        self.param_mapping = {}
        self.original_values = {}

        if "manual_split_values" in self.settings and isinstance(self.settings["manual_split_values"], dict):
            self.manual_split_values = self.settings["manual_split_values"].copy()
        else:
            self.manual_split_values = {}

        self.manual_split_results = {}

        for idx, row in self.original_df.iterrows():
            sku = row.get("Артикул", None)
            if sku is None:
                continue
            html = str(row[html_col])
            params = self.parse_html_table(html)
            self.original_values[sku] = {}
            for param, value in params.items():
                clean_value = self.remove_duplicates_from_cell(value)
                self.original_values[sku][param] = clean_value

        all_params = set()
        for sku in self.original_df["Артикул"].unique():
            for param in self.original_values.get(sku, {}):
                all_params.add(param)

        self.update_mappings_from_params(all_params)
        # 🔧 ИСПРАВЛЕНО: Используем новую функцию вместо старой
        print("🔄 Используем НОВУЮ функцию для цветового выделения...")
        similar_groups_dict = self.find_similar_parameters_advanced(list(all_params))
        
        # Конвертируем формат для цветового выделения  
        self.similar_groups = {}
        group_id = 1
        for representative, group_params in similar_groups_dict.items():
            for param in group_params:
                self.similar_groups[param] = group_id
            group_id += 1
        self.tree.delete(*self.tree.get_children())

        rownum = 1
        self.case_value_variants = defaultdict(set)

        for param in sorted(all_params):
            group_id = self.similar_groups.get(param, 0)
            orig_values_set = set()
            for sku in self.original_df["Артикул"].unique():
                vals = self.original_values.get(sku, {})
                if param in vals:
                    orig_values_set.add(vals[param])

            norm_variants = set()
            for val in orig_values_set:
                norm_val = normalize_variant(val)
                norm_variants.add(norm_val)
                self.case_value_variants[norm_val.lower()].add(val)

            orig_values_joined = "; ".join(norm_variants)

            # 🔧 ИСПРАВЛЕНО: НЕ применяем сопоставления к исходным значениям
            orig_values_processed = self.apply_case_normalization_in_str(orig_values_joined, delimiter=';')
            orig_values_processed = self.remove_duplicates_from_list([p.strip() for p in orig_values_processed.split(";") if p.strip()])
            orig_values = "; ".join(orig_values_processed)
            orig_values = self.remove_duplicates_from_cell(orig_values)

            # 🔧 ИСПРАВЛЕНО: Правильная логика сопоставлений + переводов
            
            # 1. ИСХОДНЫЕ ДАННЫЕ (без перевода) для столбцов "Исходный параметр" и "Исходные значения"
            original_param = param  # Сохраняем исходное название на английском
            original_values = orig_values  # Сохраняем исходные значения на английском
            
            # 2. ПРИМЕНЯЕМ СОПОСТАВЛЕНИЯ И ПЕРЕВОДЫ для результирующих столбцов
            # Шаг 1: Применяем сопоставления из таблицы (если есть)
            mapped_param = self.mappings.get(param, param)
            if mapped_param != param:
                print(f"📋 Сопоставление: '{param}' → '{mapped_param}'")
            
            # Шаг 2: Применяем автоперевод к результату сопоставления
            new_param = self.apply_auto_translation(mapped_param, is_parameter=True)
            
            delimiter = ";"
            if param in self.manual_split_values and isinstance(self.manual_split_values[param], str) and self.manual_split_values[param] != False:
                delimiter = self.manual_split_values[param]
            
            # 🆕 КРИТИЧЕСКАЯ ЗАЩИТА ОТ ПУСТОГО РАЗДЕЛИТЕЛЯ v4.8.5
            if not delimiter:
                delimiter = ";"
                print(f"⚠️ АВАРИЙНАЯ ЗАМЕНА разделителя для '{param}': пустой → ';'")

            # 3. ОБРАБОТКА ЗНАЧЕНИЙ: БЕЗ сопоставлений из таблицы (они только для параметров!)
            split_result_list = self.split_values(orig_values, delimiter)
            print(f"🔧 DEBUG {param}: 1.После разделения: {split_result_list}")
            
            # 🔧 ИСПРАВЛЕНО: НЕ применяем сопоставления к значениям, только нормализация регистра
            split_result_list = [self.apply_case_normalization(v) for v in split_result_list]
            print(f"🔧 DEBUG {param}: 2.После нормализации: {split_result_list}")
            
            split_result_list = self.remove_duplicates_from_list(split_result_list)
            print(f"🔧 DEBUG {param}: 3.После первой дедупликации: {split_result_list}")

            # 🆕 ВОССТАНОВЛЕНИЕ ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ в интерфейсе
            split_result_list = [self.restore_protected_values_in_string(v) for v in split_result_list]
            print(f"🔧 DEBUG {param}: 4.После восстановления: {split_result_list}")
            
            # 🆕 ПРИМЕНЯЕМ ПРАВИЛА ЗАМЕНЫ
            split_result_list = [self.apply_replacement_rules(v) for v in split_result_list]
            print(f"🔧 DEBUG {param}: 5.После правил замены: {split_result_list}")
            
            # 🆕 ПРИМЕНЯЕМ СИНОНИМЫ ЗНАЧЕНИЙ 
            split_result_list = self.synonyms_manager.merge_synonyms_in_list(split_result_list)
            print(f"🔧 DEBUG {param}: 6.После синонимов: {split_result_list}")
            
            # 🔧 ИСПРАВЛЕНО: Удаляем дубликаты ПОСЛЕ применения синонимов
            split_result_list = self.remove_duplicates_from_list(split_result_list)
            print(f"🔧 DEBUG {param}: 7.После дедупликации синонимов: {split_result_list}")
            
            # 🆕 ПРИМЕНЯЕМ ПРАВИЛА ПЕРЕВОДА ЗНАЧЕНИЙ
            split_result_list = self.apply_value_translation_rules(split_result_list)
            print(f"🔧 DEBUG {param}: 8.После правил перевода значений: {split_result_list}")
            
            # 🔧 ИСПРАВЛЕНО: НЕ применяем автоперевод к значениям (только к параметрам!)
            # Автоперевод значений вызывал частичные переводы типа "Audio in" → "Аудио in"
            # Значения должны обрабатываться только правилами замены и синонимами
            
            # 🔧 ФИНАЛЬНАЯ ДЕДУПЛИКАЦИЯ (после всех преобразований)
            split_result_list = self.remove_duplicates_from_list(split_result_list)
            print(f"🔧 DEBUG {param}: 9.ФИНАЛЬНАЯ дедупликация: {split_result_list}")
            
            split_result_str = "; ".join(split_result_list)
            print(f"🔧 DEBUG {param}: 10.ИТОГОВЫЙ результат: '{split_result_str}'")

            # 4. ЗАПОЛНЯЕМ ТАБЛИЦУ: исходные данные + переведенные результаты
            self.tree.insert("", "end", values=(
                rownum, original_param, original_values, new_param, delimiter, split_result_str
            ), tags=(f"group_{group_id}",))
            rownum += 1

        self.df = pd.DataFrame([{
            "№": int(values[0]),
            "Исходный параметр": values[1],
            "Исходные значения": values[2],
            "Новый параметр": values[3],
            "Разделитель": values[4],
            "Результат разделения": values[5]
        } for item in self.tree.get_children() for values in [self.tree.item(item, "values")]])

        # 🆕 ЦВЕТОВОЕ ВЫДЕЛЕНИЕ ПОХОЖИХ ПАРАМЕТРОВ v4.0
        self.refresh_treeview_with_colors()
        
        # Автоматическая очистка дубликатов значений
        duplicates_cleaned = self.auto_clean_duplicate_values()
        
        # Автоматическое предложение объединения похожих параметров
        self.auto_suggest_parameter_merge()
        
        # 🆕 Автоматическая нормализация формата (перенесено из отдельной кнопки)
        print("🔄 Автоматическая нормализация формата...")
        self.normalize_all_values_format(show_message=False)
        
        # 🆕 АВТОМАТИЧЕСКИЙ АНАЛИЗ ЕДИНИЦ ИЗМЕРЕНИЯ
        print("📊 Автоматический анализ единиц измерения...")
        units_added = self.auto_analyze_units_and_create_rules()
        
        # Показываем информацию о найденных группах
        similar_count = len(self.similar_param_groups)
        message = f"Обработка завершена. Найдено {len(all_params)} уникальных параметров."
        if similar_count > 0:
            message += f"\n🎨 Найдено {similar_count} групп похожих параметров (выделены цветом)."
        message += f"\n✅ Форматирование значений автоматически нормализовано."
        if units_added > 0:
            message += f"\n📊 Автоматически добавлено {units_added} правил для единиц измерения."
        
        messagebox.showinfo("Успешно", message)

    def auto_analyze_units_and_create_rules(self):
        """Автоматически анализирует единицы измерения и создает правила замены"""
        try:
            if not hasattr(self, 'tree') or not self.tree.get_children():
                return 0
            
            # Собираем все значения из столбца "Результат разделения"
            all_values = []
            for item in self.tree.get_children():
                values = self.tree.item(item, 'values')
                if len(values) >= 6:  # Убеждаемся, что есть столбец "Результат разделения"
                    split_results = values[5]  # Столбец "Результат разделения"
                    if split_results:
                        # Разделяем значения по разделителям
                        split_values = [v.strip() for v in re.split(r'[;,|]', split_results) if v.strip()]
                        all_values.extend(split_values)
            
            if not all_values:
                print("📊 Нет данных для анализа единиц измерения")
                return 0
            
            # Создаем правила для единиц измерения
            unit_rules = self.create_unit_conversion_rules(all_values)
            
            if not unit_rules:
                print("📊 Единицы измерения для перевода не найдены")
                return 0
            
            # Автоматически добавляем новые правила
            added_count = 0
            for rule in unit_rules:
                # Проверяем, не существует ли уже такое правило
                existing = any(r['name'] == rule['name'] for r in self.replacement_rules)
                if not existing:
                    self.replacement_rules.append({
                        'name': rule['name'],
                        'pattern': rule['pattern'],
                        'replacement': rule['replacement'],
                        'active': True
                    })
                    added_count += 1
                    print(f"➕ Добавлено правило: {rule['name']}")
            
            if added_count > 0:
                # Сохраняем правила
                self.save_replacement_rules(self.replacement_rules)
                print(f"✅ Автоматически добавлено {added_count} правил для единиц измерения")
            
            return added_count
            
        except Exception as e:
            print(f"❌ Ошибка при автоматическом анализе единиц измерения: {e}")
            return 0
    
    def create_unit_conversion_rules(self, values):
        """Создает правила преобразования единиц измерения (расширенная версия для IP-видеонаблюдения)"""
        unit_patterns = {
            # === ФИЗИЧЕСКИЕ РАЗМЕРЫ ===
            'inch': ('дюйм', r'\\b(\\d+(?:\\.\\d+)?)"\\b'),
            'inches': ('дюймов', r'\\b(\\d+(?:\\.\\d+)?)\\s*inches?\\b'),
            'ft': ('фут', r'\\b(\\d+(?:\\.\\d+)?)\\s*ft\\b'),
            'feet': ('футов', r'\\b(\\d+(?:\\.\\d+)?)\\s*feet\\b'),
            'mm': ('мм', r'\\b(\\d+(?:\\.\\d+)?)\\s*mm\\b'),
            'cm': ('см', r'\\b(\\d+(?:\\.\\d+)?)\\s*cm\\b'),
            'meter': ('метр', r'\\b(\\d+(?:\\.\\d+)?)\\s*meters?\\b'),
            'm': ('м', r'\\b(\\d+(?:\\.\\d+)?)\\s*m\\b'),
            'μm': ('мкм', r'\\b(\\d+(?:\\.\\d+)?)\\s*μm\\b'),
            'nm': ('нм', r'\\b(\\d+(?:\\.\\d+)?)\\s*nm\\b'),
            
            # === ВЕС ===
            'kg': ('кг', r'\\b(\\d+(?:\\.\\d+)?)\\s*kg\\b'),
            'gram': ('грамм', r'\\b(\\d+(?:\\.\\d+)?)\\s*grams?\\b'),
            'g': ('г', r'\\b(\\d+(?:\\.\\d+)?)\\s*g\\b'),
            'lb': ('фунт', r'\\b(\\d+(?:\\.\\d+)?)\\s*lbs?\\b'),
            'oz': ('унция', r'\\b(\\d+(?:\\.\\d+)?)\\s*oz\\b'),
            
            # === ЭЛЕКТРИЧЕСКИЕ ПАРАМЕТРЫ ===
            'volt': ('вольт', r'\\b(\\d+(?:\\.\\d+)?)\\s*V\\b'),
            'v': ('В', r'\\b(\\d+(?:\\.\\d+)?)\\s*V\\b'),
            'mv': ('мВ', r'\\b(\\d+(?:\\.\\d+)?)\\s*mV\\b'),
            'kv': ('кВ', r'\\b(\\d+(?:\\.\\d+)?)\\s*kV\\b'),
            'amp': ('ампер', r'\\b(\\d+(?:\\.\\d+)?)\\s*A\\b'),
            'a': ('А', r'\\b(\\d+(?:\\.\\d+)?)\\s*A\\b'),
            'ma': ('мА', r'\\b(\\d+(?:\\.\\d+)?)\\s*mA\\b'),
            'μa': ('мкА', r'\\b(\\d+(?:\\.\\d+)?)\\s*μA\\b'),
            'watt': ('ватт', r'\\b(\\d+(?:\\.\\d+)?)\\s*W\\b'),
            'w': ('Вт', r'\\b(\\d+(?:\\.\\d+)?)\\s*W\\b'),
            'mw': ('мВт', r'\\b(\\d+(?:\\.\\d+)?)\\s*mW\\b'),
            'kw': ('кВт', r'\\b(\\d+(?:\\.\\d+)?)\\s*kW\\b'),
            'wh': ('Вт⋅ч', r'\\b(\\d+(?:\\.\\d+)?)\\s*Wh\\b'),
            'kwh': ('кВт⋅ч', r'\\b(\\d+(?:\\.\\d+)?)\\s*kWh\\b'),
            'mah': ('мАч', r'\\b(\\d+(?:\\.\\d+)?)\\s*mAh\\b'),
            'ohm': ('Ом', r'\\b(\\d+(?:\\.\\d+)?)\\s*Ohm\\b'),
            'kohm': ('кОм', r'\\b(\\d+(?:\\.\\d+)?)\\s*kOhm\\b'),
            
            # === ЧАСТОТА И ВРЕМЯ ===
            'hz': ('Гц', r'\\b(\\d+(?:\\.\\d+)?)\\s*Hz\\b'),
            'khz': ('кГц', r'\\b(\\d+(?:\\.\\d+)?)\\s*kHz\\b'),
            'mhz': ('МГц', r'\\b(\\d+(?:\\.\\d+)?)\\s*MHz\\b'),
            'ghz': ('ГГц', r'\\b(\\d+(?:\\.\\d+)?)\\s*GHz\\b'),
            'fps': ('кадр/с', r'\\b(\\d+(?:\\.\\d+)?)\\s*fps\\b'),
            'sec': ('сек', r'\\b(\\d+(?:\\.\\d+)?)\\s*sec\\b'),
            's': ('с', r'\\b(\\d+(?:\\.\\d+)?)\\s*s\\b'),
            'ms': ('мс', r'\\b(\\d+(?:\\.\\d+)?)\\s*ms\\b'),
            'μs': ('мкс', r'\\b(\\d+(?:\\.\\d+)?)\\s*μs\\b'),
            'ns': ('нс', r'\\b(\\d+(?:\\.\\d+)?)\\s*ns\\b'),
            'min': ('мин', r'\\b(\\d+(?:\\.\\d+)?)\\s*min\\b'),
            'hour': ('час', r'\\b(\\d+(?:\\.\\d+)?)\\s*hours?\\b'),
            'h': ('ч', r'\\b(\\d+(?:\\.\\d+)?)\\s*h\\b'),
            'day': ('день', r'\\b(\\d+(?:\\.\\d+)?)\\s*days?\\b'),
            
            # === ПАМЯТЬ И ДАННЫЕ ===
            'b': ('Б', r'\\b(\\d+(?:\\.\\d+)?)\\s*B\\b'),
            'byte': ('байт', r'\\b(\\d+(?:\\.\\d+)?)\\s*bytes?\\b'),
            'kb': ('КБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*KB\\b'),
            'mb': ('МБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*MB\\b'),
            'gb': ('ГБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*GB\\b'),
            'tb': ('ТБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*TB\\b'),
            'pb': ('ПБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*PB\\b'),
            'eb': ('ЭБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*EB\\b'),
            
            # === СКОРОСТЬ ПЕРЕДАЧИ ДАННЫХ ===
            'bps': ('бит/с', r'\\b(\\d+(?:\\.\\d+)?)\\s*bps\\b'),
            'kbps': ('кбит/с', r'\\b(\\d+(?:\\.\\d+)?)\\s*kbps\\b'),
            'mbps': ('Мбит/с', r'\\b(\\d+(?:\\.\\d+)?)\\s*Mbps\\b'),
            'gbps': ('Гбит/с', r'\\b(\\d+(?:\\.\\d+)?)\\s*Gbps\\b'),
            'tbps': ('Тбит/с', r'\\b(\\d+(?:\\.\\d+)?)\\s*Tbps\\b'),
            
            # === ИЗОБРАЖЕНИЯ И ВИДЕО ===
            'mp': ('мп', r'\\b(\\d+(?:\\.\\d+)?)\\s*mp\\b'),
            'megapixel': ('мегапикселей', r'\\b(\\d+(?:\\.\\d+)?)\\s*megapixels?\\b'),
            'px': ('пикс', r'\\b(\\d+(?:\\.\\d+)?)\\s*px\\b'),
            'pixel': ('пиксель', r'\\b(\\d+(?:\\.\\d+)?)\\s*pixels?\\b'),
            'dpi': ('точек/дюйм', r'\\b(\\d+(?:\\.\\d+)?)\\s*dpi\\b'),
            'ppi': ('пикс/дюйм', r'\\b(\\d+(?:\\.\\d+)?)\\s*ppi\\b'),
            'lpi': ('линий/дюйм', r'\\b(\\d+(?:\\.\\d+)?)\\s*lpi\\b'),
            
            # === ЗВУК И ОСВЕЩЕННОСТЬ ===
            'db': ('дБ', r'\\b(\\d+(?:\\.\\d+)?)\\s*dB\\b'),
            'dba': ('дБА', r'\\b(\\d+(?:\\.\\d+)?)\\s*dBA\\b'),
            'dbc': ('дБС', r'\\b(\\d+(?:\\.\\d+)?)\\s*dBC\\b'),
            'dbi': ('дБи', r'\\b(\\d+(?:\\.\\d+)?)\\s*dBi\\b'),
            'dbm': ('дБм', r'\\b(\\d+(?:\\.\\d+)?)\\s*dBm\\b'),
            'dbw': ('дБВт', r'\\b(\\d+(?:\\.\\d+)?)\\s*dBW\\b'),
            'dbfs': ('дБFS', r'\\b(\\d+(?:\\.\\d+)?)\\s*dBFS\\b'),
            'lux': ('лк', r'\\b(\\d+(?:\\.\\d+)?)\\s*lux\\b'),
            'lm': ('лм', r'\\b(\\d+(?:\\.\\d+)?)\\s*lm\\b'),
            'cd': ('кд', r'\\b(\\d+(?:\\.\\d+)?)\\s*cd\\b'),
            'cd/m²': ('кд/м²', r'\\b(\\d+(?:\\.\\d+)?)\\s*cd/m²\\b'),
            'nit': ('нит', r'\\b(\\d+(?:\\.\\d+)?)\\s*nit\\b'),
            
            # === ТЕМПЕРАТУРА (только Цельсий!) ===
            'celsius': ('°C', r'\\b(\\d+(?:\\.\\d+)?)\\s*°?C\\b'),
            'kelvin': ('К', r'\\b(\\d+(?:\\.\\d+)?)\\s*K\\b'),
            
            # === ДАВЛЕНИЕ И ВЛАЖНОСТЬ ===
            'pa': ('Па', r'\\b(\\d+(?:\\.\\d+)?)\\s*Pa\\b'),
            'kpa': ('кПа', r'\\b(\\d+(?:\\.\\d+)?)\\s*kPa\\b'),
            'mpa': ('МПа', r'\\b(\\d+(?:\\.\\d+)?)\\s*MPa\\b'),
            'bar': ('бар', r'\\b(\\d+(?:\\.\\d+)?)\\s*bar\\b'),
            'mbar': ('мбар', r'\\b(\\d+(?:\\.\\d+)?)\\s*mbar\\b'),
            'psi': ('фунт/кв.дюйм', r'\\b(\\d+(?:\\.\\d+)?)\\s*psi\\b'),
            'atm': ('атм', r'\\b(\\d+(?:\\.\\d+)?)\\s*atm\\b'),
            'mmhg': ('мм рт.ст.', r'\\b(\\d+(?:\\.\\d+)?)\\s*mmHg\\b'),
            'rh': ('%ОВ', r'\\b(\\d+(?:\\.\\d+)?)\\s*%?RH\\b'),
            
            # === УГЛЫ И ГЕОМЕТРИЯ ===
            'degree': ('°', r'\\b(\\d+(?:\\.\\d+)?)\\s*degrees?\\b'),
            'fov': ('поле зрения', r'\\b(\\d+(?:\\.\\d+)?)\\s*FOV\\b'),
            'aov': ('угол обзора', r'\\b(\\d+(?:\\.\\d+)?)\\s*AOV\\b'),
            'hfov': ('гориз. обзор', r'\\b(\\d+(?:\\.\\d+)?)\\s*HFOV\\b'),
            'vfov': ('верт. обзор', r'\\b(\\d+(?:\\.\\d+)?)\\s*VFOV\\b'),
            
            # === ПРОСТЫЕ ЗАМЕНЫ БЕЗ ЦИФР ===
            'yes': ('Да', r'\\bYes\\b'),
            'no': ('Нет', r'\\bNo\\b'),
            'support': ('Поддерживается', r'\\bSupport(?:ed)?\\b'),
            'available': ('Доступно', r'\\bAvailable\\b'),
            'enabled': ('Включено', r'\\bEnabled\\b'),
            'disabled': ('Отключено', r'\\bDisabled\\b'),
            'optional': ('Опционально', r'\\bOptional\\b'),
            'required': ('Требуется', r'\\bRequired\\b'),
            'manual': ('Ручной', r'\\bManual\\b'),
            'automatic': ('Автоматический', r'\\bAutomatic\\b'),
            'auto': ('Авто', r'\\bAuto\\b'),
            'high': ('Высокий', r'\\bHigh\\b'),
            'medium': ('Средний', r'\\bMedium\\b'),
            'low': ('Низкий', r'\\bLow\\b'),
            'none': ('Нет', r'\\bNone\\b'),
            'standard': ('Стандартный', r'\\bStandard\\b'),
            'advanced': ('Продвинутый', r'\\bAdvanced\\b'),
            'professional': ('Профессиональный', r'\\bProfessional\\b'),
            'indoor': ('Внутренний', r'\\bIndoor\\b'),
            'outdoor': ('Наружный', r'\\bOutdoor\\b'),
            'wireless': ('Беспроводной', r'\\bWireless\\b'),
            'wired': ('Проводной', r'\\bWired\\b'),
            'ethernet': ('Ethernet', r'\\bEthernet\\b'),
            'wifi': ('Wi-Fi', r'\\bWi-?Fi\\b'),
            'bluetooth': ('Bluetooth', r'\\bBluetooth\\b'),
            'poe': ('PoE', r'\\bPoE\\b'),
            'poe+': ('PoE+', r'\\bPoE\\+\\b'),
            'poe++': ('PoE++', r'\\bPoE\\+\\+\\b'),
            'onvif': ('ONVIF', r'\\bONVIF\\b'),
            'rtsp': ('RTSP', r'\\bRTSP\\b'),
            'rtmp': ('RTMP', r'\\bRTMP\\b'),
            'http': ('HTTP', r'\\bHTTP\\b'),
            'https': ('HTTPS', r'\\bHTTPS\\b'),
            'tcp': ('TCP', r'\\bTCP\\b'),
            'udp': ('UDP', r'\\bUDP\\b'),
            'ip': ('IP', r'\\bIP\\b'),
            'ir': ('ИК', r'\\bIR\\b'),
            'led': ('Светодиод', r'\\bLED\\b'),
            'ccd': ('ПЗС', r'\\bCCD\\b'),
            'cmos': ('КМОП', r'\\bCMOS\\b'),
            'ptz': ('ПТЗ', r'\\bPTZ\\b'),
            'dvr': ('Видеорегистратор', r'\\bDVR\\b'),
            'nvr': ('Сетевой регистратор', r'\\bNVR\\b'),
            'hdd': ('Жесткий диск', r'\\bHDD\\b'),
            'ssd': ('SSD', r'\\bSSD\\b'),
            'sd': ('SD карта', r'\\bSD\\b'),
            'micro sd': ('MicroSD', r'\\bMicro\\s*SD\\b'),
            'motion detection': ('Детекция движения', r'\\bMotion\\s*Detection\\b'),
            'night vision': ('Ночное видение', r'\\bNight\\s*Vision\\b'),
            'day/night': ('День/Ночь', r'\\bDay/?Night\\b'),
            'full hd': ('FullHD', r'\\bFull\\s*HD\\b'),
            '4k': ('4K', r'\\b4K\\b'),
            'ultra hd': ('UltraHD', r'\\bUltra\\s*HD\\b'),
            'wide dynamic range': ('Широкий динамический диапазон', r'\\bWide\\s*Dynamic\\s*Range\\b'),
            'wdr': ('ШДД', r'\\bWDR\\b'),
            'true wdr': ('Истинный ШДД', r'\\bTrue\\s*WDR\\b'),
        }
        
        # Ищем совпадения в значениях
        found_rules = []
        values_text = ' '.join(values).lower()
        
        for unit_key, (russian, pattern) in unit_patterns.items():
            # Проверяем, есть ли единица в данных
            simple_pattern = unit_key.replace('_', ' ')
            if any(simple_pattern in value.lower() for value in values):
                # Создаем правило с оригинальным паттерном
                rule_name = f"Автоперевод: {unit_key.title()}"
                if unit_key in ['yes', 'no', 'support', 'available', 'enabled', 'disabled', 
                              'optional', 'required', 'manual', 'automatic', 'high', 'medium', 
                              'low', 'none', 'standard', 'advanced', 'professional', 'indoor', 
                              'outdoor', 'wireless', 'wired']:
                    # Простая замена без групп
                    replacement = russian
                else:
                    # Замена с сохранением числа
                    replacement = f"\\1 {russian}"
                
                found_rules.append({
                    'name': rule_name,
                    'pattern': pattern,
                    'replacement': replacement,
                    'active': True
                })
        
        return found_rules

    def apply_replacement_rules(self, text):
        """Применяет правила замены к тексту"""
        if not hasattr(self, 'replacement_rules') or not self.replacement_rules:
            return text
        
        original_text = text
        result = text
        applied_rules = []
        
        for rule in self.replacement_rules:
            if rule.get('active', True):
                try:
                    pattern = rule.get('pattern', '')
                    replacement = rule.get('replacement', '')
                    old_result = result
                    result = re.sub(pattern, replacement, result)
                    if old_result != result:
                        applied_rules.append(rule.get('name', 'Unnamed'))
                except re.error:
                    # Игнорируем неверные регулярные выражения
                    continue
        
        if applied_rules and original_text != result:
            print(f"🔄 Правила замены: '{original_text}' → '{result}' (применены: {', '.join(applied_rules)})")
        
        return result

    def normalize_units_spacing(self, text):
        """🆕 Нормализация пробелов перед единицами измерения v4.9.2"""
        if not text or not isinstance(text, str):
            return text
            
        import re
        
        # Список единиц измерения для нормализации
        units = [
            # Длина
            'mm', 'cm', 'm', 'km', 'in', 'ft',
            # Вес  
            'mg', 'g', 'kg', 'lb', 'oz',
            # Электричество
            'V', 'A', 'W', 'Wh', 'kWh', 'mAh',
            # Частота
            'Hz', 'kHz', 'MHz', 'GHz',
            # Скорость передачи данных
            'bps', 'kbps', 'Mbps', 'Gbps', 'fps',
            # Температура
            '°C', '°F', 'K',
            # Освещенность
            'lux', 'lm', 'cd',
            # Память
            'B', 'KB', 'MB', 'GB', 'TB',
            # Давление
            'Pa', 'kPa', 'MPa', 'bar', 'psi',
            # Время
            'ms', 's', 'min', 'h',
        ]
        
        normalized = text
        changes_made = 0
        
        # Для каждой единицы измерения ищем паттерн "число+единица" и заменяем на "число + единица"
        for unit in units:
            # Паттерн: число (целое или дробное) + единица без пробела
            pattern = r'(\d+(?:\.\d+)?)\s*' + re.escape(unit) + r'\b'
            replacement = r'\1 ' + unit
            
            new_text = re.sub(pattern, replacement, normalized, flags=re.IGNORECASE)
            if new_text != normalized:
                changes_made += 1
                print(f"    🔧 НОРМАЛИЗАЦИЯ: '{normalized}' → '{new_text}'")
                normalized = new_text
        
        if changes_made > 0:
            print(f"🔧 Нормализовано единиц: {changes_made}")
            
        return normalized

    def apply_value_translation_rules(self, value_list):
        """🆕 УЛУЧШЕННАЯ функция перевода значений v4.9.2 - с нормализацией единиц"""
        try:
            if not value_list:
                return value_list
            
            print(f"🌐 Применение правил перевода значений к: {value_list}")
            
            translated_values = []
            translation_count = 0
            protected_count = 0
            normalization_count = 0
            
            for value in value_list:
                translated_value = value
                translation_applied = False
                
                # Проверка на защищенные аббревиатуры
                if self.is_protected_abbreviation(value):
                    print(f"  🛡️ ЗАЩИЩЕНО: '{value}' - известная аббревиатура")
                    translated_values.append(value)
                    protected_count += 1
                    continue
                
                # 🆕 СНАЧАЛА нормализуем пробелы перед единицами измерения
                normalized_value = self.normalize_units_spacing(translated_value)
                # 🆕 ДОПОЛНИТЕЛЬНО нормализуем формат значения
                normalized_value = normalize_value_format(normalized_value)
                if normalized_value != translated_value:
                    translated_value = normalized_value
                    normalization_count += 1
                    print(f"    🔧 НОРМАЛИЗОВАНО: '{value}' → '{normalized_value}'")
                
                # Применяем базовые правила из translation_dict
                if hasattr(self, 'translation_system') and self.translation_system:
                    try:
                        basic_translation = self.translation_system.translation_dict.get(translated_value)
                        if basic_translation and basic_translation != translated_value:
                            translated_value = basic_translation
                            translation_count += 1
                            translation_applied = True
                            print(f"    ✅ БАЗОВОЕ ПРАВИЛО: '{normalized_value}' → '{basic_translation}'")
                    except Exception as e:
                        print(f"    ⚠️ Ошибка применения базовых правил: {e}")
                
                # Если базовое правило не применилось, проверяем пользовательские правила
                if not translation_applied and hasattr(self, 'value_translation_rules') and self.value_translation_rules:
                    for rule in self.value_translation_rules:
                        if rule.get("active", True):
                            original_pattern = rule.get("original", "")
                            translation = rule.get("translation", "")
                            
                            if original_pattern and translation:
                                # Точное совпадение (регистро-независимое)
                                if translated_value.lower() == original_pattern.lower():
                                    translated_value = translation
                                    translation_count += 1
                                    translation_applied = True
                                    print(f"    ✅ ПОЛЬЗОВАТЕЛЬСКОЕ ПРАВИЛО (точное): '{original_pattern}' → '{translation}'")
                                    break
                                # Проверяем частичное совпадение (для единиц измерения)
                                elif original_pattern.lower() in translated_value.lower():
                                    import re
                                    pattern = r'\b' + re.escape(original_pattern) + r'\b'
                                    if re.search(pattern, translated_value, re.IGNORECASE):
                                        new_value = re.sub(pattern, translation, translated_value, flags=re.IGNORECASE)
                                        if new_value != translated_value:
                                            translated_value = new_value
                                            translation_count += 1
                                            translation_applied = True
                                            print(f"    ✅ ПОЛЬЗОВАТЕЛЬСКОЕ ПРАВИЛО (частичное): '{value}' → '{translated_value}'")
                                            break
                
                # Если ничего не применилось, применяем дополнительную проверку для единиц измерения
                if not translation_applied:
                    # Проверяем на kbps/Mbps паттерны с помощью регулярных выражений
                    import re
                    speed_patterns = {
                        r'\b(\d+(?:\.\d+)?)\s+kbps\b': r'\1 кбит/с',
                        r'\b(\d+(?:\.\d+)?)\s+Mbps\b': r'\1 Мбит/с',
                        r'\b(\d+(?:\.\d+)?)\s+Gbps\b': r'\1 Гбит/с',
                        r'\b(\d+(?:\.\d+)?)\s+bps\b': r'\1 бит/с',
                        r'\b(\d+(?:\.\d+)?)\s+fps\b': r'\1 к/с',
                    }
                    
                    for pattern, replacement in speed_patterns.items():
                        new_value = re.sub(pattern, replacement, translated_value, flags=re.IGNORECASE)
                        if new_value != translated_value:
                            translated_value = new_value
                            translation_count += 1
                            translation_applied = True
                            print(f"    ✅ ПАТТЕРН СКОРОСТИ: '{value}' → '{translated_value}'")
                            break
                
                translated_values.append(translated_value)
            
            if normalization_count > 0:
                print(f"🔧 Нормализовано значений: {normalization_count}")
            if translation_count > 0:
                print(f"🌐 Переведено значений: {translation_count}")
            if protected_count > 0:
                print(f"🛡️ Защищено аббревиатур: {protected_count}")
            
            return translated_values
        
        except Exception as e:
            print(f"❌ ОШИБКА в apply_value_translation_rules: {e}")
            return value_list

    def log_print(self, message, log_file=None):
        """Функция для вывода сообщений и в консоль, и в лог-файл"""
        print(message)
        if log_file:
            try:
                with open(log_file, 'a', encoding='utf-8') as f:
                    f.write(message + '\n')
            except Exception as e:
                print(f"⚠️ Ошибка записи в лог: {e}")

    def save_results(self):
        if self.original_df is None:
            messagebox.showwarning("Внимание", "Сначала загрузите файл!")
            return

        # 🆕 СОЗДАНИЕ ФАЙЛА ЛОГОВ
        from datetime import datetime
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        log_filename = f"debug_export_{timestamp}.log"
        
        # Создаем лог-файл и записываем заголовок
        try:
            with open(log_filename, 'w', encoding='utf-8') as f:
                f.write(f"ОТЛАДОЧНЫЙ ЛОГ ЭКСПОРТА\n")
                f.write(f"Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                f.write(f"=" * 80 + "\n\n")
        except Exception as e:
            print(f"⚠️ Не удалось создать лог-файл: {e}")
            log_filename = None

        # 🆕 ОТЛАДОЧНАЯ ИНФОРМАЦИЯ ДЛЯ ДИАГНОСТИКИ ПРОБЛЕМЫ
        self.log_print("\n" + "="*80, log_filename)
        self.log_print("🔍 ОТЛАДКА SAVE_RESULTS НАЧАТА", log_filename)
        self.log_print("="*80, log_filename)
        self.log_print(f"📊 Исходный файл: {self.original_df.shape[0]} строк, {self.original_df.shape[1]} столбцов", log_filename)
        self.log_print(f"📋 Столбцы файла: {list(self.original_df.columns)}", log_filename)

        # Получаем названия дополнительных столбцов из настроек
        extra_col_names = self.settings.get("extra_cols", [])
        
        # Проверяем, что выбранные столбцы существуют в DataFrame
        available_extra_cols = [col for col in extra_col_names if col in self.original_df.columns]
        
        if extra_col_names and not available_extra_cols:
            self.log_print(f"⚠️ Предупреждение: выбранные столбцы {extra_col_names} не найдены в файле", log_filename)
        
        cols_to_save = ["Артикул"] + available_extra_cols
        self.log_print(f"📋 Столбцы для сохранения: {cols_to_save}", log_filename)

        result = []

        html_col_idx = int(self.features_col.get()) - 1
        self.log_print(f"📋 Индекс столбца характеристик: {html_col_idx} (столбец: {self.original_df.columns[html_col_idx]})", log_filename)
        
        # Проблемные товары для отслеживания
        problematic_articles = ['IPC523-F255-NR', 'IPC2241-FN/FNW-SIR', 'IPC427', 'IPC422', 'IPC980']
        processed_problematic = []

        for idx, (_, row) in enumerate(self.original_df.iterrows()):
            sku = row.get("Артикул", None)
            if sku is None:
                continue
                
            is_problematic = sku in problematic_articles
            if is_problematic:
                self.log_print(f"\n🔍 ОБРАБОТКА ПРОБЛЕМНОГО ТОВАРА {idx}: {sku}", log_filename)
                
            item_data = {col: row[col] for col in cols_to_save if col in row}

            html = str(row[self.original_df.columns[html_col_idx]])
            
            if is_problematic:
                self.log_print(f"   📋 HTML длина: {len(html)}", log_filename)
                self.log_print(f"   📋 HTML начало: {html[:100]}...", log_filename)
                
            params = self.parse_html_table(html)
            
            if is_problematic:
                self.log_print(f"   ✅ Параметров найдено: {len(params)}", log_filename)
                if len(params) == 0:
                    self.log_print(f"   ❌ НЕ НАЙДЕНО ПАРАМЕТРОВ для {sku}!", log_filename)
                else:
                    self.log_print(f"   📋 Первые параметры: {list(params.keys())[:3]}...", log_filename)

            param_count = 0
            for param, value in params.items():
                try:
                    clean_value = self.remove_duplicates_from_cell(value)
                    mapped_param = self.mappings.get(param, param)

                    delimiter = ";"
                    node_id = None
                    for iid in self.tree.get_children():
                        vals = self.tree.item(iid, "values")
                        if vals[1] == param:
                            node_id = iid
                            break
                    if node_id:
                        try:
                            delimiter_val = self.tree.item(node_id, "values")[4].strip()
                            if delimiter_val:
                                delimiter = delimiter_val
                        except:
                            pass
                    
                    # 🆕 ФИНАЛЬНАЯ ЗАЩИТА ОТ ПУСТОГО РАЗДЕЛИТЕЛЯ v4.8.5
                    if not delimiter:
                        delimiter = ";"
                        self.log_print(f"      ⚠️ ПУСТОЙ разделитель заменен на ';'", log_filename)

                    split_vals = self.split_values(clean_value, delimiter)
                    
                    if is_problematic and param_count == 0:  # Показываем только для первого параметра
                        self.log_print(f"   🔄 Первый параметр '{param}':", log_filename)
                        self.log_print(f"      Исходное: {value[:50]}...", log_filename)
                        self.log_print(f"      Очищенное: {clean_value[:50]}...", log_filename)
                        self.log_print(f"      Разделитель: '{delimiter}'", log_filename)
                        self.log_print(f"      Разделенные: {split_vals}", log_filename)
                        
                    param_count += 1
                except Exception as e:
                    if is_problematic:
                        self.log_print(f"   ❌ Ошибка обработки параметра {param}: {e}", log_filename)
                    continue

                # 🆕 УЛУЧШЕННАЯ ОБРАБОТКА: Применяем маппинг к каждому значению отдельно
                # 🔧 ИСПРАВЛЕНО: НЕ применяем сопоставления к значениям
                split_vals_mapped = [val.strip() for val in split_vals if val.strip()]
                
                self.log_print(f"🔄 МНОЖЕСТВЕННЫЕ РАЗДЕЛИТЕЛИ '{delimiter}': {len(split_vals)} значений → {split_vals_mapped}", log_filename)

                split_vals_mapped = [self.apply_case_normalization(v) for v in split_vals_mapped]
                
                # 🆕 ВОССТАНОВЛЕНИЕ ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ в split_vals_mapped
                split_vals_mapped = [self.restore_protected_values_in_string(v) for v in split_vals_mapped]
                
                seen_normalized = set()
                final_split_vals = []
                for v in split_vals_mapped:
                    key_norm = normalize_variant(v).lower()
                    if key_norm not in seen_normalized:
                        seen_normalized.add(key_norm)
                        final_split_vals.append(v)

                if param in self.manual_split_results:
                    result_str = self.manual_split_results[param]
                    # 🆕 ПРЕОБРАЗОВАНИЕ для WooCommerce: ";" → ", " (запятая с пробелом)
                    if "; " in result_str:
                        result_str = result_str.replace("; ", ", ")
                        self.log_print(f"🔄 ПРЕОБРАЗОВАНО для WooCommerce: '{param}' → '; ' заменено на ', '", log_filename)
                else:
                    result_str = ", ".join(final_split_vals)

                    # 🆕 ВОССТАНОВЛЕНИЕ ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ при экспорте
                    result_str = self.restore_protected_values_in_string(result_str)

                    item_data[mapped_param] = result_str
                    
                    if is_problematic and param_count == 1:  # Показываем результат для первого параметра
                        self.log_print(f"      Финальный результат: '{result_str[:50]}...'", log_filename)

            if is_problematic:
                self.log_print(f"   📊 Обработано параметров: {param_count}", log_filename)
                self.log_print(f"   📊 Итого полей в товаре: {len(item_data)}", log_filename)
                if len(item_data) <= 1:  # Только Артикул
                    self.log_print(f"   ❌ ТОВАР БУДЕТ ПУСТЫМ!", log_filename)
                    processed_problematic.append(f"{sku} - ПУСТОЙ")
                else:
                    self.log_print(f"   ✅ Товар обработан успешно", log_filename)
                    processed_problematic.append(f"{sku} - ОК ({len(item_data)-1} полей)")

            result.append(item_data)

        df_result = pd.DataFrame(result)
        
        self.log_print(f"\n📊 ИТОГОВАЯ СТАТИСТИКА:", log_filename)
        self.log_print(f"   Обработано товаров: {len(result)}", log_filename)
        self.log_print(f"   Столбцов в результате: {len(df_result.columns) if not df_result.empty else 0}", log_filename)
        self.log_print(f"   Проблемные товары:", log_filename)
        for status in processed_problematic:
            self.log_print(f"      {status}", log_filename)
        self.log_print("="*80, log_filename)
        self.log_print("🔍 ОТЛАДКА SAVE_RESULTS ЗАВЕРШЕНА", log_filename)
        self.log_print("="*80, log_filename)
        
        # Сообщаем пользователю о сохранении лога
        if log_filename:
            self.log_print(f"\n💾 ОТЛАДОЧНЫЙ ЛОГ СОХРАНЕН В ФАЙЛ: {log_filename}", log_filename)
            messagebox.showinfo("Отладка", f"Отладочный лог сохранен в файл:\n{log_filename}\n\nИспользуйте этот файл для анализа проблем с экспортом.")
        
        # Определяем директорию для сохранения
        output_folder = self.output_folder_var.get().strip()
        if output_folder and os.path.exists(output_folder):
            initial_dir = output_folder
        else:
            initial_dir = os.getcwd()
        
        # Предлагаем имя файла на основе исходного
        source_file = self.source_file_var.get().strip()
        if source_file:
            base_name = os.path.splitext(os.path.basename(source_file))[0]
            suggested_name = f"{base_name}_обработанный.xlsx"
        else:
            suggested_name = "результат_обработки.xlsx"
        
        suggested_path = os.path.join(initial_dir, suggested_name)
        
        save_path = filedialog.asksaveasfilename(
            title="Сохранить результат обработки",
            initialdir=initial_dir,
            initialfile=suggested_name,
            defaultextension=".xlsx", 
            filetypes=[("Excel файлы", "*.xlsx"), ("Все файлы", "*.*")]
        )

        if save_path:
            try:
                df_result.to_excel(save_path, index=False)
                messagebox.showinfo("Успех", f"Результат сохранён в {save_path}")
                
                # Обновляем папку сохранения в настройках
                saved_folder = os.path.dirname(save_path)
                self.output_folder_var.set(saved_folder)
                self.save_settings()
                
            except Exception as e:
                messagebox.showerror("Ошибка сохранения", str(e))

    def on_close(self):
        """Обработчик закрытия приложения"""
        self.save_settings()
        self.save_merge_settings()
        self.root.destroy()

    def open_replacement_rules_editor(self):
        dialog = ReplacementRulesEditorDialog(self.root, self.replacement_rules, self.save_replacement_rules)

    def open_case_normalization_dialog(self):
        dialog = CaseNormalizationDialog(self.root, self.case_normalization, self.save_case_normalization)
    
    # 🆕 НОВЫЕ МЕТОДЫ v4.0 - Синонимы и цветовое выделение
    def open_value_synonyms_dialog(self):
        """Открывает диалог настройки синонимов значений"""
        dialog = ValueSynonymsDialog(self.root, self.synonyms_manager)
        if dialog.result:
            # После изменения синонимов обновляем отображение
            self.refresh_treeview_with_colors()
            messagebox.showinfo("Успешно", "Настройки синонимов сохранены")
    
    def open_protected_values_dialog(self):
        """🆕 Открывает диалог управления защищенными значениями"""
        dialog = Toplevel(self.root)
        dialog.title("🛡️ Защищенные значения от разделения")
        dialog.geometry("600x500")
        dialog.transient(self.root)
        dialog.grab_set()
        
        # Центрируем диалог
        dialog.update_idletasks()
        x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2)
        y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2)
        dialog.geometry(f"+{x}+{y}")
        
        # Заголовок с описанием
        header_frame = Frame(dialog)
        header_frame.pack(fill=X, padx=10, pady=10)
        
        Label(header_frame, text="🛡️ Защищенные значения", 
              font=("Arial", 14, "bold")).pack()
        
        info_text = ("Значения из этого списка НЕ БУДУТ разделяться, даже если содержат символ разделителя.\n"
                    "Пример: при разделителе '/' значение 'B/W' останется как 'B/W', а не станет 'B; W'")
        Label(header_frame, text=info_text, 
              font=("Arial", 9), fg="gray", wraplength=550, justify=LEFT).pack(pady=5)
        
        # Пример использования
        example_frame = LabelFrame(dialog, text="💡 Пример использования", padx=5, pady=5)
        example_frame.pack(fill=X, padx=10, pady=5)
        
        example_text = ('Исходное: "Auto (ICR) / Color / B/W"\n'
                       'Разделитель: "/" (без защиты) → "Auto (ICR); Color; B; W" ❌\n'
                       'Разделитель: "/" (с защитой "B/W") → "Auto (ICR); Color; B/W" ✅')
        Label(example_frame, text=example_text, font=("Courier", 9), 
              justify=LEFT, bg="#FFFACD").pack(fill=X)
        
        # Список защищенных значений
        list_frame = LabelFrame(dialog, text="📋 Текущие защищенные значения", padx=5, pady=5)
        list_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        # Создаем Treeview для списка
        tree_frame = Frame(list_frame)
        tree_frame.pack(fill=BOTH, expand=True)
        
        protected_tree = ttk.Treeview(tree_frame, columns=('value',), show='headings', height=10)
        protected_tree.heading('value', text='Защищенное значение')
        protected_tree.column('value', width=500)
        protected_tree.pack(side=LEFT, fill=BOTH, expand=True)
        
        scrollbar = Scrollbar(tree_frame, orient=VERTICAL, command=protected_tree.yview)
        scrollbar.pack(side=RIGHT, fill=Y)
        protected_tree.configure(yscrollcommand=scrollbar.set)
        
        # Заполняем список текущими значениями
        def refresh_protected_list():
            for item in protected_tree.get_children():
                protected_tree.delete(item)
            for value in self.protected_values:
                protected_tree.insert('', END, values=(value,))
        
        refresh_protected_list()
        
        # Кнопки управления
        buttons_frame = Frame(list_frame)
        buttons_frame.pack(fill=X, pady=5)
        
        def add_protected_value():
            """Добавить новое защищенное значение"""
            value = simpledialog.askstring(
                "Добавить защищенное значение", 
                "Введите значение, которое НЕ должно разделяться:\n(например: B/W, I/O, AC/DC, USB-C)"
            )
            if value and value.strip():
                clean_value = value.strip()
                if clean_value not in self.protected_values:
                    self.protected_values.append(clean_value)
                    self.save_protected_values()
                    refresh_protected_list()
                    messagebox.showinfo("Добавлено", f"Значение '{clean_value}' добавлено в защищенные")
                else:
                    messagebox.showwarning("Предупреждение", f"Значение '{clean_value}' уже в списке")
        
        def remove_protected_value():
            """Удалить выбранное защищенное значение"""
            selection = protected_tree.selection()
            if not selection:
                messagebox.showwarning("Предупреждение", "Выберите значение для удаления")
                return
            
            for item in selection:
                value = protected_tree.item(item, 'values')[0]
                if value in self.protected_values:
                    self.protected_values.remove(value)
            
            self.save_protected_values()
            refresh_protected_list()
            messagebox.showinfo("Удалено", "Выбранные значения удалены")
        
        def add_common_values():
            """Добавить часто используемые защищенные значения"""
            common_values = ["B/W", "I/O", "AC/DC", "USB-C", "Wi-Fi", "Bluetooth", "RJ-45", "RS-232", "RS-485"]
            added = []
            
            for value in common_values:
                if value not in self.protected_values:
                    self.protected_values.append(value)
                    added.append(value)
            
            if added:
                self.save_protected_values()
                refresh_protected_list()
                messagebox.showinfo("Добавлено", f"Добавлено {len(added)} распространенных значений:\n" + ", ".join(added))
            else:
                messagebox.showinfo("Информация", "Все распространенные значения уже в списке")
        
        Button(buttons_frame, text="➕ Добавить", command=add_protected_value).pack(side=LEFT, padx=2)
        Button(buttons_frame, text="🗑️ Удалить", command=remove_protected_value).pack(side=LEFT, padx=2)
        Button(buttons_frame, text="📥 Добавить распространенные", command=add_common_values).pack(side=LEFT, padx=2)
        
        # Кнопка закрытия
        close_frame = Frame(dialog)
        close_frame.pack(fill=X, padx=10, pady=10)
        
        Button(close_frame, text="✅ Готово", command=dialog.destroy, bg="#E6FFE6").pack()
    
    def detect_similar_parameters(self):
        """Определяет похожие параметры для цветового выделения"""
        if not hasattr(self, 'tree') or not self.tree.get_children():
            return
        
        # Собираем все параметры
        all_params = []
        for item in self.tree.get_children():
            values = self.tree.item(item, "values")
            param = values[1]  # Исходный параметр
            if param not in all_params:
                all_params.append(param)
        
        # Находим похожие группы
        self.similar_param_groups = {}
        processed = set()
        color_index = 0
        
        for param1 in all_params:
            if param1 in processed:
                continue
                
            similar_group = [param1]
            processed.add(param1)
            
            # Ищем похожие параметры
            for param2 in all_params:
                if param2 == param1 or param2 in processed:
                    continue
                    
                # Проверяем похожесть
                similarity = fuzz.ratio(param1.lower(), param2.lower())
                if similarity >= 70:  # Порог похожести
                    similar_group.append(param2)
                    processed.add(param2)
            
            # Если группа содержит более одного параметра, назначаем цвет
            if len(similar_group) > 1:
                representative = min(similar_group, key=len)  # Самый короткий как представитель
                self.similar_param_groups[representative] = similar_group
                
                # Назначаем цвет всем параметрам группы
                color = self.similar_param_colors[color_index % len(self.similar_param_colors)]
                for param in similar_group:
                    self.param_color_map[param] = color
                color_index += 1
        
        print(f"🎨 Найдено групп похожих параметров: {len(self.similar_param_groups)}")
        for rep, group in self.similar_param_groups.items():
            print(f"  🎨 {rep}: {group}")
    
    def refresh_treeview_with_colors(self):
        """Обновляет отображение дерева с цветовым выделением"""
        if not hasattr(self, 'tree'):
            return
            
        # Сначала определяем похожие параметры
        self.detect_similar_parameters()
        
        # Применяем цвета
        for item in self.tree.get_children():
            values = self.tree.item(item, "values")
            param = values[1]  # Исходный параметр
            
            # Получаем цвет для параметра
            if param in self.param_color_map:
                color = self.param_color_map[param]
                # Настраиваем теги для цветового выделения
                tag_name = f"color_{color.replace('#', '')}"
                self.tree.set(item, tags=(tag_name,))
                self.tree.tag_configure(tag_name, background=color)
            else:
                # Убираем цветовое выделение
                self.tree.set(item, tags=())

    def load_mappings(self):
        if os.path.exists(self.MAPPINGS_FILE):
            try:
                with open(self.MAPPINGS_FILE, "r", encoding="utf-8") as f:
                    self.mappings = json.load(f)
            except:
                self.mappings = {}
        else:
            self.mappings = {}

    def save_mappings(self):
        try:
            with open(self.MAPPINGS_FILE, "w", encoding="utf-8") as f:
                json.dump(self.mappings, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"Ошибка сохранения маппингов: {e}")

    def refresh_mapping_table(self):
        if not hasattr(self, 'mapping_tree'):
            return
            
        self.mapping_tree.delete(*self.mapping_tree.get_children())
        self._mapping_items_cache = []
        for orig, repl in self.mappings.items():
            self._mapping_items_cache.append((orig, repl))
        self._mapping_items_cache.sort(key=lambda x: x[0].lower())
        for orig, repl in self._mapping_items_cache:
            self.mapping_tree.insert("", "end", values=(orig, repl))

    def update_main_tree_with_mappings(self):
        """Обновляет основную таблицу с учетом сопоставлений И автоперевода"""
        updated_count = 0
        
        for item in self.tree.get_children():
            values = list(self.tree.item(item, "values"))
            param = values[1]  # Исходный параметр
            orig_values = values[2]  # Исходные значения
            delimiter = values[4]
            if not delimiter:
                delimiter = ";"

            # 🔧 ИСПРАВЛЕНО: Применяем сопоставления + автоперевод
            # Шаг 1: Применяем сопоставление из таблицы
            mapped_param = self.mappings.get(param, param)
            
            # Шаг 2: Применяем автоперевод к результату сопоставления
            new_param = self.apply_auto_translation(mapped_param, is_parameter=True)
            
            # Проверяем, изменился ли параметр
            if values[3] != new_param:
                updated_count += 1
                
            values[3] = new_param

            if param in self.manual_split_results:
                split_result_str = self.manual_split_results[param]
            else:
                if param in self.manual_split_values and isinstance(self.manual_split_values[param], str) and self.manual_split_values[param] != False:
                    split_vals = [v.strip() for v in self.manual_split_values[param].split(";") if v.strip()]
                else:
                    # 🆕 ДОПОЛНИТЕЛЬНАЯ ЗАЩИТА ОТ ПУСТОГО РАЗДЕЛИТЕЛЯ v4.8.5
                    if not delimiter:
                        delimiter = ";"
                        print(f"  ⚠️ ДУБЛИРОВАННАЯ ЗАЩИТА: пустой разделитель → ';' для '{param}'")
                    
                    split_vals = self.split_values(orig_values, delimiter)

                # 🔧 ИСПРАВЛЕНО: БЕЗ сопоставлений к значениям (только для параметров!)
                split_vals_processed = [val.strip() for val in split_vals if val.strip()]
                print(f"🔧 DEBUG UPDATE {param}: 1.После разделения: {split_vals_processed}")
                
                split_vals_processed = [self.apply_case_normalization(v) for v in split_vals_processed]
                print(f"🔧 DEBUG UPDATE {param}: 2.После нормализации: {split_vals_processed}")
                
                split_vals_processed = self.remove_duplicates_from_list(split_vals_processed)
                print(f"🔧 DEBUG UPDATE {param}: 3.После первой дедупликации: {split_vals_processed}")
                
                # 🆕 ВОССТАНОВЛЕНИЕ ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ в интерфейсе
                split_vals_processed = [self.restore_protected_values_in_string(v) for v in split_vals_processed]
                print(f"🔧 DEBUG UPDATE {param}: 4.После восстановления: {split_vals_processed}")
                
                # 🆕 ПРИМЕНЯЕМ ПРАВИЛА ЗАМЕНЫ
                split_vals_processed = [self.apply_replacement_rules(v) for v in split_vals_processed]
                print(f"🔧 DEBUG UPDATE {param}: 5.После правил замены: {split_vals_processed}")
                
                # 🆕 ПРИМЕНЯЕМ СИНОНИМЫ ЗНАЧЕНИЙ
                split_vals_processed = self.synonyms_manager.merge_synonyms_in_list(split_vals_processed)
                print(f"🔧 DEBUG UPDATE {param}: 6.После синонимов: {split_vals_processed}")
                
                # 🔧 ИСПРАВЛЕНО: Удаляем дубликаты ПОСЛЕ применения синонимов
                split_vals_processed = self.remove_duplicates_from_list(split_vals_processed)
                print(f"🔧 DEBUG UPDATE {param}: 7.После дедупликации синонимов: {split_vals_processed}")
                
                # 🔧 ИСПРАВЛЕНО: НЕ применяем автоперевод к значениям (только к параметрам!)
                # Автоперевод значений вызывал частичные переводы типа "Audio in" → "Аудио in"
                # Значения должны обрабатываться только правилами замены и синонимами
                
                # 🔧 ФИНАЛЬНАЯ ДЕДУПЛИКАЦИЯ (без автоперевода)
                split_vals_processed = self.remove_duplicates_from_list(split_vals_processed)
                print(f"🔧 DEBUG UPDATE {param}: 8.ФИНАЛЬНАЯ дедупликация: {split_vals_processed}")
                
                split_result_str = "; ".join(split_vals_processed)
                print(f"🔧 DEBUG UPDATE {param}: 9.ИТОГОВЫЙ результат: '{split_result_str}'")

            values[5] = split_result_str
            self.tree.item(item, values=values)
        
        # Выводим информацию об обновлениях только если были изменения
        if updated_count > 0:
            print(f"🔄 Обновлено переводов в основной таблице: {updated_count}")

    def load_excl(self):
        if os.path.exists(self.EXCLUSIONS_FILE):
            try:
                with open(self.EXCLUSIONS_FILE, "r", encoding="utf-8") as f:
                    self.exclusions = json.load(f)
            except:
                self.exclusions = []
        else:
            self.exclusions = []

    def load_protected_values(self):
        """🆕 Загрузка защищенных значений от разделения"""
        if os.path.exists(self.PROTECTED_VALUES_FILE):
            try:
                with open(self.PROTECTED_VALUES_FILE, "r", encoding="utf-8") as f:
                    self.protected_values = json.load(f)
                print(f"🛡️ ЗАГРУЖЕНО {len(self.protected_values)} защищенных значений: {self.protected_values}")
            except Exception as e:
                print(f"⚠️ Ошибка загрузки защищенных значений: {e}")
                self.protected_values = []
        else:
            self.protected_values = []

    def save_protected_values(self):
        """🆕 Сохранение защищенных значений"""
        try:
            with open(self.PROTECTED_VALUES_FILE, "w", encoding="utf-8") as f:
                json.dump(self.protected_values, f, ensure_ascii=False, indent=2)
            print(f"💾 СОХРАНЕНО {len(self.protected_values)} защищенных значений")
        except Exception as e:
            print(f"⚠️ Ошибка сохранения защищенных значений: {e}")

    def restore_protected_values_in_string(self, text):
        """🆕 Восстановление защищенных значений из плейсхолдеров в строке"""
        if not text or not self.protected_values:
            return text
            
        # Ищем и заменяем плейсхолдеры на оригинальные защищенные значения
        restored_text = text
        for i, protected_value in enumerate(self.protected_values):
            placeholder = f"__PROTECTED_{i}__"
            if placeholder in restored_text:
                restored_text = restored_text.replace(placeholder, protected_value)
                print(f"🔓 ВОССТАНОВЛЕНО в экспорте: '{placeholder}' → '{protected_value}'")
        
        return restored_text

    def load_case_normalization(self):
        if os.path.exists(self.CASE_NORM_FILE):
            try:
                with open(self.CASE_NORM_FILE, "r", encoding="utf-8") as f:
                    self.case_normalization = json.load(f)
            except:
                self.case_normalization = {}
        else:
            self.case_normalization = {}

    def load_replacement_rules(self):
        if os.path.exists(self.REPLACEMENT_RULES_FILE):
            try:
                with open(self.REPLACEMENT_RULES_FILE, "r", encoding="utf-8") as f:
                    self.replacement_rules = json.load(f)
            except:
                self.replacement_rules = []
        else:
            self.replacement_rules = []

    def save_replacement_rules(self, rules):
        self.replacement_rules = rules
        try:
            with open(self.REPLACEMENT_RULES_FILE, "w", encoding="utf-8") as f:
                json.dump(self.replacement_rules, f, ensure_ascii=False, indent=2)
            
            # 🆕 ДИНАМИЧЕСКОЕ ОБНОВЛЕНИЕ после сохранения правил замены v4.9
            print("🔄 Выполняем динамическое обновление после изменения правил замены...")
            self.dynamic_update_main_table()
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось сохранить правила замены: {str(e)}")

    def save_case_normalization(self, case_norm):
        self.case_normalization = case_norm
        try:
            with open(self.CASE_NORM_FILE, "w", encoding="utf-8") as f:
                json.dump(self.case_normalization, f, ensure_ascii=False, indent=2)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось сохранить настройки нормализации: {str(e)}")

    def update_exclusions_tree(self):
        """Обновление дерева исключений"""
        for item in self.exclusions_tree.get_children():
            self.exclusions_tree.delete(item)
        
        for exclusion in self.exclusions:
            self.exclusions_tree.insert('', END, values=(exclusion,))

    def add_exclusion(self):
        """Добавление исключения"""
        exclusion = simpledialog.askstring("Добавить исключение", "Введите исключение:")
        if exclusion and exclusion.strip():
            self.exclusions.append(exclusion.strip())
            self.update_exclusions_tree()

    def delete_exclusion(self):
        """Удаление исключения"""
        selection = self.exclusions_tree.selection()
        if selection:
            for item in selection:
                exclusion = self.exclusions_tree.item(item, 'values')[0]
                if exclusion in self.exclusions:
                    self.exclusions.remove(exclusion)
            self.update_exclusions_tree()

    def filter_mapping_table(self, *args):
        if not hasattr(self, '_mapping_items_cache'):
            return
            
        search_text = self.search_var.get().lower()
        self.mapping_tree.delete(*self.mapping_tree.get_children())
        filtered = []
        for orig, repl in self._mapping_items_cache:
            if search_text in orig.lower():
                filtered.append((orig, repl))
        for orig, repl in filtered:
            self.mapping_tree.insert("", "end", values=(orig, repl))

    def sort_mapping_tree_by_original_col(self):
        if not hasattr(self, '_mapping_items_cache'):
            return
            
        self.mapping_sort_reverse = not self.mapping_sort_reverse
        self._mapping_items_cache.sort(key=lambda x: x[0].lower(), reverse=self.mapping_sort_reverse)
        self.mapping_tree.delete(*self.mapping_tree.get_children())
        for orig, repl in self._mapping_items_cache:
            self.mapping_tree.insert("", "end", values=(orig, repl))

    def add_mapping(self):
        original = simpledialog.askstring("Добавить сопоставление", "Исходное значение:")
        if original:
            replacement = simpledialog.askstring("Добавить сопоставление", "Замена:")
            if replacement is not None:
                self.mappings[original] = replacement
                self.save_mappings()
                self.refresh_mapping_table()
                # 🔧 ИСПРАВЛЕНО: Автоматически обновляем основную таблицу
                self.update_main_tree_with_mappings()

    def delete_mapping(self):
        selected = self.mapping_tree.selection()
        if selected:
            for item in selected:
                values = self.mapping_tree.item(item, 'values')
                original = values[0]
                if original in self.mappings:
                    del self.mappings[original]
            self.save_mappings()
            self.refresh_mapping_table()
            # 🔧 ИСПРАВЛЕНО: Автоматически обновляем основную таблицу
            self.update_main_tree_with_mappings()

    def delete_all_mappings(self):
        if messagebox.askyesno("Подтверждение", "Удалить все сопоставления?"):
            self.mappings.clear()
            self.save_mappings()
            self.refresh_mapping_table()
            # 🔧 ИСПРАВЛЕНО: Автоматически обновляем основную таблицу
            self.update_main_tree_with_mappings()

    def import_mappings(self):
        filename = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")])
        if filename:
            try:
                with open(filename, "r", encoding="utf-8") as f:
                    imported_mappings = json.load(f)
                self.mappings.update(imported_mappings)
                self.save_mappings()
                self.refresh_mapping_table()
                # 🔧 ИСПРАВЛЕНО: Автоматически обновляем основную таблицу
                self.update_main_tree_with_mappings()
                messagebox.showinfo("Успешно", f"Импортировано {len(imported_mappings)} сопоставлений")
            except Exception as e:
                messagebox.showerror("Ошибка", f"Не удалось импортировать: {str(e)}")

    def export_mappings(self):
        filename = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON files", "*.json")]
        )
        if filename:
            try:
                with open(filename, "w", encoding="utf-8") as f:
                    json.dump(self.mappings, f, ensure_ascii=False, indent=2)
                messagebox.showinfo("Успешно", f"Экспортировано {len(self.mappings)} сопоставлений")
            except Exception as e:
                messagebox.showerror("Ошибка", f"Не удалось экспортировать: {str(e)}")

    def update_df_from_treeview(self):
        """Обновление DataFrame из текущего содержимого Treeview"""
        if not hasattr(self, 'tree') or not self.tree.get_children():
            return
        
        # Создаем новый DataFrame на основе текущего содержимого treeview
        self.df = pd.DataFrame([{
            "№": int(values[0]),
            "Исходный параметр": values[1],
            "Исходные значения": values[2],
            "Новый параметр": values[3],
            "Разделитель": values[4],
            "Результат разделения": values[5]
        } for item in self.tree.get_children() for values in [self.tree.item(item, "values")]])

    def normalize_all_values_format(self, show_message=True):
        """Нормализация формата всех значений
        
        Args:
            show_message (bool): Показывать ли сообщение о завершении (False при автоматическом вызове)
        """
        if not hasattr(self, 'df') or self.df is None or self.df.empty:
            if show_message:
                messagebox.showwarning("Ошибка", "Сначала обработайте таблицу")
            return
        
        count = 0
        for item in self.tree.get_children():
            values = list(self.tree.item(item, "values"))
            split_result = values[5]
            if split_result:
                normalized_values = []
                for val in split_result.split(";"):
                    val = val.strip()
                    if val:
                        normalized_val = normalize_value_format(val)
                        normalized_values.append(normalized_val)
                # Удаляем дубликаты, сохраняя порядок
                seen = set()
                unique_normalized = []
                for v in normalized_values:
                    v_lower = v.lower()
                    if v_lower not in seen:
                        seen.add(v_lower)
                        unique_normalized.append(v)
                new_split_result = "; ".join(unique_normalized)
                if new_split_result != split_result:
                    values[5] = new_split_result
                    self.tree.item(item, values=values)
                    count += 1
        
        # Обновляем df и отображение
        self.update_df_from_treeview()
        
        if show_message:
            messagebox.showinfo("Завершено", f"Нормализован формат в {count} строках")

    def force_refresh_delimiters(self):
        """🔧 МАССОВОЕ ОБНОВЛЕНИЕ всех разделителей (обычно не нужно - обновление происходит автоматически)"""
        if not hasattr(self, 'df') or self.df is None or self.df.empty:
            messagebox.showwarning("Ошибка", "Сначала обработайте таблицу")
            return
        
        # Предупреждаем пользователя
        result = messagebox.askyesno(
            "Массовое обновление", 
            "🔄 Разделители теперь обновляются автоматически при изменении!\n\n" +
            "Эта функция нужна только для массового обновления всех строк.\n\n" +
            "Продолжить массовое обновление?"
        )
        
        if not result:
            return
        
        print("\n🔄 МАССОВОЕ ОБНОВЛЕНИЕ ВСЕХ РАЗДЕЛИТЕЛЕЙ НАЧАТО")
        print("=" * 80)
        
        updated_count = 0
        
        for item in self.tree.get_children():
            values = list(self.tree.item(item, "values"))
            param = values[1]  # Исходный параметр
            orig_values = values[2]  # Исходные значения
            delimiter = values[4]  # Разделитель
            current_result = values[5]  # Текущий результат разделения
            
            # Применяем улучшенную логику разделения
            print(f"\n🔍 ОБНОВЛЕНИЕ: '{param}'")
            print(f"  📋 Разделитель: '{delimiter}'")
            print(f"  📋 Исходные значения: '{orig_values}'")
            print(f"  📋 Текущий результат: '{current_result}'")
            
            # 🆕 ЗАЩИТА ОТ ПУСТОГО РАЗДЕЛИТЕЛЯ v4.8.5
            if not delimiter:
                delimiter = ";"
                print(f"  ⚠️ ЗАМЕНА пустого разделителя → ';'")
            
            # 🆕 ИСПОЛЬЗУЕМ УЛУЧШЕННУЮ ФУНКЦИЮ split_values
            new_split_vals = self.split_values(orig_values, delimiter)
            
            # 🔧 ИСПРАВЛЕНО: НЕ применяем сопоставления к значениям
            split_vals_mapped = [val.strip() for val in new_split_vals if val.strip()]
            
            # Применяем нормализацию регистра
            split_vals_mapped = [self.apply_case_normalization(v) for v in split_vals_mapped]
            
            # Удаляем дубликаты
            split_vals_mapped = self.remove_duplicates_from_list(split_vals_mapped)
            
            # 🆕 ВОССТАНОВЛЕНИЕ ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ в интерфейсе
            split_vals_mapped = [self.restore_protected_values_in_string(v) for v in split_vals_mapped]
            
            # Формируем новый результат
            new_result = "; ".join(split_vals_mapped)
            
            print(f"  ✅ Новый результат: '{new_result}'")
            
            # Обновляем только если результат изменился
            if new_result != current_result:
                values[5] = new_result
                self.tree.item(item, values=values)
                updated_count += 1
                print(f"  🔄 ОБНОВЛЕНО!")
            else:
                print(f"  ✓ Без изменений")
        
        print(f"\n✅ ОБНОВЛЕНИЕ ЗАВЕРШЕНО: {updated_count} строк изменено")
        
        if updated_count > 0:
            messagebox.showinfo("Массовое обновление завершено", 
                               f"Обновлено {updated_count} строк.\n\n" +
                               "💡 В дальнейшем разделители будут обновляться автоматически при изменении!")
        else:
            messagebox.showinfo("Информация", 
                               "Все разделители уже корректны.\n\n" +
                               "💡 Теперь разделители обновляются автоматически при изменении!")

    # Остальные методы - заглушки для совместимости  
    def on_double_click(self, event):
        region = self.tree.identify("region", event.x, event.y)
        if region != "cell":
            return

        column = self.tree.identify_column(event.x)
        item = self.tree.identify_row(event.y)

        if not item or not column:
            return

        col_index = int(column.replace('#', '')) - 1
        values = list(self.tree.item(item, "values"))
        orig_param = values[1]
        current_value = values[col_index]

        if col_index not in [3, 4, 5]:
            return

        if self.edit_entry:
            self.edit_entry.destroy()
            self.edit_entry = None
            self.edit_undo_stack.clear()

        entry = Entry(self.tree)
        entry.insert(0, current_value)
        entry.select_range(0, END)
        entry.focus_set()

        x, y, width, height = self.tree.bbox(item, column)
        entry.place(x=x, y=y, width=width, height=height)

        self.edit_undo_stack = [current_value]
        self.edit_entry = entry

        def save_and_destroy(event=None):
            new_value = entry.get().strip()
            values[col_index] = new_value

            if col_index == 4:  # Разделитель
                self.manual_split_values[orig_param] = new_value
                self.settings["manual_split_values"] = self.manual_split_values.copy()
                self.save_settings()
                
                # 🆕 ПРОСТОЕ И НАДЕЖНОЕ ОБНОВЛЕНИЕ при изменении разделителя v4.8.1
                print(f"\n🔄 ОБНОВЛЕНИЕ разделителя для '{orig_param}'")
                print(f"  📋 Новый разделитель: '{new_value}'")
                
                # Получаем исходные значения для пересчета
                orig_values = values[2]  # Столбец "Исходные значения"
                print(f"  📋 Исходные значения: '{orig_values}'")
                
                # Простое разделение значений
                if not orig_values:
                    new_result = ""
                else:
                    # Разделяем по новому разделителю или стандартному
                    delimiter = new_value if new_value else ";"
                    split_parts = [part.strip() for part in orig_values.split(delimiter) if part.strip()]
                    print(f"  📋 Разделенные части: {split_parts}")
                    
                    # Применяем только базовую обработку
                    processed_parts = []
                    for part in split_parts:
                        # Очищаем пустые и неправильные значения
                        clean_part = part.strip()
                        if clean_part and clean_part not in ["/", ";", ",", "-"]:
                            processed_parts.append(clean_part)
                    
                    print(f"  📋 Очищенные части: {processed_parts}")
                    
                    # Удаляем дубликаты, сохраняя порядок
                    unique_parts = []
                    seen = set()
                    for part in processed_parts:
                        if part.lower() not in seen:
                            seen.add(part.lower())
                            unique_parts.append(part)
                    
                    print(f"  📋 Уникальные части: {unique_parts}")
                    
                    # Применяем переводы ТОЛЬКО если они есть
                    if hasattr(self, 'value_translation_rules') and self.value_translation_rules:
                        try:
                            translated_parts = self.apply_value_translation_rules(unique_parts)
                            unique_parts = translated_parts
                            print(f"  📋 После переводов: {unique_parts}")
                        except Exception as e:
                            print(f"  ⚠️ Ошибка применения переводов: {e}")
                    
                    # Формируем итоговый результат
                    new_result = "; ".join(unique_parts)
                
                print(f"  ✅ ИТОГОВЫЙ результат: '{new_result}'")
                
                # Обновляем столбец "Результат разделения" (индекс 5)
                values[5] = new_result
            elif col_index == 3:  # Новый параметр
                old_map_val = self.mappings.get(orig_param, orig_param)
                if old_map_val != new_value and new_value.strip():
                    self.mappings[orig_param] = new_value.strip()
                    self.save_mappings()
                    self.refresh_mapping_table()
                    self.update_main_tree_with_mappings()

            self.tree.item(item, values=values)

            entry.destroy()
            self.edit_entry = None
            self.edit_undo_stack.clear()

        entry.bind("<Return>", save_and_destroy)
        entry.bind("<FocusOut>", save_and_destroy)

    def show_context_menu(self, event):
        """Показать контекстное меню"""
        try:
            self.context_menu.post(event.x_root, event.y_root)
        except:
            pass

    def sort_column(self, event):
        """Сортировка колонки"""
        # Простая реализация - можно расширить при необходимости
        pass

    def copy_selected_value(self, event=None):
        """Копирование выбранного значения"""
        try:
            selection = self.tree.selection()
            if selection:
                item = selection[0]
                values = self.tree.item(item, "values")
                if values:
                    pyperclip.copy(str(values[1]))  # Копируем исходный параметр
        except:
            pass

    def select_all_rows(self, event):
        """Выделение всех строк"""
        try:
            for item in self.tree.get_children():
                self.tree.selection_add(item)
        except:
            pass

    def on_mapping_double_click(self, event):
        """Обработка двойного клика на маппинге - редактирование столбца 'Замена'"""
        print(f"🔍 DEBUG: Двойной клик на mapping_tree! x={event.x}, y={event.y}")
        
        try:
            # Получаем выделенный элемент
            selection = self.mapping_tree.selection()
            print(f"🔍 DEBUG: selection = {selection}")
            
            if not selection:
                print("🔍 DEBUG: Нет выделенного элемента")
                return
                
            item = selection[0]
            values = self.mapping_tree.item(item, "values")
            print(f"🔍 DEBUG: values = {values}")
            
            if len(values) < 2:
                print(f"🔍 DEBUG: Недостаточно значений: {len(values)}")
                return
                
            original_value = values[0]  # Исходное значение
            current_replacement = values[1]  # Текущая замена
            
            print(f"🔍 DEBUG: Открываем диалог редактирования для '{original_value}' → '{current_replacement}'")
            
            # 🔧 УПРОЩЕНО: Открываем диалог редактирования при любом двойном клике
            new_replacement = simpledialog.askstring(
                "Редактировать замену", 
                f"Исходное значение: {original_value}\n\nВведите новую замену:",
                initialvalue=current_replacement
            )
            
            if new_replacement is not None and new_replacement != current_replacement:
                # Обновляем маппинг
                self.mappings[original_value] = new_replacement
                self.save_mappings()
                self.refresh_mapping_table()
                
                # 🔧 ИСПРАВЛЕНО: Динамически обновляем основную таблицу
                self.update_main_tree_with_mappings()
                
                print(f"📝 Изменено сопоставление: '{original_value}' → '{new_replacement}'")
                messagebox.showinfo("Успешно", f"Сопоставление обновлено:\n'{original_value}' → '{new_replacement}'")
            elif new_replacement is None:
                print("🔍 DEBUG: Пользователь отменил редактирование")
            else:
                print("🔍 DEBUG: Значение не изменилось")
                
        except Exception as e:
            print(f"🔍 DEBUG: Ошибка при редактировании маппинга: {e}")
            import traceback
            traceback.print_exc()
            messagebox.showerror("Ошибка", f"Не удалось отредактировать сопоставление:\n{e}")

    def select_all_mappings(self, event=None):
        """Выделение всех маппингов"""
        try:
            for item in self.mapping_tree.get_children():
                self.mapping_tree.selection_add(item)
        except:
            pass

    def delete_selected_mappings(self, event):
        """Удаление выбранных маппингов"""
        try:
            selection = self.mapping_tree.selection()
            for item in selection:
                values = self.mapping_tree.item(item, "values")
                if len(values) >= 2:
                    orig_param = values[0]
                    if orig_param in self.mappings:
                        del self.mappings[orig_param]
                self.mapping_tree.delete(item)
        except:
            pass

    def copy_mapping_selected_value(self, event):
        """Копирование выбранного маппинга"""
        try:
            selection = self.mapping_tree.selection()
            if selection:
                item = selection[0]
                values = self.mapping_tree.item(item, "values")
                if values:
                    pyperclip.copy(str(values[0]))
        except:
            pass

    def on_exclusion_double_click(self, event):
        """Обработка двойного клика на исключении"""
        # Можно добавить редактирование исключения
        pass

    def copy_exclusion_selected_value(self, event):
        """Копирование выбранного исключения"""
        try:
            selection = self.exclusions_tree.selection()
            if selection:
                item = selection[0]
                values = self.exclusions_tree.item(item, "values")
                if values:
                    pyperclip.copy(str(values[0]))
        except:
            pass

    def global_ctrl_c(self, event):
        """Глобальное копирование (Ctrl+C)"""
        try:
            widget = self.root.focus_get()
            if hasattr(widget, 'selection_get'):
                pyperclip.copy(widget.selection_get())
        except:
            pass

    def global_ctrl_v(self, event):
        """Глобальная вставка (Ctrl+V)"""
        try:
            widget = self.root.focus_get()
            if hasattr(widget, 'insert'):
                widget.insert('insert', pyperclip.paste())
        except:
            pass

    def global_ctrl_z(self, event):
        """Глобальная отмена (Ctrl+Z)"""
        # Можно добавить функциональность отмены
        pass

    def global_ctrl_a(self, event):
        """Глобальное выделение всего (Ctrl+A)"""
        try:
            widget = self.root.focus_get()
            if hasattr(widget, 'select_range'):
                widget.select_range(0, 'end')
            elif widget == self.tree:
                self.select_all_rows(event)
        except:
            pass

    def global_delete(self, event):
        """Глобальное удаление (Delete)"""
        try:
            widget = self.root.focus_get()
            if widget == self.tree:
                # Можно добавить удаление выбранных строк
                pass
            elif widget == self.mapping_tree:
                self.delete_selected_mappings(event)
        except:
            pass

    def merge_selected_similar_params(self):
        """Объединение выбранных похожих параметров"""
        selection = self.tree.selection()
        if len(selection) < 2:
            messagebox.showwarning("Внимание", "Выберите как минимум 2 параметра для объединения")
            return
        
        # Получаем имена выбранных параметров
        selected_params = []
        for item in selection:
            values = self.tree.item(item, "values")
            param = values[1]  # Исходный параметр
            selected_params.append(param)
        
        # Показываем диалог выбора основного параметра
        dialog = ParameterMergeDialog(self.root, selected_params)
        if dialog.result:
            representative = dialog.result
            merge_groups = {representative: selected_params}
            self.apply_parameter_merges(merge_groups)
            
            # 🔧 ИСПРАВЛЕНО: Обновляем переводы после объединения
            print(f"🔄 Обновляем переводы после объединения под представителем: '{representative}'")
            
            # Добавляем сопоставление для представителя если его нет
            translation_copied = False
            if representative not in self.mappings:
                # Ищем перевод представителя среди объединяемых параметров
                for param in selected_params:
                    if param in self.mappings:
                        # Копируем перевод на представителя
                        self.mappings[representative] = self.mappings[param]
                        print(f"📋 Скопирован перевод: '{representative}' ← '{param}' → '{self.mappings[param]}'")
                        translation_copied = True
                        break
            
            # Сохраняем обновленные маппинги
            if translation_copied:
                self.save_mappings()
                self.refresh_mapping_table()
            
            # Обновляем переводы в основной таблице
            self.update_main_tree_with_mappings()
            
            messagebox.showinfo("Успешно", 
                f"Параметры объединены под именем '{representative}'\n"
                f"Переводы обновлены автоматически")
    
    def merge_selected_params_and_values(self):
        """ЭКСТРА-ОТЛАДОЧНАЯ версия объединения выбранных параметров и их значений"""
        print("\n" + "🔥"*100)
        print("🔥 ЭКСТРА-ОТЛАДКА: merge_selected_params_and_values НАЧАЛО")
        print("🔥"*100)
        
        selection = self.tree.selection()
        print(f"📋 Количество выбранных элементов: {len(selection)}")
        
        if len(selection) < 2:
            messagebox.showwarning("Внимание", "Выберите как минимум 2 параметра для объединения")
            return
        
        # Получаем информацию о выбранных параметрах
        selected_items_info = []
        selected_params = []
        
        print("\n🔥 АНАЛИЗ ВЫБРАННЫХ ПАРАМЕТРОВ:")
        for i, item in enumerate(selection):
            values = self.tree.item(item, "values")
            param = values[1]  # Исходный параметр
            selected_params.append(param)
            selected_items_info.append({
                'item': item,
                'param': param,
                'original_values': values[2],
                'result_values': values[5]
            })
            print(f"  📋 Элемент {i+1}: '{param}' → исходные: '{values[2]}' → результат: '{values[5]}'")
        
        print(f"\n🔥 СПИСОК ВЫБРАННЫХ ПАРАМЕТРОВ: {selected_params}")
        
        # Показываем диалог выбора основного параметра
        dialog = ParameterMergeDialog(self.root, selected_params)
        if dialog.result:
            representative = dialog.result
            print(f"\n🔥 ВЫБРАННЫЙ ОСНОВНОЙ ПАРАМЕТР: '{representative}'")
            
            # Создаем группу для объединения
            merge_groups = {representative: selected_params}
            print(f"🔥 СФОРМИРОВАННЫЕ ГРУППЫ: {merge_groups}")
            
            # Применяем объединение параметров
            self.apply_parameter_merges(merge_groups)
            
            # 🔧 ИСПРАВЛЕНО: После объединения НУЖНО обновить переводы!
            # Переводы должны применяться к представителю (representative)
            print(f"🔄 Обновляем переводы после объединения под представителем: '{representative}'")
            
            # Добавляем сопоставление для представителя если его нет
            translation_copied_extended = False
            if representative not in self.mappings:
                # Ищем перевод представителя среди объединяемых параметров
                for param in selected_params:
                    if param in self.mappings:
                        # Копируем перевод на представителя
                        self.mappings[representative] = self.mappings[param]
                        print(f"📋 Скопирован перевод: '{representative}' ← '{param}' → '{self.mappings[param]}'")
                        translation_copied_extended = True
                        break
            
            # Сохраняем обновленные маппинги
            if translation_copied_extended:
                self.save_mappings()
                self.refresh_mapping_table()
            
            # Обновляем переводы в основной таблице
            self.update_main_tree_with_mappings()
            
            messagebox.showinfo("Успешно", 
                f"Параметры и их значения объединены под именем '{representative}'\n"
                f"Объединено {len(selected_params)} параметров\n"
                f"Переводы обновлены автоматически")
            
            # Обновляем настройки объединения параметров для сохранения
            if representative not in self.param_merge_settings:
                self.param_merge_settings[representative] = {
                    'similar_params': selected_params,
                    'selected': True
                }
            
            self.save_merge_settings()
            
            print("\n🔥 ЭКСТРА-ОТЛАДКА: merge_selected_params_and_values ЗАВЕРШЕНО")
            print("🔥"*100)

    def apply_parameter_merges(self, merge_groups):
        """
        Исправленная функция объединения параметров:
        - Корректно собирает все значения из всех объединяемых параметров
        - В столбец 'Исходные значения' пишет все уникальные исходные значения (до перевода), нормализованные
        - В столбец 'Результат разделения' пишет все уникальные переведённые значения, нормализованные
        - Дубли удаляются после всех преобразований
        """
        print("\n" + "🔥"*80)
        print("🔥 ИСПРАВЛЕННАЯ ВЕРСИЯ ОБЪЕДИНЕНИЯ - ПОЛНОЕ РЕШЕНИЕ ПРОБЛЕМ")
        print("🔥"*80)
        
        if not merge_groups:
            print("❌ Нет групп для объединения")
            return
        
        print(f"📋 Группы объединения: {merge_groups}")
        
        for representative, param_list in merge_groups.items():
            print(f"\n🔥 ГРУППА: '{representative}' ← {param_list}")
            rows_to_merge = []
            target_row = None
            for item in self.tree.get_children():
                values = self.tree.item(item, "values")
                param_name = values[1]
                if param_name in param_list:
                    rows_to_merge.append({
                        'item': item,
                        'param': param_name,
                        'original': values[2],
                        'result': values[5],
                        'delimiter': values[4] or ";"
                    })
                    if param_name == representative:
                        target_row = item
            if len(rows_to_merge) < 2:
                print(f"  ⚠️ Недостаточно строк для объединения: {len(rows_to_merge)}")
                continue
            # Сбор всех исходных значений (до перевода)
            all_original_values = []
            for row in rows_to_merge:
                source_text = row['result'] if row['result'] else row['original']
                if source_text:
                    parts = self._smart_split_values(source_text, row['delimiter'])
                    for part in parts:
                        clean_part = normalize_value_format(part.strip())
                        if clean_part:
                            all_original_values.append(clean_part)
            # Удаляем дубли (без учёта регистра)
            seen = set()
            unique_original_values = []
            for v in all_original_values:
                v_lower = v.lower()
                if v_lower not in seen:
                    seen.add(v_lower)
                    unique_original_values.append(v)
            # Сбор переведённых значений
            translated_values = []
            for v in unique_original_values:
                # Применяем перевод к каждому значению
                translated = self.apply_value_translation_rules([v])[0] if hasattr(self, 'apply_value_translation_rules') else v
                translated = normalize_value_format(translated.strip())
                if translated:
                    translated_values.append(translated)
            # Удаляем дубли после перевода
            seen_tr = set()
            unique_translated_values = []
            for v in translated_values:
                v_lower = v.lower()
                if v_lower not in seen_tr:
                    seen_tr.add(v_lower)
                    unique_translated_values.append(v)
            # Формируем итоговые строки
            final_delimiter = rows_to_merge[0]['delimiter'] if rows_to_merge else ";"
            original_result = final_delimiter.join(unique_original_values)
            translated_result = final_delimiter.join(unique_translated_values)
            # Выбор целевой строки
            if not target_row:
                target_row = rows_to_merge[0]['item']
            # Обновляем целевую строку
            target_values = list(self.tree.item(target_row, "values"))
            target_values[1] = representative  # Исходный параметр
            target_values[2] = original_result  # Исходные значения (уникальные, нормализованные)
            target_values[3] = self.mappings.get(representative, representative)  # Новый параметр (переведённый)
            target_values[5] = translated_result  # Результат разделения (уникальные, переведённые)
            self.tree.item(target_row, values=target_values)
            # Удаляем остальные строки
            for row in rows_to_merge:
                if row['item'] != target_row:
                    self.tree.delete(row['item'])
        print("\n" + "🔥"*80)
        print("🔥 ОБЪЕДИНЕНИЕ ЗАВЕРШЕНО УСПЕШНО (исправлено)")
        print("🔥"*80)
        self.dynamic_update_main_table()
        self.root.after(1000, self.delayed_check_after_merge)

    def _smart_split_values(self, text, delimiter):
        """🆕 Умное разделение значений с сохранением переводов в скобках v4.8.5"""
        if not text:
            return []
        
        # 🆕 ЗАЩИТА ОТ ПУСТОГО РАЗДЕЛИТЕЛЯ v4.8.5
        if not delimiter:
            delimiter = ";"
            print(f"⚠️ УМНОЕ РАЗДЕЛЕНИЕ: пустой разделитель → ';'")
        
        # Сначала пробуем разделить по указанному разделителю
        if delimiter and delimiter in text:
            parts = [p.strip() for p in text.split(delimiter) if p.strip()]
        elif "; " in text:
            parts = [p.strip() for p in text.split("; ") if p.strip()]
        elif ";" in text:
            parts = [p.strip() for p in text.split(";") if p.strip()]
        elif " / " in text:
            parts = [p.strip() for p in text.split(" / ") if p.strip()]
        elif "/" in text:
            parts = [p.strip() for p in text.split("/") if p.strip()]
        else:
            # Если разделителей нет, возвращаем как одно значение
            parts = [text.strip()] if text.strip() else []
        
        # Очищаем от пустых и системных символов
        clean_parts = []
        for part in parts:
            if part and part not in ["/", ";", ",", "-"]:
                clean_parts.append(part)
        
        return clean_parts

    def delayed_check_after_merge(self):
        """Проверка состояния данных через 1 секунду после объединения"""
        print("\n" + "🕐"*100)
        print("🕐 ОТЛОЖЕННАЯ ПРОВЕРКА ЧЕРЕЗ 1 СЕКУНДУ:")
        print("🎯 ФИНАЛЬНАЯ ПРОВЕРКА ВСЕЙ ТАБЛИЦЫ:")
        for item in self.tree.get_children():
            values = self.tree.item(item, "values")
            if "Multi-Stream" in values[1] or "Multi-stream" in values[1]:
                print(f"  📋 {values[1]}: '{values[5]}'")  # Параметр: Результат разделения
        print("🕐"*100)
    
    def merge_similar_values_in_set(self, values_set):
        """Объединяет похожие значения в наборе, оставляя наиболее частые"""
        if not values_set:
            return values_set
            
        values_list = list(values_set)
        return self.merge_similar_values_in_list(values_list)
    
    def merge_similar_values_in_list(self, values_list):
        """Улучшенное объединение похожих значений в списке"""
        if not values_list:
            return []
        # 🆕 НОРМАЛИЗАЦИЯ ФОРМАТА ЗНАЧЕНИЙ ПЕРЕД ОБЪЕДИНЕНИЕМ
        clean_values = [normalize_value_format(v.strip()) for v in values_list if v and v.strip()]
        if not clean_values:
            return []
        
        merged_values = []
        processed = set()
        
        # Подсчитываем частоту каждого конкретного значения в исходных данных
        value_frequencies = {}
        for sku_data in self.original_values.values():
            for param_values in sku_data.values():
                if param_values:
                    if isinstance(param_values, str):
                        # Более аккуратное разбиение значений
                        vals = self.split_parameter_values(param_values)
                        for val in vals:
                            if val in clean_values:
                                value_frequencies[val] = value_frequencies.get(val, 0) + 1
        
        # Группируем похожие значения
        similar_groups = {}
        for value in clean_values:
            if value in processed:
                continue
                
            group = [value]
            for other_value in clean_values:
                if other_value != value and other_value not in processed:
                    if self.are_values_similar_conservative(value, other_value):
                        group.append(other_value)
            
            # Выбираем представителя группы
            if len(group) > 1:
                # Сортируем по частоте, потом по длине (предпочитаем более полные варианты)
                representative = max(group, key=lambda x: (
                    value_frequencies.get(x, 0),  # Частота использования
                    len(x),  # Длина (более полные варианты)
                    -clean_values.index(x)  # Порядок появления (обратный)
                ))
            else:
                representative = group[0]
            
            merged_values.append(representative)
            processed.update(group)
        
        # Сохраняем исходный порядок
        result = []
        for value in clean_values:
            if value not in result and value in merged_values:
                result.append(value)
            elif value not in result:
                # Находим представителя группы для этого значения
                for merged_val in merged_values:
                    if self.are_values_similar_conservative(value, merged_val):
                        if merged_val not in result:
                            result.append(merged_val)
                        break
        
        return result
    
    def are_values_similar_advanced(self, val1, val2):
        """Расширенная проверка схожести значений для объединения"""
        val1_norm = normalize_variant(val1).lower()
        val2_norm = normalize_variant(val2).lower()
        
        # Точное совпадение после нормализации
        if val1_norm == val2_norm:
            return True
        
        # Одно значение содержится в другом (например, "Support" в "Supported")
        if val1_norm in val2_norm or val2_norm in val1_norm:
            # Дополнительная проверка: разница в длине не должна быть слишком большой
            len_diff = abs(len(val1_norm) - len(val2_norm))
            max_len = max(len(val1_norm), len(val2_norm))
            if max_len > 0 and len_diff / max_len <= 0.3:  # Максимум 30% разницы
                return True
        
        # Проверка на опечатки для коротких значений
        if max(len(val1_norm), len(val2_norm)) <= 10:
            return self.check_typo_similarity(val1_norm, val2_norm)
        
        return False
    
    def are_values_similar_conservative(self, val1, val2):
        """Консервативная проверка схожести значений - более строгая"""
        if not val1 or not val2:
            return False
            
        val1_norm = normalize_variant(val1).lower().strip()
        val2_norm = normalize_variant(val2).lower().strip()
        
        # Точное совпадение после нормализации
        if val1_norm == val2_norm:
            return True
        
        # Проверяем только очевидные варианты
        # Один является подстрокой другого, но с ограничениями
        if len(val1_norm) >= 3 and len(val2_norm) >= 3:
            # Разрешаем только если одно значение почти полностью содержит другое
            if val1_norm in val2_norm:
                # Разница не более 20% от длины большего значения
                if (len(val2_norm) - len(val1_norm)) / len(val2_norm) <= 0.2:
                    return True
            elif val2_norm in val1_norm:
                if (len(val1_norm) - len(val2_norm)) / len(val1_norm) <= 0.2:
                    return True
        
        # Проверка только явных опечаток для коротких значений
        if 3 <= max(len(val1_norm), len(val2_norm)) <= 8:
            # Только одна замена символа
            if abs(len(val1_norm) - len(val2_norm)) <= 1:
                from difflib import SequenceMatcher
                similarity = SequenceMatcher(None, val1_norm, val2_norm).ratio()
                if similarity >= 0.85:  # Очень высокая схожесть
                    return True
        
        return False

    def split_parameter_values(self, param_values):
        """Более аккуратное разбиение значений параметров"""
        if not param_values:
            return []
        
        # Пробуем разные разделители
        for delimiter in [';', ',', '|']:
            if delimiter in param_values:
                values = [v.strip() for v in param_values.split(delimiter) if v.strip()]
                if len(values) > 1:
                    return values
        
        # Если разделителей нет, возвращаем как есть
        return [param_values.strip()] if param_values.strip() else []

    def are_values_similar(self, val1, val2):
        """Проверяет, являются ли два значения похожими"""
        # Удаляем пунктуацию и приводим к нижнему регистру
        import string
        val1_clean = val1.translate(str.maketrans('', '', string.punctuation)).replace(' ', '')
        val2_clean = val2.translate(str.maketrans('', '', string.punctuation)).replace(' ', '')
        
        # Проверяем, содержит ли одно значение другое
        if val1_clean in val2_clean or val2_clean in val1_clean:
            return True
            
        # Проверяем схожесть с помощью SequenceMatcher
        similarity = SequenceMatcher(None, val1_clean, val2_clean).ratio()
        return similarity > 0.8  # 80% схожести

    def apply_value_merges(self, merge_data):
        """Применение объединений значений"""
        if not merge_data:
            return
            
        # Проходим по всем элементам дерева и применяем объединения значений
        for item in self.tree.get_children():
            values = list(self.tree.item(item, "values"))
            param = values[1]  # Исходный параметр
            orig_values = values[2]  # Исходные значения
            result_values = values[5]  # Результат разделения
            
            # Проверяем, есть ли объединения для этого параметра
            if param in merge_data:
                param_merges = merge_data[param]
                
                # Применяем объединения к исходным значениям
                if orig_values:
                    merged_orig_values = self.apply_value_merges_to_string(orig_values, param_merges)
                    values[2] = merged_orig_values
                
                # Применяем объединения к результирующим значениям
                if result_values:
                    merged_result_values = self.apply_value_merges_to_string(result_values, param_merges)
                    values[5] = merged_result_values
                
                # Обновляем элемент дерева
                self.tree.item(item, values=values)
        
        # ВРЕМЕННО УБРАНО: Обновление DataFrame может вызывать проблемы  
        # self.df = pd.DataFrame([{
        #     "№": int(values[0]),
        #     "Исходный параметр": values[1],
        #     "Исходные значения": values[2],
        #     "Новый параметр": values[3],
        #     "Разделитель": values[4],
        #     "Результат разделения": values[5]
        # } for item in self.tree.get_children() for values in [self.tree.item(item, "values")]])
        
        # УБРАНО: self.refresh_treeview_with_colors() - ВТОРАЯ ФУНКЦИЯ тоже перезаписывала результаты!
        # Обновление цветов не критично для объединения значений
        
        # 🆕 ДИНАМИЧЕСКОЕ ОБНОВЛЕНИЕ после объединения значений v4.9
        print("🔄 Выполняем динамическое обновление после объединения значений...")
        self.dynamic_update_main_table()
    
    def apply_value_merges_to_string(self, values_string, param_merges):
        """Применяет объединения значений к строке значений"""
        if not values_string or not param_merges:
            return values_string
            
        # Разбиваем строку на отдельные значения
        values_list = [v.strip() for v in values_string.split(';') if v.strip()]
        merged_values = []
        processed_values = set()
        
        for value in values_list:
            if value in processed_values:
                continue
                
            # Проверяем, нужно ли объединить это значение
            merged = False
            for representative, merge_info in param_merges.items():
                if merge_info.get('selected', False):
                    similar_values = merge_info.get('similar_values', [])
                    if value in similar_values:
                        # Добавляем представителя группы, если его еще нет
                        if representative not in merged_values:
                            merged_values.append(representative)
                        processed_values.update(similar_values)
                        merged = True
                        break
            
            # Если значение не было объединено, добавляем его как есть
            if not merged:
                merged_values.append(value)
                processed_values.add(value)
        
        return "; ".join(merged_values)

    def clean_cell(self, value):
        """🔧 ИСПРАВЛЕНО: Улучшенная очистка ячейки"""
        if not isinstance(value, str):
            return value
        value = value.strip()
        
        # Используем улучшенную функцию очистки
        return clean_value_enhanced(value)

    def remove_duplicates_from_cell(self, cell_value):
        """🔧 ИСПРАВЛЕНО: Улучшенное удаление дубликатов из ячейки"""
        if not cell_value:
            return cell_value
        seps = [',', ';']
        sep = None
        for s in seps:
            if s in cell_value:
                sep = s
                break
        if sep is None:
            return cell_value
        
        items = [self.clean_cell(v) for v in cell_value.split(sep)]
        seen = set()
        unique = []
        
        for v in items:
            # Проверяем валидность значения
            if v and is_valid_value_enhanced(v):
                norm_v = normalize_variant(v).lower()
                if norm_v not in seen:
                    seen.add(norm_v)
                    unique.append(v)
        
        return sep.join(unique) if unique else ""

    def remove_duplicates_from_list(self, values):
        seen = set()
        result = []
        removed_duplicates = []
        
        for v in values:
            if v:
                norm_v = normalize_variant(v).lower()
                if norm_v not in seen:
                    seen.add(norm_v)
                    result.append(v)
                else:
                    removed_duplicates.append(v)
        
        if removed_duplicates:
            print(f"    🗑️ УДАЛЕНЫ ДУБЛИ: {removed_duplicates} (оставлены: {result})")
        
        return result

    def parse_html_table(self, html):
        """🆕 УЛУЧШЕННЫЙ ПАРСЕР HTML-ТАБЛИЦ для многоколоночных структур
        
        🔧 ВАЖНО: Этот метод сохраняет ИСХОДНЫЕ данные без перевода!
        Переводы применяются только в процессе отображения результатов.
        """
        soup = BeautifulSoup(html, 'html.parser')
        params = {}
        current_param = None
        
        for tr in soup.find_all('tr'):
            tds = tr.find_all('td')

            if len(tds) == 0:
                continue

            # 🆕 ОБРАБОТКА СТРОК С ОДНОЙ ЯЧЕЙКОЙ
            if len(tds) == 1:
                colspan = tds[0].get('colspan')
                try:
                    colspan_num = int(colspan) if colspan else 1
                except:
                    colspan_num = 1
                
                cell_text = self.clean_cell(tds[0].get_text())
                
                # Если это заголовок секции (colspan >= 2), пропускаем
                if colspan_num >= 2:
                    current_param = None
                    continue
                    
                # Если это дополнительное значение для текущего параметра
                if current_param and cell_text:
                    # 🔧 ИСПРАВЛЕНО: Сохраняем исходные данные без перевода
                    params[current_param] += f"; {cell_text}"
                continue

            # 🆕 ОБРАБОТКА МНОГОКОЛОНОЧНЫХ СТРОК (2+ столбцов)
            if len(tds) >= 2:
                # Первый столбец - название параметра
                param_cell = tds[0]
                param = self.clean_cell(param_cell.get_text())
                
                # Если первый столбец пустой или это заголовок с colspan, пропускаем
                if not param:
                    current_param = None
                    continue
                
                param_colspan = param_cell.get('colspan')
                try:
                    param_colspan_num = int(param_colspan) if param_colspan else 1
                except:
                    param_colspan_num = 1
                
                # Если параметр занимает несколько столбцов, это заголовок секции
                if param_colspan_num >= 2:
                    current_param = None
                    continue
                
                # 🆕 СОБИРАЕМ ЗНАЧЕНИЯ ИЗ ВСЕХ ОСТАЛЬНЫХ СТОЛБЦОВ
                values = []
                for i in range(1, len(tds)):
                    value_cell = tds[i]
                    value = self.clean_cell(value_cell.get_text())
                    
                    if value:
                        # Проверяем colspan значения
                        value_colspan = value_cell.get('colspan')
                        try:
                            value_colspan_num = int(value_colspan) if value_colspan else 1
                        except:
                            value_colspan_num = 1
                        
                        values.append(value)
                        
                        # Если значение занимает несколько столбцов, 
                        # пропускаем соответствующее количество следующих ячеек
                        if value_colspan_num > 1:
                            i += value_colspan_num - 1
                
                # 🆕 ОБЪЕДИНЯЕМ ВСЕ НАЙДЕННЫЕ ЗНАЧЕНИЯ
                if param and values:
                    # Удаляем дубликаты значений, сохраняя порядок
                    unique_values = []
                    seen = set()
                    for val in values:
                        val_normalized = val.lower().strip()
                        if val_normalized not in seen:
                            seen.add(val_normalized)
                            unique_values.append(val)
                    
                    combined_value = "; ".join(unique_values)
                    
                    # 🔧 ИСПРАВЛЕНО: Сохраняем исходные данные без перевода
                    params[param] = combined_value
                    current_param = param
                else:
                    current_param = None
                continue

            current_param = None
            
        return params

    def find_similar_param_groups(self, param_series):
        """
        🚨 ЗАБЛОКИРОВАННАЯ СТАРАЯ ФУНКЦИЯ - НЕ ИСПОЛЬЗОВАТЬ!
        
        Эта функция была источником ошибочной группировки Weight + Night.
        Заменена на find_similar_parameters_advanced().
        """
        print("🚨 ВНИМАНИЕ: Вызвана заблокированная старая функция find_similar_param_groups!")
        print("🚨 Используйте find_similar_parameters_advanced() вместо неё!")
        
        # Возвращаем пустой результат, чтобы не было группировки по старым критериям
        return {param: 0 for param in param_series}

    def update_mappings_from_params(self, params):
        """Обновление маппингов из параметров"""
        for param in params:
            if param not in self.mappings:
                self.mappings[param] = param

    # ❌ УДАЛЕН метод replace_mapped_values_in_str - ИСТОЧНИК ЧАСТИЧНОГО ПЕРЕВОДА
    # Этот метод применял сопоставления к значениям, что вызывало частичный перевод
    # Теперь сопоставления применяются ТОЛЬКО к параметрам

    def apply_case_normalization(self, value):
        """Применение нормализации регистра"""
        if not value or not isinstance(value, str):
            return value
        
        # Проверяем точное совпадение
        if value in self.case_normalization:
            return self.case_normalization[value]
        
        # Проверяем нечувствительное к регистру совпадение
        for norm_value, replacement in self.case_normalization.items():
            if value.lower() == norm_value.lower():
                return replacement
        
        return value

    def apply_case_normalization_in_str(self, value, delimiter=';'):
        """Применение нормализации регистра в строке"""
        if not value:
            return value
        
        parts = [part.strip() for part in value.split(delimiter)]
        normalized_parts = [self.apply_case_normalization(part) for part in parts]
        
        return delimiter.join(normalized_parts)

    def split_values(self, value, delimiter):
        """
        🆕 УЛУЧШЕННОЕ разделение значений с поддержкой защищенных значений
        
        НОВЫЕ ВОЗМОЖНОСТИ:
        - Поддержка множественных разделителей (например: ",/" означает разделение по "," И по "/")
        - 🛡️ ЗАЩИЩЕННЫЕ ЗНАЧЕНИЯ: не разделяются, даже если содержат символ разделителя
        - Пример: "Auto (ICR) / Color / B/W" с защищенным "B/W" → ["Auto (ICR)", "Color", "B/W"]
        """
        if not value:
            return []
        
        # 🆕 ПРОВЕРКА РАЗДЕЛИТЕЛЯ v4.8.5
        if not delimiter:
            print("⚠️ ПУСТОЙ РАЗДЕЛИТЕЛЬ - используем стандартный ';'")
            delimiter = ";"
        
        # 🛡️ ЗАЩИТА ЗНАЧЕНИЙ ОТ РАЗДЕЛЕНИЯ
        protected_replacements = {}
        modified_value = value
        
        if self.protected_values:
            print(f"🛡️ ПРОВЕРКА ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ: {self.protected_values}")
            
            for i, protected in enumerate(self.protected_values):
                if protected in modified_value:
                    placeholder = f"__PROTECTED_{i}__"
                    protected_replacements[placeholder] = protected
                    modified_value = modified_value.replace(protected, placeholder)
                    print(f"  🔒 ЗАЩИЩЕНО: '{protected}' → '{placeholder}'")
            
            print(f"  📋 После защиты: '{modified_value}'")
        
        # 🔍 ОБРАБОТКА МНОЖЕСТВЕННЫХ РАЗДЕЛИТЕЛЕЙ
        if len(delimiter) > 1 and delimiter != "; ":
            # Множественные разделители (например: ",/" означает разделение по "," И "/")
            import re
            
            # Создаем регулярное выражение для разделения по любому из символов
            # Экранируем специальные символы для regex
            escaped_chars = [re.escape(char) for char in delimiter]
            pattern = '[' + ''.join(escaped_chars) + ']'
            
            print(f"🔍 МНОЖЕСТВЕННЫЕ РАЗДЕЛИТЕЛИ: '{delimiter}' → pattern: '{pattern}'")
            result_values = [v.strip() for v in re.split(pattern, modified_value) if v.strip()]
        else:
            # Обычное разделение по одному разделителю
            # 🆕 ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА РАЗДЕЛИТЕЛЯ v4.8.5
            if not delimiter:
                delimiter = ";"
                print("⚠️ АВАРИЙНАЯ ЗАМЕНА: пустой разделитель → ';'")
            
            result_values = [v.strip() for v in modified_value.split(delimiter) if v.strip()]
        
        # 🔓 ВОССТАНОВЛЕНИЕ ЗАЩИЩЕННЫХ ЗНАЧЕНИЙ
        if protected_replacements:
            restored_values = []
            for val in result_values:
                if val in protected_replacements:
                    restored_val = protected_replacements[val]
                    print(f"  🔓 ВОССТАНОВЛЕНО: '{val}' → '{restored_val}'")
                    restored_values.append(restored_val)
                else:
                    restored_values.append(val)
            result_values = restored_values
        
        # 🔧 ИСПРАВЛЕНО: Очистка невалидных значений
        cleaned_values = []
        for val in result_values:
            cleaned = clean_value_enhanced(val)
            if is_valid_value_enhanced(cleaned):
                cleaned_values.append(cleaned)
            else:
                print(f"  🗑️ Удалено невалидное значение: '{val}'")
        
        # 🆕 УДАЛЕНИЕ ДУБЛИКАТОВ ПОСЛЕ РАЗДЕЛЕНИЯ
        # Используем метод remove_duplicates_from_list, который учитывает варианты написания
        result_values = self.remove_duplicates_from_list(cleaned_values)
        
        print(f"  ✅ ФИНАЛЬНЫЙ РЕЗУЛЬТАТ (очищенный, без дубликатов): {result_values}")
        return result_values

    def refresh_treeview_with_colors(self):
        """Обновление раскраски дерева"""
        # Настраиваем теги для раскраски
        colors = ['#f0f0f0', '#e0e0e0', '#d0d0d0', '#c0c0c0', '#b0b0b0']
        
        # Определяем группы параметров по их схожести
        group_colors = {}
        color_index = 0
        
        for item in self.tree.get_children():
            values = self.tree.item(item, "values")
            tags = self.tree.item(item, "tags")
            
            if tags:
                group_tag = tags[0]
                if group_tag not in group_colors:
                    group_colors[group_tag] = colors[color_index % len(colors)]
                    color_index += 1
                
                # Настраиваем тег с цветом
                self.tree.tag_configure(group_tag, background=group_colors[group_tag])
            else:
                # Для элементов без группы используем белый фон
                self.tree.tag_configure("no_group", background="white")
                self.tree.item(item, tags=("no_group",))

    def auto_clean_duplicate_values(self):
        """Автоматическая очистка дубликатов"""
        # Простая реализация
        return 0

    def auto_suggest_parameter_merge(self):
        """🚨 ОТКЛЮЧЕНА: Автоматическое предложение объединения параметров
        
        ЭТА ФУНКЦИЯ СОЗДАВАЛА ДУБЛИРОВАНИЕ В param_merge_settings!
        Теперь используется только find_similar_parameters_for_tab()
        """
        print("🚨 ФУНКЦИЯ auto_suggest_parameter_merge ОТКЛЮЧЕНА - источник дублирования!")
        return
        
        # СТАРЫЙ КОД ЗАКОММЕНТИРОВАН
        # if not hasattr(self, 'similar_groups') or not self.similar_groups:
        #     return
        # 
        # # Собираем группы похожих параметров
        # param_groups = {}
        # for param, group_id in self.similar_groups.items():
        #     if group_id not in param_groups:
        #         param_groups[group_id] = []
        #     param_groups[group_id].append(param)
        # 
        # # Создаем предложения для объединения групп с более чем одним параметром
        # suggestions = []
        # for group_id, params in param_groups.items():
        #     if len(params) > 1:
        #         # Выбираем наиболее частый параметр как представителя
        #         param_frequencies = {}
        #         for param in params:
        #             count = 0
        #             for sku_data in self.original_values.values():
        #                 if param in sku_data:
        #                     count += 1
        #             param_frequencies[param] = count
        #         
        #         if param_frequencies:
        #             representative = max(param_frequencies.items(), key=lambda x: x[1])[0]
        #             suggestions.append((representative, params))
        # 
        # # Сохраняем предложения в настройки объединения параметров
        # for representative, similar_params in suggestions:
        #     self.param_merge_settings[representative] = {
        #         'similar_params': similar_params,
        #         'selected': False
        #     }
        # 
        # # Обновляем дерево параметров на вкладке объединения
        # if hasattr(self, 'params_tree'):
        #     self.update_params_tree()

    def update_param_frequency(self, params):
        """ИСПРАВЛЕННОЕ обновление частоты использования параметров"""
        print("📊 ПОДСЧЕТ ЧАСТОТЫ ИСПОЛЬЗОВАНИЯ ПАРАМЕТРОВ")
        self.param_frequency = {}
        
        for param in params:
            count = 0
            for sku_params in self.original_values.values():
                if param in sku_params:
                    count += 1
            self.param_frequency[param] = count
        
        # Показываем статистику для отладки
        total_params = len(params)
        print(f"📋 Обработано параметров: {total_params}")
        
        if total_params > 0:
            avg_frequency = sum(self.param_frequency.values()) / total_params
            max_frequency = max(self.param_frequency.values()) if self.param_frequency else 0
            print(f"📊 Средняя частота: {avg_frequency:.1f}")
            print(f"📊 Максимальная частота: {max_frequency}")
            
            # Показываем примеры Multi параметров для демонстрации
            multi_params = [p for p in params if 'multi' in p.lower()]
            if multi_params:
                print("🔍 Multi параметры и их частота:")
                for param in sorted(multi_params, key=lambda x: self.param_frequency.get(x, 0), reverse=True):
                    freq = self.param_frequency.get(param, 0)
                    print(f"  • {param}: {freq} раз")

    def find_similar_parameters_advanced(self, params):
        """
        ИСПРАВЛЕННЫЙ поиск похожих параметров с правильной логикой группировки
        
        ИСПРАВЛЕНИЯ:
        - Повышены пороги схожести для уменьшения ложных срабатываний
        - Добавлена проверка на содержательную похожесть (общие слова)
        - Исправлена логика выбора основного параметра по частоте
        - Убрано дублирование основных параметров
        """
        print("🔍 ПОИСК ПОХОЖИХ ПАРАМЕТРОВ - ИСПРАВЛЕННАЯ ВЕРСИЯ")
        print(f"📊 Получено параметров для обработки: {len(params)}")
        
        similar_groups = {}
        processed = set()
        
        for i, param1 in enumerate(params):
            if param1 in processed:
                continue
                
            group = [param1]
            
            for j, param2 in enumerate(params[i+1:], i+1):
                if param2 in processed:
                    continue
                
                # БОЛЕЕ СТРОГИЕ КРИТЕРИИ СХОЖЕСТИ
                is_similar = self.are_parameters_truly_similar(param1, param2)
                
                if is_similar:
                    group.append(param2)
                    print(f"  ✅ Найдена похожая пара: '{param1}' ↔ '{param2}'")
            
            if len(group) > 1:
                # 🆕 ИСПРАВЛЕНИЕ v4.9.2: Выбираем параметр с НАИБОЛЬШИМ КОЛИЧЕСТВОМ ЗНАЧЕНИЙ как представителя
                def get_value_count(param_name):
                    """Подсчитывает количество значений в параметре"""
                    total_values = 0
                    for sku_params in self.original_values.values():
                        if param_name in sku_params:
                            values = sku_params[param_name]
                            if isinstance(values, list):
                                total_values += len([v for v in values if v.strip()])
                            elif isinstance(values, str) and values.strip():
                                # Считаем количество значений разделенных точкой с запятой
                                parts = [v.strip() for v in values.split(';') if v.strip()]
                                total_values += len(parts)
                    return total_values
                
                # Выбираем параметр с максимальным количеством значений
                value_counts = {}
                for param in group:
                    value_counts[param] = get_value_count(param)
                
                representative = max(group, key=lambda x: value_counts.get(x, 0))
                
                # 🔥 УБИРАЕМ ИЗ ГРУППЫ ПРЕДСТАВИТЕЛЯ - оставляем только похожие
                other_params = [p for p in group if p != representative]
                
                print(f"🎯 ГРУППА НАЙДЕНА:")
                print(f"  👑 ОСНОВНОЙ: {representative}")
                print(f"      📊 Количество значений: {value_counts.get(representative, 0)}")
                print(f"      📈 Частота: {self.param_frequency.get(representative, 0)}")
                
                for param in other_params:
                    values_count = value_counts.get(param, 0)
                    freq = self.param_frequency.get(param, 0)
                    print(f"     ↳ {param}")
                    print(f"       📊 Количество значений: {values_count}")
                    print(f"       📈 Частота: {freq}")
                
                # 🔥 КЛЮЧЕВОЕ ИСПРАВЛЕНИЕ: Сохраняем представителя → список остальных
                # 🔥 ДОПОЛНИТЕЛЬНАЯ ЗАЩИТА: Убираем представителя если случайно попал
                clean_other_params = [p for p in other_params if p != representative]
                similar_groups[representative] = clean_other_params
                
                print(f"  🔍 ПРОВЕРКА ДУБЛИРОВАНИЯ:")
                print(f"     Представитель: {representative}")
                print(f"     Список похожих: {clean_other_params}")
                if representative in clean_other_params:
                    print(f"  🚨 ОШИБКА: Представитель найден в списке похожих!")
                else:
                    print(f"  ✅ ОК: Дублирования нет")
                
                # 🔥 ОТМЕЧАЕМ ВСЕ ПАРАМЕТРЫ ГРУППЫ КАК ОБРАБОТАННЫЕ
                processed.update(group)
            else:
                # Если группа из одного элемента - просто отмечаем как обработанный
                processed.add(param1)
        
        print(f"📊 ИТОГО НАЙДЕНО ГРУПП: {len(similar_groups)}")
        return similar_groups
    
    def are_parameters_truly_similar(self, param1, param2):
        """
        🚨 ЭКСТРЕМАЛЬНАЯ ОТЛАДКА: Проверяет, действительно ли параметры похожи
        """
        print(f"\n🔍 ПРОВЕРКА СХОЖЕСТИ: '{param1}' ↔ '{param2}'")
        
        param1_lower = param1.lower().strip()
        param2_lower = param2.lower().strip()
        
        # 1. ТОЧНОЕ СОВПАДЕНИЕ (разный регистр)
        if param1_lower == param2_lower:
            print(f"  ✅ ТОЧНОЕ СОВПАДЕНИЕ")
            return True
        
        # 2. ПРОВЕРКА НА ОБЩИЕ КЛЮЧЕВЫЕ СЛОВА
        words1 = set(re.findall(r'\b\w+\b', param1_lower))
        words2 = set(re.findall(r'\b\w+\b', param2_lower))
        
        print(f"  📋 Слова1: {words1}")
        print(f"  📋 Слова2: {words2}")
        
        # Исключаем очень общие слова
        common_words = {'and', 'or', 'the', 'a', 'an', 'is', 'are', 'in', 'on', 'at', 'to', 'for', 'of', 'with'}
        words1_filtered = words1 - common_words
        words2_filtered = words2 - common_words
        
        print(f"  📋 Слова1 (отфильтр): {words1_filtered}")
        print(f"  📋 Слова2 (отфильтр): {words2_filtered}")
        
        if not words1_filtered or not words2_filtered:
            print(f"  ❌ ОТКЛОНЕНО: Пустые слова после фильтрации")
            return False
            
        # 🔥 УСИЛЕННЫЕ КРИТЕРИИ: Должно быть значимое пересечение слов
        common_significant_words = words1_filtered & words2_filtered
        print(f"  📋 Общие слова: {common_significant_words}")
        
        if not common_significant_words:
            print(f"  ❌ ОТКЛОНЕНО: Нет общих значимых слов")
            return False
        
        # 🔥 НОВЫЙ КРИТЕРИЙ: Отклоняем если общие слова слишком общие
        # Слова типа "stream", "dual", "output", "input" могут встречаться в разных контекстах
        very_common_tech_words = {'stream', 'output', 'input', 'mode', 'type', 'format', 'level', 'rate', 'dual', 'multi'}
        only_common_tech_words = common_significant_words.issubset(very_common_tech_words)
        
        if only_common_tech_words and len(common_significant_words) == 1:
            common_word = list(common_significant_words)[0]
            print(f"  ❌ ОТКЛОНЕНО: Только одно техническое слово '{common_word}' недостаточно")
            return False
        
        # 🔥 ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: Если есть общие слова, но контекст разный
        if len(common_significant_words) == 1:
            # Проверяем, не слишком ли разные остальные слова
            unique_words1 = words1_filtered - common_significant_words
            unique_words2 = words2_filtered - common_significant_words
            
            if unique_words1 and unique_words2:
                # Есть уникальные слова в обеих группах - может быть разный контекст
                print(f"  📋 Уникальные слова1: {unique_words1}")
                print(f"  📋 Уникальные слова2: {unique_words2}")
                
                # Проверяем семантическую совместимость
                incompatible_pairs = [
                    ({'multi'}, {'adc', 'dac', 'analog', 'digital'}),
                    ({'dual'}, {'ir', 'infrared', 'night'}),
                    ({'audio'}, {'video', 'image', 'picture'}),
                    ({'ip'}, {'stream', 'streaming', 'video', 'audio'}),  # 🆕 IP ≠ Stream!
                    ({'stream'}, {'ip', 'protocol', 'network', 'address'}),  # 🆕 Stream ≠ IP!
                ]
                
                for set1, set2 in incompatible_pairs:
                    if (unique_words1 & set1 and unique_words2 & set2) or (unique_words1 & set2 and unique_words2 & set1):
                        print(f"  ❌ ОТКЛОНЕНО: Семантически несовместимые контексты")
                        return False
        
        # 3. ВЫСОКАЯ СХОЖЕСТЬ ПО НЕСКОЛЬКИМ МЕТРИКАМ ОДНОВРЕМЕННО
        ratio = fuzz.ratio(param1_lower, param2_lower)
        token_sort_ratio = fuzz.token_sort_ratio(param1_lower, param2_lower)
        token_set_ratio = fuzz.token_set_ratio(param1_lower, param2_lower)
        
        print(f"  📊 Метрики схожести:")
        print(f"    • ratio: {ratio}")
        print(f"    • token_sort_ratio: {token_sort_ratio}")
        print(f"    • token_set_ratio: {token_set_ratio}")
        
        # БОЛЕЕ СТРОГИЕ ПОРОГИ
        high_threshold = 85
        
        # Требуем высокой схожести по НЕСКОЛЬКИМ метрикам
        high_similarity_count = 0
        if ratio >= high_threshold:
            high_similarity_count += 1
            print(f"    ✅ ratio >= {high_threshold}")
        if token_sort_ratio >= high_threshold:
            high_similarity_count += 1
            print(f"    ✅ token_sort_ratio >= {high_threshold}")
        if token_set_ratio >= high_threshold:
            high_similarity_count += 1
            print(f"    ✅ token_set_ratio >= {high_threshold}")
        
        print(f"  📊 Высоких метрик: {high_similarity_count}/3")
        
        # Требуем минимум 2 из 3 метрик с высокой схожестью
        if high_similarity_count >= 2:
            print(f"  ✅ ПРИНЯТО: Высокая схожесть по {high_similarity_count} метрикам")
            return True
        
        # 4. ПРОВЕРКА НА ПРОСТЫЕ ВАРИАЦИИ (дефисы, пробелы, регистр)
        normalized1 = re.sub(r'[-_\s]+', '', param1_lower)
        normalized2 = re.sub(r'[-_\s]+', '', param2_lower)
        
        print(f"  📋 Нормализованные: '{normalized1}' ↔ '{normalized2}'")
        
        if normalized1 == normalized2:
            print(f"  ✅ ПРИНЯТО: Одинаковые после нормализации")
            return True
        
        # 5. ПРОВЕРКА НА ОЧЕНЬ ПОХОЖИЕ ОКОНЧАНИЯ/НАЧАЛА
        if len(common_significant_words) > 0:
            print(f"  🔍 Проверка похожих окончаний...")
            for word1 in words1_filtered:
                for word2 in words2_filtered:
                    if (len(word1) >= 4 and len(word2) >= 4 and
                        (word1.startswith(word2[:4]) or word2.startswith(word1[:4])) and
                        abs(len(word1) - len(word2)) <= 3):
                        edit_ratio = fuzz.ratio(word1, word2)
                        print(f"    📋 Сравнение '{word1}' ↔ '{word2}': {edit_ratio}")
                        if edit_ratio >= 80:
                            print(f"    ✅ ПРИНЯТО: Похожие окончания ({edit_ratio}%)")
                            return True
        
        print(f"  ❌ ОТКЛОНЕНО: Не прошли все проверки")
        return False
    
    def calculate_edit_distance(self, s1, s2):
        """Вычисляет расстояние Левенштейна между двумя строками"""
        if len(s1) < len(s2):
            return self.calculate_edit_distance(s2, s1)
        
        if len(s2) == 0:
            return len(s1)
        
        previous_row = list(range(len(s2) + 1))
        for i, c1 in enumerate(s1):
            current_row = [i + 1]
            for j, c2 in enumerate(s2):
                insertions = previous_row[j + 1] + 1
                deletions = current_row[j] + 1
                substitutions = previous_row[j] + (c1 != c2)
                current_row.append(min(insertions, deletions, substitutions))
            previous_row = current_row
        
        return previous_row[-1]
    
    def check_typo_similarity(self, s1, s2):
        """Проверяет, являются ли строки опечатками друг друга"""
        len_diff = abs(len(s1) - len(s2))
        max_len = max(len(s1), len(s2))
        min_len = min(len(s1), len(s2))
        
        # Слишком большая разница в длине - не опечатка
        if len_diff > 2:
            return False
        
        # Если одно слово значительно длиннее другого (более чем на 30%), это скорее не опечатка
        if max_len > 0 and len_diff / max_len > 0.4:
            return False
            
        distance = self.calculate_edit_distance(s1, s2)
        
        # Специальная обработка для очень коротких слов (2-3 символа)
        if max_len <= 3:
            return distance == 1  # Только одна ошибка
        
        # Для коротких слов (4-6 символов)
        if max_len <= 6:
            # Если разница в длине больше 1 символа, то должно быть расстояние 1
            if len_diff > 1:
                return distance == 1
            else:
                return distance <= 2
        
        # Для средних слов (7-10 символов)
        if max_len <= 10:
            return distance <= 2
        
        # Для длинных слов
        return distance <= 3

    def find_similar_values_in_all_parameters(self):
        """Поиск похожих значений во всех параметрах"""
        similar_values_data = {}
        
        # Собираем все значения для каждого параметра
        param_values = defaultdict(set)
        for sku_params in self.original_values.values():
            for param, values_list in sku_params.items():
                if isinstance(values_list, list):
                    param_values[param].update(values_list)
                else:
                    param_values[param].add(values_list)
        
        # Ищем похожие значения в каждом параметре
        for param, values in param_values.items():
            values_list = list(values)
            if len(values_list) < 2:
                continue
                
            similar_groups = {}
            processed = set()
            
            for i, val1 in enumerate(values_list):
                if val1 in processed:
                    continue
                    
                group = [val1]
                for j, val2 in enumerate(values_list[i+1:], i+1):
                    if val2 in processed:
                        continue
                        
                    # Различные метрики схожести
                    ratio = fuzz.ratio(val1.lower(), val2.lower())
                    token_sort_ratio = fuzz.token_sort_ratio(val1.lower(), val2.lower())
                    
                    if ratio >= 80 or token_sort_ratio >= 85:
                        group.append(val2)
                        processed.add(val2)
                
                if len(group) > 1:
                    representative = min(group, key=len)  # Выбираем самое короткое как представителя
                    similar_groups[representative] = group
                    processed.update(group)
            
            if similar_groups:
                similar_values_data[param] = similar_groups
        
        return similar_values_data

    # 🆕 ФУНКЦИИ ДЛЯ РЕДАКТИРОВАНИЯ ГРУПП ПАРАМЕТРОВ

    def show_params_context_menu(self, event):
        """Показать контекстное меню для таблицы параметров"""
        try:
            # Определяем, на какой строке был клик
            item = self.params_tree.identify_row(event.y)
            if item:
                self.params_tree.selection_set(item)
                self.params_context_menu.post(event.x_root, event.y_root)
        except:
            pass

    def edit_param_group(self):
        """Редактировать группу параметров"""
        try:
            selection = self.params_tree.selection()
            if not selection:
                messagebox.showwarning("Предупреждение", "Выберите группу для редактирования.")
                return
            
            item = selection[0]
            values = self.params_tree.item(item, "values")
            representative = values[1]  # Основной параметр
            current_similar = values[2]  # Похожие параметры
            
            if representative not in self.param_merge_settings:
                messagebox.showerror("Ошибка", "Группа не найдена в настройках.")
                return
            
            # Открываем диалог редактирования
            self.open_group_edit_dialog(representative, current_similar)
            
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка при редактировании группы: {e}")

    def remove_from_param_group(self):
        """Удалить выбранные параметры из группы"""
        try:
            selection = self.params_tree.selection()
            if not selection:
                messagebox.showwarning("Предупреждение", "Выберите группу для редактирования.")
                return
            
            item = selection[0]
            values = self.params_tree.item(item, "values")
            representative = values[1]  # Основной параметр
            current_similar = values[2]  # Похожие параметры
            
            if representative not in self.param_merge_settings:
                messagebox.showerror("Ошибка", "Группа не найдена в настройках.")
                return
            
            # Получаем список похожих параметров
            similar_list = self.param_merge_settings[representative].get('similar_params', [])
            
            if not similar_list:
                messagebox.showinfo("Информация", "В группе нет параметров для удаления.")
                return
            
            # Открываем диалог выбора параметров для удаления
            self.open_remove_params_dialog(representative, similar_list)
            
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка при удалении из группы: {e}")

    def delete_param_group(self):
        """Удалить всю группу параметров"""
        try:
            selection = self.params_tree.selection()
            if not selection:
                messagebox.showwarning("Предупреждение", "Выберите группу для удаления.")
                return
            
            item = selection[0]
            values = self.params_tree.item(item, "values")
            representative = values[1]  # Основной параметр
            
            if representative not in self.param_merge_settings:
                messagebox.showerror("Ошибка", "Группа не найдена в настройках.")
                return
            
            # Подтверждение удаления
            result = messagebox.askyesno(
                "Подтверждение", 
                f"Удалить группу '{representative}' полностью?\n\nЭто действие нельзя отменить."
            )
            
            if result:
                del self.param_merge_settings[representative]
                self.save_merge_settings()
                self.update_params_tree()
                messagebox.showinfo("Успех", f"Группа '{representative}' удалена.")
            
        except Exception as e:
            messagebox.showerror("Ошибка", f"Ошибка при удалении группы: {e}")

    def open_group_edit_dialog(self, representative, current_similar):
        """Открыть диалог редактирования группы"""
        dialog = Toplevel(self.root)
        dialog.title(f"Редактирование группы: {representative}")
        dialog.geometry("600x400")
        dialog.transient(self.root)
        dialog.grab_set()
        
        # Центрируем диалог
        dialog.update_idletasks()
        x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2)
        y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2)
        dialog.geometry(f"+{x}+{y}")
        
        # Заголовок
        Label(dialog, text=f"Основной параметр: {representative}", 
              font=("Arial", 12, "bold")).pack(pady=10)
        
        # Текущие похожие параметры
        Label(dialog, text="Похожие параметры (по одному на строку):").pack(anchor=W, padx=10)
        
        text_frame = Frame(dialog)
        text_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        text_widget = Text(text_frame, height=15, width=70)
        scrollbar = Scrollbar(text_frame, orient=VERTICAL, command=text_widget.yview)
        text_widget.configure(yscrollcommand=scrollbar.set)
        
        text_widget.pack(side=LEFT, fill=BOTH, expand=True)
        scrollbar.pack(side=RIGHT, fill=Y)
        
        # Заполняем текущими параметрами
        current_params = self.param_merge_settings[representative].get('similar_params', [])
        text_widget.insert(END, '\n'.join(current_params))
        
        # Кнопки
        button_frame = Frame(dialog)
        button_frame.pack(fill=X, padx=10, pady=10)
        
        def save_changes():
            try:
                # Получаем новый список параметров
                content = text_widget.get(1.0, END).strip()
                new_params = [p.strip() for p in content.split('\n') if p.strip()]
                
                # Удаляем основной параметр, если он случайно попал в список
                new_params = [p for p in new_params if p != representative]
                
                if not new_params:
                    # Если список пуст, удаляем всю группу
                    if messagebox.askyesno("Подтверждение", 
                                         "Список похожих параметров пуст.\nУдалить всю группу?"):
                        del self.param_merge_settings[representative]
                        self.save_merge_settings()
                        self.update_params_tree()
                        dialog.destroy()
                        messagebox.showinfo("Успех", "Группа удалена.")
                    return
                
                # Сохраняем изменения
                self.param_merge_settings[representative]['similar_params'] = new_params
                self.save_merge_settings()
                self.update_params_tree()
                dialog.destroy()
                messagebox.showinfo("Успех", "Группа обновлена.")
                
            except Exception as e:
                messagebox.showerror("Ошибка", f"Ошибка при сохранении: {e}")
        
        def cancel_changes():
            dialog.destroy()
        
        Button(button_frame, text="💾 Сохранить", command=save_changes).pack(side=LEFT, padx=5)
        Button(button_frame, text="❌ Отмена", command=cancel_changes).pack(side=LEFT, padx=5)
        
        # Подсказка
        Label(dialog, text="💡 Совет: Удалите ненужные параметры или добавьте новые", 
              font=("Arial", 9), fg="gray").pack(pady=5)

    def open_remove_params_dialog(self, representative, similar_list):
        """Открыть диалог выбора параметров для удаления"""
        dialog = Toplevel(self.root)
        dialog.title(f"Удаление из группы: {representative}")
        dialog.geometry("500x300")
        dialog.transient(self.root)
        dialog.grab_set()
        
        # Центрируем диалог
        dialog.update_idletasks()
        x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2)
        y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2)
        dialog.geometry(f"+{x}+{y}")
        
        Label(dialog, text=f"Выберите параметры для удаления из группы '{representative}':", 
              font=("Arial", 11, "bold")).pack(pady=10)
        
        # Список с чекбоксами
        frame = Frame(dialog)
        frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        canvas = Canvas(frame)
        scrollbar = Scrollbar(frame, orient="vertical", command=canvas.yview)
        scrollable_frame = Frame(canvas)
        
        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # Переменные для чекбоксов
        checkbox_vars = {}
        for param in similar_list:
            var = BooleanVar()
            checkbox_vars[param] = var
            Checkbutton(scrollable_frame, text=param, variable=var).pack(anchor=W, padx=5, pady=2)
        
        # Кнопки
        button_frame = Frame(dialog)
        button_frame.pack(fill=X, padx=10, pady=10)
        
        def remove_selected():
            try:
                # Получаем выбранные для удаления параметры
                to_remove = [param for param, var in checkbox_vars.items() if var.get()]
                
                if not to_remove:
                    messagebox.showwarning("Предупреждение", "Выберите параметры для удаления.")
                    return
                
                # Подтверждение
                result = messagebox.askyesno(
                    "Подтверждение", 
                    f"Удалить {len(to_remove)} параметр(ов) из группы?\n\n" + 
                    '\n'.join(to_remove)
                )
                
                if result:
                    # Обновляем список
                    new_similar_list = [p for p in similar_list if p not in to_remove]
                    
                    if not new_similar_list:
                        # Если список стал пустым, удаляем всю группу
                        if messagebox.askyesno("Подтверждение", 
                                             "Список похожих параметров стал пустым.\nУдалить всю группу?"):
                            del self.param_merge_settings[representative]
                        dialog.destroy()
                    else:
                        # Сохраняем обновленный список
                        self.param_merge_settings[representative]['similar_params'] = new_similar_list
                        dialog.destroy()
                    
                    self.save_merge_settings()
                    self.update_params_tree()
                    messagebox.showinfo("Успех", f"Удалено {len(to_remove)} параметр(ов).")
                
            except Exception as e:
                messagebox.showerror("Ошибка", f"Ошибка при удалении: {e}")
        
        def cancel_removal():
            dialog.destroy()
        
        Button(button_frame, text="🗑️ Удалить выбранные", command=remove_selected).pack(side=LEFT, padx=5)
        Button(button_frame, text="❌ Отмена", command=cancel_removal).pack(side=LEFT, padx=5)

    def open_exclusions_dialog(self):
        """Открывает диалог управления исключениями"""
        dialog = Toplevel(self.root)
        dialog.title("Управление исключениями")
        dialog.geometry("500x400")
        dialog.transient(self.root)
        dialog.grab_set()
        
        # Центрируем диалог
        dialog.geometry("+%d+%d" % (self.root.winfo_rootx() + 100, self.root.winfo_rooty() + 100))
        
        Label(dialog, text="Управление исключениями", font=("Arial", 12, "bold")).pack(pady=10)
        Label(dialog, text="Параметры в списке исключений не будут разделяться на значения", 
              font=("Arial", 9)).pack(pady=(0, 10))
        
        # Фрейм со списком
        list_frame = Frame(dialog)
        list_frame.pack(fill=BOTH, expand=True, padx=10, pady=5)
        
        # Список исключений
        self.exclusions_listbox = Listbox(list_frame)
        self.exclusions_listbox.pack(side=LEFT, fill=BOTH, expand=True)
        
        scrollbar = Scrollbar(list_frame, orient=VERTICAL, command=self.exclusions_listbox.yview)
        scrollbar.pack(side=RIGHT, fill=Y)
        self.exclusions_listbox.configure(yscrollcommand=scrollbar.set)
        
        # Обновляем список
        for exclusion in self.exclusions:
            self.exclusions_listbox.insert(END, exclusion)
        
        # Кнопки управления
        btn_frame = Frame(dialog)
        btn_frame.pack(fill=X, padx=10, pady=10)
        
        Button(btn_frame, text="➕ Добавить", command=lambda: self.add_exclusion_dialog(dialog)).pack(side=LEFT, padx=5)
        Button(btn_frame, text="🗑️ Удалить", command=lambda: self.delete_exclusion_dialog(dialog)).pack(side=LEFT, padx=5)
        Button(btn_frame, text="✅ Закрыть", command=dialog.destroy).pack(side=RIGHT, padx=5)

    def add_exclusion_dialog(self, parent):
        """Добавляет новое исключение"""
        new_exclusion = simpledialog.askstring("Добавить исключение", 
                                              "Введите название параметра для исключения:", 
                                              parent=parent)
        if new_exclusion and new_exclusion.strip():
            new_exclusion = new_exclusion.strip()
            if new_exclusion not in self.exclusions:
                self.exclusions.append(new_exclusion)
                self.exclusions_listbox.insert(END, new_exclusion)
                self.save_excl()

    def delete_exclusion_dialog(self, parent):
        """Удаляет выбранное исключение"""
        selection = self.exclusions_listbox.curselection()
        if selection:
            index = selection[0]
            exclusion = self.exclusions_listbox.get(index)
            if messagebox.askyesno("Подтверждение", 
                                 f"Удалить исключение '{exclusion}'?", 
                                 parent=parent):
                self.exclusions.remove(exclusion)
                self.exclusions_listbox.delete(index)
                self.save_excl()

    def show_about_dialog(self):
        """Показывает информацию о программе"""
        translation_status = "✅ Активна" if self.translation_available else "❌ Недоступна"
        
        about_text = f"""Редактор атрибутов товаров v4.5 + Переводы

🎯 Компактная эргономичная версия
🌐 Система автоматического перевода: {translation_status}

Основные возможности:
• Парсинг HTML-характеристик из Excel
• Автоматическое разделение значений
• Маппинг параметров
• Правила замен и нормализации
• Управление синонимами значений
• Защищенные значения
• 🌐 Автоматический перевод EN→RU
• 🎯 Интеллектуальный поиск переводов
• 📚 Пользовательские словари переводов
• Экспорт результатов

Версия: 4.5 + Translation System
Дата: 2025-06-02
Автор: AI Assistant

© 2025 - Инструмент для обработки товарных атрибутов"""
        
        messagebox.showinfo("О программе", about_text)

    # 🌐 МЕТОДЫ ДЛЯ РАБОТЫ С ПЕРЕВОДАМИ

    def open_translation_dialog(self):
        """Открывает функциональное меню переводов"""
        if not self.translation_available:
            messagebox.showwarning("Переводы недоступны", 
                                 "Система переводов не загружена. Проверьте файлы переводов.")
            return
        
        # Создаем функциональное меню переводов
        menu = Menu(self.root, tearoff=0)
        
        # Основные функции переводов
        menu.add_command(label="🌐 Перевести параметры", 
                        command=self.translate_current_parameters)
        menu.add_command(label="🔤 Перевести значения", 
                        command=self.translate_current_values)
        menu.add_separator()
        
        # Управление переводами
        menu.add_command(label="📥 Импорт переводов", 
                        command=self.import_translations)
        menu.add_command(label="📤 Экспорт переводов", 
                        command=self.export_translations)
        menu.add_separator()
        
        # Настройки
        menu.add_command(label="⚙️ Настройки переводов", 
                        command=self.show_translation_settings)
        menu.add_command(label="ℹ️ О системе переводов", 
                        command=self.show_translation_info)
        
        # Показываем меню рядом с кнопкой
        try:
            x = self.btn_translate.winfo_rootx()
            y = self.btn_translate.winfo_rooty() + self.btn_translate.winfo_height()
            menu.post(x, y)
        except Exception as e:
            print(f"Ошибка отображения меню: {e}")
            # Fallback - показываем в центре окна
            menu.post(self.root.winfo_x() + 100, self.root.winfo_y() + 100)

    def show_translation_settings(self):
        """Показывает настройки системы переводов"""
        if not self.translation_available:
            messagebox.showwarning("Переводы недоступны", "Система переводов не загружена.")
            return
        
        # 🔧 ИСПРАВЛЕНО: Проверяем тип системы переводов
        if TRANSLATION_SYSTEM_TYPE in ["advanced", "basic_integrated"]:
            try:
                # Пробуем открыть расширенный диалог настроек
                dialog = TranslationManagementDialog(self.root, self.translation_system)
                return
            except Exception as e:
                print(f"Ошибка открытия расширенного диалога: {e}")
        
        # Для встроенной системы или при ошибке - показываем простые настройки
        self.show_simple_translation_settings()

    def show_simple_translation_settings(self):
        """Простые настройки переводов"""
        settings_window = Toplevel(self.root)
        settings_window.title("Настройки переводов")
        settings_window.geometry("400x300")
        settings_window.transient(self.root)
        settings_window.grab_set()
        
        # Настройки автоперевода
        auto_frame = LabelFrame(settings_window, text="Автоперевод", padx=10, pady=10)
        auto_frame.pack(fill=X, padx=10, pady=10)
        
        auto_var = BooleanVar()
        if hasattr(self.translation_system, 'auto_translate'):
            auto_var.set(self.translation_system.auto_translate)
        else:
            auto_var.set(True)
            
        auto_check = Checkbutton(auto_frame, text="Включить автоперевод", variable=auto_var)
        auto_check.pack(anchor=W)
        
        # Кнопки
        btn_frame = Frame(settings_window)
        btn_frame.pack(fill=X, padx=10, pady=10)
        
        def save_settings():
            if hasattr(self.translation_system, 'auto_translate'):
                self.translation_system.auto_translate = auto_var.get()
            settings_window.destroy()
            # Обновляем основную таблицу с новыми настройками
            self.update_main_tree_with_mappings()
        
        Button(btn_frame, text="Сохранить", command=save_settings).pack(side=RIGHT, padx=5)
        Button(btn_frame, text="Отмена", command=settings_window.destroy).pack(side=RIGHT)

    def show_translation_info(self):
        """Показывает информацию о системе переводов"""
        if not self.translation_available:
            messagebox.showwarning("Переводы недоступны", "Система переводов не загружена.")
            return
        
        # Собираем информацию о системе
        info_lines = [
            "🌐 СИСТЕМА ПЕРЕВОДОВ",
            "",
            f"📚 Доступно переводов: {len(getattr(self.translation_system, 'translation_dict', {}))}"
        ]
        
        if hasattr(self.translation_system, 'auto_translate'):
            status = "Включен" if self.translation_system.auto_translate else "Отключен"
            info_lines.append(f"🎯 Автоперевод: {status}")
        
        if hasattr(self.translation_system, 'fuzzy_match_threshold'):
            info_lines.append(f"🔍 Нечеткий поиск: {self.translation_system.fuzzy_match_threshold}%")
        
        info_lines.extend([
            "",
            "📋 Статистика сопоставлений:",
            f"• Всего сопоставлений: {len(self.mappings)}",
            f"• Обработано строк: {len(self.tree.get_children()) if hasattr(self, 'tree') else 0}"
        ])
        
        messagebox.showinfo("О системе переводов", "\n".join(info_lines))

    def translate_current_parameters(self):
        """Переводит текущие параметры в маппингах"""
        if not self.translation_available or not self.translation_system:
            messagebox.showwarning("Переводы недоступны", "Система переводов не загружена.")
            return
        
        if not self.mappings:
            messagebox.showinfo("Информация", "Нет параметров для перевода. Сначала обработайте таблицу.")
            return
        
        translated_count = 0
        for original_param in list(self.mappings.keys()):
            translated = self.translation_system.translate_text(original_param, is_parameter=True)
            if translated != original_param:
                # Заменяем ключ в маппингах
                self.mappings[translated] = self.mappings.pop(original_param)
                translated_count += 1
        
        if translated_count > 0:
            self.refresh_mapping_table()
            # 🔧 ИСПРАВЛЕНО: Автоматически обновляем основную таблицу
            self.update_main_tree_with_mappings()
            messagebox.showinfo("Перевод завершен", f"Переведено параметров: {translated_count}")
        else:
            messagebox.showinfo("Перевод завершен", "Новых переводов не найдено.")

    def translate_current_values(self):
        """Переводит текущие значения в маппингах"""
        if not self.translation_available or not self.translation_system:
            messagebox.showwarning("Переводы недоступны", "Система переводов не загружена.")
            return
        
        if not self.mappings:
            messagebox.showinfo("Информация", "Нет значений для перевода. Сначала обработайте таблицу.")
            return
        
        translated_count = 0
        for param, values in self.mappings.items():
            if isinstance(values, list):
                new_values = []
                for value in values:
                    translated = self.translation_system.translate_text(str(value), is_parameter=False)
                    new_values.append(translated)
                    if translated != str(value):
                        translated_count += 1
                self.mappings[param] = new_values
        
        if translated_count > 0:
            self.refresh_mapping_table()
            # 🔧 ИСПРАВЛЕНО: Автоматически обновляем основную таблицу
            self.update_main_tree_with_mappings()
            messagebox.showinfo("Перевод завершен", f"Переведено значений: {translated_count}")
        else:
            messagebox.showinfo("Перевод завершен", "Новых переводов не найдено.")

    def export_translations(self):
        """Экспортирует пользовательские переводы"""
        if not self.translation_available or not self.translation_system:
            messagebox.showwarning("Переводы недоступны", "Система переводов не загружена.")
            return
        
        filename = filedialog.asksaveasfilename(
            title="Экспорт переводов",
            defaultextension=".json",
            filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
        )
        
        if filename:
            try:
                export_data = {
                    "custom_translations": getattr(self.translation_system, 'custom_translations', {}),
                    "settings": {
                        "auto_translate": getattr(self.translation_system, 'auto_translate', True),
                        "fuzzy_match_threshold": getattr(self.translation_system, 'fuzzy_match_threshold', 80),
                        "translate_values": getattr(self.translation_system, 'translate_values', True),
                        "translate_parameters": getattr(self.translation_system, 'translate_parameters', True)
                    }
                }
                
                with open(filename, 'w', encoding='utf-8') as f:
                    json.dump(export_data, f, ensure_ascii=False, indent=2)
                
                messagebox.showinfo("Экспорт завершен", f"Переводы сохранены в {filename}")
            except Exception as e:
                messagebox.showerror("Ошибка экспорта", f"Не удалось сохранить переводы: {e}")

    def import_translations(self):
        """Импортирует пользовательские переводы"""
        if not self.translation_available or not self.translation_system:
            messagebox.showwarning("Переводы недоступны", "Система переводов не загружена.")
            return
        
        filename = filedialog.askopenfilename(
            title="Импорт переводов",
            filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
        )
        
        if filename:
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    import_data = json.load(f)
                
                # Импортируем пользовательские переводы
                if "custom_translations" in import_data:
                    if hasattr(self.translation_system, 'custom_translations'):
                        self.translation_system.custom_translations.update(import_data["custom_translations"])
                        if hasattr(self.translation_system, 'save_custom_translations'):
                            self.translation_system.save_custom_translations()
                
                # Импортируем настройки
                if "settings" in import_data:
                    settings = import_data["settings"]
                    if hasattr(self.translation_system, 'auto_translate'):
                        self.translation_system.auto_translate = settings.get("auto_translate", True)
                    if hasattr(self.translation_system, 'fuzzy_match_threshold'):
                        self.translation_system.fuzzy_match_threshold = settings.get("fuzzy_match_threshold", 80)
                    if hasattr(self.translation_system, 'translate_values'):
                        self.translation_system.translate_values = settings.get("translate_values", True)
                    if hasattr(self.translation_system, 'translate_parameters'):
                        self.translation_system.translate_parameters = settings.get("translate_parameters", True)
                
                messagebox.showinfo("Импорт завершен", f"Переводы загружены из {filename}")
            except Exception as e:
                messagebox.showerror("Ошибка импорта", f"Не удалось загрузить переводы: {e}")

    def apply_auto_translation(self, text, is_parameter=True):
        """Применяет автоматический перевод к тексту, если включено"""
        if not self.can_auto_translate():
            return text
            
        # Пробуем разные методы перевода в зависимости от системы
        if hasattr(self.translation_system, 'translate_text'):
            translated = self.translation_system.translate_text(text, is_parameter=is_parameter)
            # Показываем только если перевод действительно произошел
            if translated != text:
                print(f"🌐 Перевод: '{text}' → '{translated}'")
            return translated
        elif hasattr(self.translation_system, 'basic_translate'):
            translated = self.translation_system.basic_translate(text)
            if translated != text:
                print(f"🌐 Перевод: '{text}' → '{translated}'")
            return translated
            
        return text

    def can_auto_translate(self):
        """Проверяет, доступен ли автоперевод в текущей системе переводов"""
        if not self.translation_available or not self.translation_system:
            return False
        
        # Для встроенной системы проверяем атрибут auto_translate
        if hasattr(self.translation_system, 'auto_translate'):
            return self.translation_system.auto_translate
        
        # Для IntegratedTranslationSystem считаем, что автоперевод всегда доступен
        return True

    def show_hotkeys_dialog(self):
        """Показывает список горячих клавиш"""
        hotkeys_text = """Горячие клавиши:

📁 Файлы:
Ctrl+O - Загрузить файл
Ctrl+P - Обработать таблицу  
Ctrl+S - Сохранить результат
Ctrl+Q - Выход

⌨️ Управление:
Ctrl+C - Копировать
Ctrl+V - Вставить
Ctrl+Z - Отмена
Ctrl+A - Выбрать все
Delete - Удалить выбранное

🌐 Переводы:
Меню "🌐 Переводы" → "⚙️ Настройки переводов"
Меню "🌐 Переводы" → "🔄 Перевести параметры"
Меню "🌐 Переводы" → "🔄 Перевести значения"
Кнопка "🌐 Переводы" на панели инструментов

🖱️ Интерфейс:
ПКМ - Контекстное меню
F1 - Справка
Esc - Закрыть диалог"""
        
        messagebox.showinfo("Горячие клавиши", hotkeys_text)


if __name__ == "__main__":
    root = Tk()
    app = AttributeEditor(root)
    root.mainloop()

Напиши по пунктам, что должно делать мое приложение. Вот пример:
Автоматическое обновление "Результат разделения" при изменении синонимов
Динамические обновления при изменениях в "Объединение параметров" 
Автоматические обновления при изменениях в "Переводы значений"
Динамическое обновление при изменениях в "Правила замен"
Обновление "Новый параметр" при изменении "Таблица сопоставлений"
Система обратных вызовов (callbacks) для связи между вкладками
Мгновенное отражение всех изменений без ручного обновления
ДИНАМИЧЕСКОЕ обновление основной таблицы при изменении правил перевода
ЗАЩИТА известных аббревиатур (H.264, G.711, IP67) от неправильного перевода  
МАССОВОЕ управление ТОЛЬКО выбранными правилами перевода
Счетчик выбранных элементов в реальном времени
ИСПРАВЛЕНО сохранение переводов в скобках "Auto (АТО) / Color (Цв.)"
ИСПРАВЛЕН сбор ВСЕХ значений из ВСЕХ выбранных параметров
ГАРАНТИРОВАННОЕ сохранение ВСЕХ уникальных значений
ПРИМЕНЕНИЕ переводов ко всем собранным значениям
Корректное объединение значений в столбце "Результат разделения"
Правильное применение синонимов из настроек
Удаление некорректных значений типа "/;"
Динамическое обновление при изменении синонимов
Сохранение всех уникальных значений без потерь
Полнофункциональный GUI редактор переводов вместо простого информационного окна
Управление множественными словарями (основной, пользовательские, специализированные)
Система категоризации переводов (16 категорий)
Мгновенный поиск и фильтрация переводов
Импорт/экспорт словарей (JSON, CSV)
Статистика использования переводов
Гибкие настройки системы переводов
Автодополнение при вводе
Автоматическое резервное копирование
Поддержка машинного перевода (опционально)
Автоматический перевод параметров и значений EN→RU
Интеллектуальный поиск переводов с нечетким соответствием
Пользовательские словари переводов
Настройки системы переводов
Компактный эргономичный интерфейс
Логическое группирование элементов в блоки
Панель инструментов с иконками
Перенос редко используемых функций в меню
Цветовые индикаторы и эмодзи для навигации
Улучшенная читаемость и удобство использования
Диалог справки "📚 Справка" с подробной инструкцией (F1)
Корректная работа кнопки выбора столбцов
Сохранение выбранных столбцов между сессиями
Правильная загрузка сохраненных настроек столбцов
Кнопки "Выбрать все" и "Снять все" в диалоге
Визуальная индикация количества выбранных столбцов
Правильная обработка выбранных столбцов при экспорте
Корректное объединение параметров без потери данных
Правильная дедупликация значений
Сохранение всех уникальных значений при объединении
Детальная отладка процесса объединения
Настройки синонимов для объединения значений
GUI для управления синонимами значений
корректное применение синонимов
поиск синонимов без учета регистра
Добавлена вкладка "Переводы значений" с полным управлением правилами
Таблица правил перевода значений и единиц измерения (mm→мм, kg→кг, etc.)
Автоматическое обнаружение единиц измерения при загрузке исходного файла
Импорт/экспорт правил перевода в JSON формате
Интеграция правил в процесс обработки значений
ASSISTANT
Ваше приложение — это мощный и многофункциональный инструмент для обработки, нормализации, перевода и объединения атрибутов товаров из Excel-файлов с HTML-характеристиками. Ниже приведён подробный список того, что оно должно делать, основанный на представленном коде:

---

### Основные функции и возможности приложения:

1. **Загрузка исходного Excel файла с HTML-характеристиками товаров**  
   - Поддержка форматов `.xlsx`, `.xls`  
   - Выбор столбца с HTML-характеристиками (по номеру столбца)

2. **Анализ и парсинг HTML таблиц в ячейках**  
   - Поддержка сложной многоколоночной структуры HTML  
   - Извлечение параметров и их значений без раннего перевода

3. **Обработка и очистка значений параметров**  
   - Удаление дубликатов  
   - Очистка мусорных и некорректных значений  
   - Поддержка ручной настройки разделителей для каждого параметра  
   - Использование защищённых значений (которых разделитель не должен разделять) — например, “B/W”, “I/O” и др.

4. **Динамическое (мгновенное) обновление основного результата (“Результат разделения”) при изменениях, включая:**  
   - Изменения в настройках синонимов значений  
   - Изменения в правилах замены (регулярные выражения)  
   - Изменения в таблице сопоставлений (маппингах) параметров  
   - Изменения в правилах перевода значений

5. **Система синонимов значений с поддержкой обратных вызовов (callbacks)**  
   - Добавление/удаление синонимов с мгновенным обновлением результатов  
   - Автоматическая замена синонимов на предпочтительные значения при обработке

6. **Система правил замены текста на основе регулярных выражений**  
   - Редактор правил с поддержкой импорта/экспорта  
   - Тестирование правил на произвольных строках  
   - Активирование/деактивирование отдельных правил  
   - Сохранение и загрузка правил из JSON

7. **Поддержка нормализации регистра строк**  
   - Пользовательские настройки предпочтительного регистра для значений  
   - Применение нормализации регистра к значениям параметров

8. **Расширенная система переводов (переключаемая между расширенной, интегрированной и встроенной базовой версией)**  
   - Автоматический перевод параметров и значений с английского на русский  
   - Защита известных аббревиатур (H.264, G.711, IP67 и др.) от неправильного перевода  
   - Категоризация правил перевода (16 категорий)  
   - Поддержка нечеткого поиска и интеллектуального сопоставления  
   - Импорт/экспорт пользовательских словарей и правил перевода  
   - GUI для управления правилами и параметрами перевода  
   - Автоматическое применение переводов при обработке данных  
   - Счётчик и управление состоянием активных правил в реальном времени  

9. **Автоматический анализ и добавление правил перевода единиц измерения, включая IP-видеонаблюдение**  
   - Автоматическое обнаружение новых единиц измерения в данных  
   - Интеграция с расширенной базой единиц IP-видеонаблюдения  
   - Создание и добавление правил перевода для новых найденных единиц  
   - Нормализация пробелов и формата значений с единицами

10. **Цветовое выделение и группировка похожих параметров и значений**  
    - На вкладке “Объединение параметров”  
    - Автоматический поиск и сгруппирование похожих параметров (с расширенной логикой исключения ложных совпадений)  
    - Цветовое выделение групп в основной таблице  
    - Управление группами: редактирование, удаление отдельных параметров из групп, полное удаление группы  
    - Выбор основных параметров и параметров для объединения  
    - Аналогичная функциональность для похожих значений внутри параметров с возможностью их объединения

11. **Полнофункциональный диалог и редактор правил перевода значений**  
    - Добавление/редактирование/удаление правил  
    - Удобный интерфейс с примерами и описаниями  
    - Массовые операции с выбранными правилами (активировать/деактивировать)  
    - Импорт/экспорт правил перевода в JSON формат

12. **Комфортный и эргономичный GUI**  
    - Наличие всплывающих подсказок (ToolTips) для элементов интерфейса  
    - Перенос редко используемых функций в меню  
    - Контекстное меню с функциями копирования, объединения параметров и значений  
    - Горячие клавиши для основных операций  
    - Вкладки с разделениями по функционалу: основная таблица, объединение параметров, переводы значений и др.  
    - Отдельные диалоги для управления синонимами, нормализацией регистра, правилами замены и защищёнными значениями  
    - Отображение статистики количества правил, активных правил и выбранных элементов  
    - Автоматическое сохранение настроек и состояний между сессиями

13. **Импорт/экспорт mаппингов параметров**  
    - Загрузка/сохранение из JSON  
    - Быстрая фильтрация и поиск по таблице сопоставлений  
    - Динамическое применение сопоставлений к параметрам в основной таблице

14. **Расширенные инструменты для работы с данными**  
    - Умное разделение значений с поддержкой множественных разделителей  
    - Восстановление защищённых значений после разделения  
    - Автоматическая дедупликация значений  
    - Нормализация формата значений (пробелы, аббревиатуры, единицы измерения)  
    - Правильное объединение параметров и их значений с гарантированным сохранением уникальных данных и корректным применением переводов и синонимов

15. **Подробная справочная система с навигацией и форматированием**  
    - Встроенный текстовый диалог с инструкцией пользователя  
    - Быстрая навигация по разделам  
    - Описание функционала и примеры использования  
    - Информация о горячих клавишах и основных операциях

16. **Логирование и отладка**  
    - Создание лог-файлов при сохранении результата  
    - Детальный вывод отладочной информации для проблемных товаров  
    - Вывод в консоль подробной информации о внутренних процессах обработки

17. **Защита от ошибок и стабильность**  
    - Обработка исключений при работе с файлами, JSON, регулярными выражениями  
    - Предотвращение применения невалидных правил  
    - Проверка и автоматическая замена пустых разделителей на стандартные  
    - Обработка нестандартных и сложных случаев, включая сложные HTML структуры и неправильные пользовательские данные

---

### Кратко по пунктам (фактический перечень):

- Загрузка Excel файла (с выбором столбцов и дополнительных столбцов)  
- Парсинг HTML в ячейках с корректной обработкой многоколоночной таблицы  
- Автоматическое и ручное разделение значений с поддержкой нескольких разделителей  
- Защищённые значения: сохраняются целиком, не разделяются  
- Удаление дубликатов и мусорных значений в значениях параметров  
- Нормализация регистра и формата значений  
- Система замены значений по правилам с регулярными выражениями  
- GUI для управления правилами замены с импортом/экспортом  
- Менеджер синонимов значений с callback-уведомлениями и UI для управления  
- Автоматический и ручной перевод терминов (полнофункциональная, интегрированная или базовая система перевода)  
- Интеллектуальные функции поиска похожих параметров с расширенной логикой — защита от ложных совпадений  
- Группировка и цветовое выделение похожих параметров  
- Объединение параметров и значений с сохранением уникальности и правильным применением переводов  
- Хранение и восстановление настроек, правил, синонимов, защищённых значений, исключений и пр.  
- Массовые операции (выбор всё/снять выбор), фильтрация, сортировка в таблицах  
- Мгновенное динамическое обновление основной таблицы при любых изменениях в правилах, синонимах, сопоставлениях и переводах  
- Поддержка расширенной базы единиц для IP-видеонаблюдения с интеграцией в систему переводов  
- Автоматический анализ данных для добавления новых правил перевода единиц измерения  
- Логирование и отладочная информация для диагностики проблем  
- Удобные диалоги и справка с навигацией и форматированием  
- Контекстные меню и горячие клавиши для быстрого управления  

---

Если нужны пояснения по конкретным модулям/функциям — могу детализировать!

turns-00084.parquet:32597

99c85e40d52625c1d6bdb2cb
turn 1/1gpt-4.1-mini-2025-04-14EnglishHong Kong1869 words
degenerate_repetitionAbsentFinal dense release
USER
I am developing the online web shop with CodeIgniter 4 framework and making the redeem system to allow the user to use the redeem code.
I have complete the database structure as the following:
external_promo_code, for store the root data of the redeem product:
 	# 	名稱 	類型 	編碼與排序 	屬性 	空值(Null) 	預設值 	備註 	額外資訊 	動作
	1 	id 主鍵 	int(11) 			否 	無 		AUTO_INCREMENT 	修改 修改 	刪除 刪除 	
	2 	pid 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	(product_id)
	3 	startDate 	datetime 			否 	無 			修改 修改 	刪除 刪除 	
	4 	endDate 	datetime 			是 	NULL 			修改 修改 	刪除 刪除 	
	5 	period 	int(11) 			是 	NULL 			修改 修改 	刪除 刪除 	
	6 	repeatTime 	int(11) 			是 	NULL 			修改 修改 	刪除 刪除 	
	7 	created_at 	datetime 			是 	NULL 			修改 修改 	刪除 刪除 	
	8 	updated_at 	datetime 			是 	NULL 			修改 修改 	刪除 刪除 	

external_promo_redeem, for store the distrubuted redeem code (the code belong to the user):
 	# 	名稱 	類型 	編碼與排序 	屬性 	空值(Null) 	預設值 	備註 	額外資訊 	動作
	1 	id 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	
	2 	proId 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	(id from external_promo_code)
	3 	userId 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	(purchased user id)
	4 	starttime 	datetime 			否 	無 			修改 修改 	刪除 刪除 	
	5 	endtime 	datetime 			否 	無 			修改 修改 	刪除 刪除 	
	6 	repeatTime 	int(11) 			是 	NULL 			修改 修改 	刪除 刪除 	(0=unlimited)
	7 	distrubuteDate 	datetime 			否 	無 			修改 修改 	刪除 刪除 	
	8 	status 	int(11) 			否 	1 			修改 修改 	刪除 刪除 	

external_promo_redeem_records, for store the redeemed code record:
 	# 	名稱 	類型 	編碼與排序 	屬性 	空值(Null) 	預設值 	備註 	額外資訊 	動作
	1 	id 主鍵 	int(11) 			否 	無 		AUTO_INCREMENT 	修改 修改 	刪除 刪除 	
	2 	proId 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	
	3 	redeemId 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	(id from external_promo_redeem)
	4 	userId 	int(11) 			否 	無 			修改 修改 	刪除 刪除 	
	5 	dateTime 	datetime 			否 	無 			修改 修改 	刪除 刪除 	
	6 	expiredDate 	datetime 			否 	無 			修改 修改 	刪除 刪除 	
	7 	checkcode 	text 	utf8mb4_unicode_ci 		否 	無 			修改 修改 	刪除 刪除 	
	8 	status 	int(11) 			否 	1 	1=available 0=used 		修改 修改 	刪除 刪除 	
	9 	usedDate 	datetime 			是 	current_timestamp() 			修改 修改 	刪除 刪除 	
	10 	verifiedSellerID 	int(11) 			是 	NULL 			修改 修改 	刪除 刪除 	


The flow for the redeem code:
1. The seller will setup the product with the type of redeem and related setting (i.e. repeatTIme, startDate, endDate, period) in the management panel, the redeem data will be store in the external_promo_code as the root data.
2. the user can buy the product to get the redeem code, the related purchased redeem code will be store in external_promo_redeem, and will be show in the front-end page to let the user to check and use the redeem
3. the user can use the redeem in the front-end page, the record with checkcode will be generated and stored to external_promo_redeem_records, user is required to use the code within 15 mins, otherwise will be expired and the user need to use the redeem to regenerate the code again.
4. user can browse the available generated code again by redeem the same code until it become expired. User will see the page with the details of redeem (such as the name, seller name), the QRCode with the checkcode data, text of checkcode, and the countdown timer for expire
5. if the used time (the count of generated code with status 0 of the same user) of redeem exceed the repeat time set in external_promo_code, the user cannot use the redeem and should be hidden in the front-end page

Can you first design the front-end page to list all the avaliable redeem with the dummy data, and the redeem page to fulfill the flow step no.4?
ASSISTANT
Sure! Below I'll provide a simple front-end mock-up using plain HTML, CSS, and JavaScript to:

1. **List all available redeem codes** (step 5 condition applied: hide codes when usage exceeds `repeatTime` limit).
2. **Show the redeem details page with the QR code, checkcode text, seller & product info, and countdown timer for expiration** (step 4).

---

### Assumptions for dummy data:

- Each `redeem` available has:
  - `id`
  - `productName`
  - `sellerName`
  - `checkcode`
  - `usedCount` (how many times user already used)
  - `repeatTime` (limit, 0 means unlimited)
  - `expireAt` (for the current generated code, i.e. 15 min lifetime)
  - `codeUsed` (boolean, if current generated code is used or not)

---

## 1. Redeem List Page (`redeem_list.html`)

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Available Redeems</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; background: #f7f7f7; }
        h1 { margin-bottom: 20px; }
        .redeem-list { max-width: 600px; margin: auto; }
        .redeem-item {
            background: #fff;
            padding: 15px 20px;
            margin-bottom: 12px;
            border-radius: 6px;
            box-shadow: 0 1px 4px rgba(0,0,0,0.1);
        }
        .redeem-item h3 { margin: 0 0 8px; }
        .redeem-item button {
            margin-top: 10px;
            background-color: #0078d4;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 8px 14px;
            cursor: pointer;
        }
        .redeem-item button:disabled {
            background-color: #aaa;
            cursor: not-allowed;
        }
    </style>
</head>
<body>
    <h1>Your Available Redeems</h1>
    <div class="redeem-list" id="redeemList"></div>

    <script>
        // Dummy data (normally would be fetched from backend API)
        const redeems = [
            {
                id: 1,
                productName: "Product A Redeem",
                sellerName: "Seller A",
                userId: 123,
                usedCount: 1,
                repeatTime: 3,
                codeUsed: false,
                expireAt: new Date(Date.now() + 10 * 60 * 1000), // expire in 10 mins
                checkcode: "ABC123",
            },
            {
                id: 2,
                productName: "Product B Redeem",
                sellerName: "Seller B",
                userId: 123,
                usedCount: 3,
                repeatTime: 3,
                codeUsed: true,
                expireAt: new Date(Date.now() - 1000), // expired
                checkcode: "XYZ789",
            },
            {
                id: 3,
                productName: "Product C Redeem",
                sellerName: "Seller C",
                userId: 123,
                usedCount: 0,
                repeatTime: 0, // unlimited usage
                codeUsed: false,
                expireAt: null, // no expire for unused/uncreated code hypothetically
                checkcode: null,
            }
        ];

        const redeemListEl = document.getElementById('redeemList');

        function canUseRedeem(redeem) {
            // If repeatTime 0 => unlimited
            if (redeem.repeatTime === 0) return true;
            return redeem.usedCount < redeem.repeatTime;
        }

        function renderRedeemList() {
            redeemListEl.innerHTML = '';

            const availableRedeems = redeems.filter(canUseRedeem);

            if (availableRedeems.length === 0) {
                redeemListEl.innerHTML = `<p>You have no available redeems at the moment.</p>`;
                return;
            }

            availableRedeems.forEach(redeem => {
                // Check if codeUsed = false && expireAt is still valid => allow direct use
                // Otherwise allow generate new

                const now = new Date();
                const stillValid = redeem.codeUsed === false && redeem.expireAt && new Date(redeem.expireAt) > now;

                const buttonText = stillValid ? "View Redeem" : "Generate New Code";

                const item = document.createElement('div');
                item.className = 'redeem-item';
                item.innerHTML = `
                    <h3>${redeem.productName}</h3>
                    <p>Seller: ${redeem.sellerName}</p>
                    <p>Used Times: ${redeem.usedCount} / ${redeem.repeatTime === 0 ? 'Unlimited' : redeem.repeatTime}</p>
                    <button data-id="${redeem.id}">${buttonText}</button>
                `;
                redeemListEl.appendChild(item);

                const btn = item.querySelector('button');
                btn.onclick = () => {
                    // Navigate to redeem details page with redeem id
                    window.location.href = `redeem_detail.html?id=${redeem.id}`;
                }
            });
        }

        renderRedeemList();
    </script>
</body>
</html>
```

---

## 2. Redeem Details Page (`redeem_detail.html`)

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Redeem Detail</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; background: #fefefe; max-width: 400px; margin: auto; }
        h1 { margin-bottom: 10px; }
        .detail-box {
            border: 1px solid #ccc;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            box-shadow: 0 1px 5px rgba(0,0,0,0.1);
        }
        .product-name { font-weight: bold; font-size: 1.5em; }
        .seller-name { margin-top: 4px; color: #555; }
        .qrcode {
            margin: 20px 0;
        }
        .checkcode-text {
            padding: 8px;
            background: #eee;
            border-radius: 4px;
            font-family: monospace;
            font-size: 1.2em;
            user-select: all;
        }
        .countdown {
            margin-top: 15px;
            font-size: 1.2em;
            color: #d9534f;
            font-weight: bold;
        }
        .expired {
            color: #888;
            font-weight: normal;
        }
        button#regenerateBtn {
            margin-top: 20px;
            background-color: #0078d4;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 10px 18px;
            cursor: pointer;
            font-size: 1em;
        }
        button#regenerateBtn:disabled {
            background-color: #aaa;
            cursor: not-allowed;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/qrcode"></script> <!-- QRCode.js CDN -->
</head>
<body>

<div class="detail-box" id="detailBox">
    <h1>Redeem Detail</h1>
    <div class="product-name" id="productName">Product Name</div>
    <div class="seller-name" id="sellerName">Seller Name</div>
    <div class="qrcode" id="qrcode"></div>
    <div class="checkcode-text" id="checkcodeText">CODE123456</div>
    <div class="countdown" id="countdown"></div>
    <button id="regenerateBtn">Regenerate Code</button>
</div>

<script>
    // In real app, this data should dynamically load by redeem id (by API/backend)
    // Here we'll mock it based on query param id

    const urlParams = new URLSearchParams(window.location.search);
    const redeemId = parseInt(urlParams.get('id')) || 1;

    // Dummy data to illustrate (should be fetched from backend API)
    const dummyRedeems = {
        1: {
            productName: "Product A Redeem",
            sellerName: "Seller A",
            checkcode: "ABC123XYZ",
            usedCount: 1,
            repeatTime: 3,
            expireAt: new Date(Date.now() + 9 * 60 * 1000), // 9 mins later
            codeUsed: false,
        },
        2: {
            productName: "Product B Redeem",
            sellerName: "Seller B",
            checkcode: "EXPIRED123",
            usedCount: 3,
            repeatTime: 3,
            expireAt: new Date(Date.now() - 1000), // expired
            codeUsed: true,
        },
        3: {
            productName: "Product C Redeem",
            sellerName: "Seller C",
            checkcode: null, // no code yet generated
            usedCount: 0,
            repeatTime: 0,
            expireAt: null,
            codeUsed: false,
        }
    };

    const detailBox = document.getElementById('detailBox');
    const productNameEl = document.getElementById('productName');
    const sellerNameEl = document.getElementById('sellerName');
    const qrcodeEl = document.getElementById('qrcode');
    const checkcodeTextEl = document.getElementById('checkcodeText');
    const countdownEl = document.getElementById('countdown');
    const regenerateBtn = document.getElementById('regenerateBtn');

    let intervalId;
    let currentExpireAt;
    let currentCheckcode;
    let codeUsed;
    let usedCount;
    let repeatTime;

    function generateNewCode() {
        // Normally: call backend API to generate new checkcode, create new record, get expireAt
        // Here just generate a random string + 15 mins expire time

        currentCheckcode = Math.random().toString(36).substring(2, 10).toUpperCase();
        currentExpireAt = new Date(Date.now() + 15 * 60 * 1000); // 15 min
        codeUsed = false;
        usedCount++;
        renderPage();
    }

    function startCountdown(expireAt) {
        clearInterval(intervalId);

        function updateTimer() {
            let now = new Date();
            let diff = expireAt - now;
            if(diff <= 0) {
                clearInterval(intervalId);
                countdownEl.textContent = "Code expired!";
                countdownEl.classList.add('expired');
                regenerateBtn.disabled = false;
                codeUsed = true; // simulate expiration
                renderQRCode(null);
                checkcodeTextEl.textContent = "-";
                return;
            }
            countdownEl.textContent = `Expires in: ${Math.floor(diff/60000)}m ${Math.floor((diff%60000)/1000)}s`;
            countdownEl.classList.remove('expired');
            regenerateBtn.disabled = true;
        }

        updateTimer();
        intervalId = setInterval(updateTimer, 1000);
    }

    function renderQRCode(text) {
        qrcodeEl.innerHTML = "";
        if (!text) return;
        new QRCode(qrcodeEl, {
            text: text,
            width: 180,
            height: 180,
            colorDark : "#000000",
            colorLight : "#ffffff",
        });
    }

    function renderPage() {
        const redeem = dummyRedeems[redeemId];

        productNameEl.textContent = redeem.productName;
        sellerNameEl.textContent = redeem.sellerName;

        if (redeem.checkcode && !codeUsed) {
            checkcodeTextEl.textContent = currentCheckcode ?? redeem.checkcode;
            renderQRCode(currentCheckcode ?? redeem.checkcode);
            startCountdown(currentExpireAt ?? new Date(redeem.expireAt));
            regenerateBtn.disabled = true;
        } else {
            // No valid code exists or expired/used
            checkcodeTextEl.textContent = "-";
            renderQRCode(null);
            countdownEl.textContent = "No available code. Please regenerate.";
            countdownEl.classList.add('expired');
            regenerateBtn.disabled = false;
        }

        // Disable regenerate button if repeatTime reached
        if (redeem.repeatTime !== 0 && usedCount >= redeem.repeatTime) {
            regenerateBtn.disabled = true;
            countdownEl.textContent = "Redeem limit reached.";
            countdownEl.classList.add('expired');
        }
    }

    regenerateBtn.addEventListener('click', () => {
        const redeem = dummyRedeems[redeemId];
        if (redeem.repeatTime !== 0 && usedCount >= redeem.repeatTime) {
            alert("You have reached the maximum usage limit for this redeem.");
            return;
        }

        generateNewCode();
    });

    // Initialize
    (function init() {
        const redeem = dummyRedeems[redeemId];
        if (!redeem) {
            detailBox.innerHTML = "<p>Invalid redeem id.</p>";
            return;
        }

        currentCheckcode = redeem.checkcode;
        currentExpireAt = new Date(redeem.expireAt);
        codeUsed = redeem.codeUsed;
        usedCount = redeem.usedCount;
        repeatTime = redeem.repeatTime;

        renderPage();
    })();
</script>

</body>
</html>
```

---

### How to Use

- Save both files `redeem_list.html` and `redeem_detail.html` in your front-end directory.
- Open `redeem_list.html`, which shows the available redeems based on dummy data and conditions.
- Clicking each redeem item leads to `redeem_detail.html?id=REDEEM_ID` which shows the QR code & countdown timer for the checkcode.
- The "Regenerate Code" button simulates generating a new redeem code with 15 minutes expiration and disables itself until expiry.

---

### Final Notes for Integration

- Replace the dummy data with your actual API calls to fetch redeem info/get/generate checkcodes.
- Ensure the count, status, and expiry are validated and updated from backend accordingly.
- Use a QR code generation library you prefer on the backend or front-end (I used [davidshimjs/qrcodejs](https://github.com/davidshimjs/qrcodejs) CDN here).
- Add user authentication and protect redeem pages as needed.

If you want, I can also help you create the backend API endpoints for these flows! Just ask!

turns-00084.parquet:32598

71b669f992106d19bbd9daed
turn 1/2gpt-4.1-mini-2025-04-14EnglishPhilippines43 words
degenerate_repetitionAbsentFinal dense release
USER
please correct; Hello John,
I Agree. I’ll check per line items where admin support can be booked against. 
ASSISTANT
Sure! Here's a corrected version of your sentence:

Hello John,  
I agree. I’ll check each line item to see where admin support can be booked.

turns-00084.parquet:32599

a988aebf9dce9f757d580f5a
turn 2/2gpt-4.1-mini-2025-04-14EnglishPhilippines54 words
degenerate_repetitionAbsentFinal dense release
USER
please correct: Hello Sir Gil,
Please review the attached quotation for aft ramp. This scope the remained area for preservation left by ENN. 
ASSISTANT
Certainly! Here's a corrected version of your sentence:

Hello Sir Gil,  
Please review the attached quotation for the aft ramp. This scope covers the remaining area for preservation left by ENN.