From 638e2446d5ce6afd24a3a61a370b9ef620681a5d Mon Sep 17 00:00:00 2001 From: prog1764 Date: Wed, 5 Aug 2026 01:35:30 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=98=D0=98=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BF=D0=BE=20=D0=B3=D0=B0=D0=BB=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=20(=D0=BD=D0=B0=D1=80=D1=83=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C/=D1=83=D0=B3=D1=80=D0=BE=D0=B7=D1=8B/?= =?UTF-8?q?=D0=B8=D0=BD=D1=86=D0=B8=D0=B4=D0=B5=D0=BD=D1=82=D1=8B/=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D0=B8/=D0=BE?= =?UTF-8?q?=D0=B1=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5/?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dokogen/ui.py | 141 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 111 insertions(+), 30 deletions(-) diff --git a/dokogen/ui.py b/dokogen/ui.py index c678bcb..94aaa53 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -3043,47 +3043,128 @@ class DokoGenApp: self.log(f"❌ Проверка безопасности: {err}") return d = result['data'] - lines = [f"Источник: {result['source']}"] - lines.append(f"Оценка: {d.get('rating', 'warning')}") notes = d.get('notes') or [] - if notes: - lines.append("\nЗамечания:") - for n in notes: - lines.append(f" • {n}") props = d.get('proposals') or {} - if any(props.values()): - lines.append("\nПредложения ИИ:") - for key, label in [('attacker_category', 'Нарушитель'), - ('threats', 'Угрозы'), - ('incidents', 'Инциденты'), - ('criteria_values', 'Показатели'), - ('criteria_justification', 'Обоснование'), - ('org_measures', 'Орг. меры'), - ('tech_measures', 'Тех. меры')]: - v = props.get(key) - if v: - lines.append(f" {label}: {v}") - self._verify_log('\n'.join(lines)) + self._verify_log(f"🤖 ИИ: оценка {d.get('rating', 'warning')}, замечаний: {len(notes)}") self.log(f"🤖 ИИ: оценка {d.get('rating', 'warning')}, замечаний: {len(notes)}") - # Показываем результат отдельным окном (панель проверки на вкладке «Организация» может быть не видна) - self._show_security_ai_result(lines) + self._show_security_ai_result(notes, props, d.get('rating', 'warning'), result.get('source', 'ИИ')) - def _show_security_ai_result(self, lines): - """Отдельное окно с результатом ИИ-проверки безопасности.""" + def _show_security_ai_result(self, lines, proposals=None, rating='', source='ИИ'): + """Окно с результатом ИИ-проверки безопасности. + + Если переданы proposals — показываем каждый раздел с галочкой + и кнопкой «Применить отмеченное» (применение по галочкам, ИИ может ошибаться). + Иначе — просто текстовый результат (для остальных вызовов).""" try: + if proposals is None: + win = tk.Toplevel(self.root) + win.title("🤖 Проверка безопасности — результат ИИ") + win.geometry("760x560") + win.transient(self.root) + txt = scrolledtext.ScrolledText(win, wrap=tk.WORD, font=('', 10), state=tk.NORMAL) + txt.pack(fill=tk.BOTH, expand=True, padx=10, pady=(10, 4)) + txt.insert('1.0', '\n'.join(lines)) + txt.config(state=tk.DISABLED) + ttk.Button(win, text="Закрыть", command=win.destroy).pack(pady=(0, 10)) + win.grab_set() + return + + # Режим с галочками: предложения ИИ по разделам безопасности win = tk.Toplevel(self.root) - win.title("🤖 Проверка безопасности — результат ИИ") - win.geometry("760x560") + win.title("🤖 Проверка безопасности — предложения ИИ") + win.geometry("960x760") win.transient(self.root) - txt = scrolledtext.ScrolledText(win, wrap=tk.WORD, font=('', 10), state=tk.NORMAL) - txt.pack(fill=tk.BOTH, expand=True, padx=10, pady=(10, 4)) - txt.insert('1.0', '\n'.join(lines)) - txt.config(state=tk.DISABLED) - ttk.Button(win, text="Закрыть", command=win.destroy).pack(pady=(0, 10)) + frm = ttk.Frame(win, padding=8) + frm.pack(fill=tk.BOTH, expand=True) + + header = f"Оценка ИИ: {rating}. Отметьте галочками, что применить в объект (ИИ может ошибаться — проверяйте)." + ttk.Label(frm, text=header, font=('Arial', 10, 'bold'), + wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) + + if notes_lines := [l for l in (lines or []) if l]: + ttk.Label(frm, text="Замечания:", font=('', 9, 'bold')).pack(anchor=tk.W, pady=(6, 0)) + t_notes = scrolledtext.ScrolledText(frm, height=4, wrap=tk.WORD, font=('', 9)) + t_notes.pack(fill=tk.X) + t_notes.insert('1.0', '\n'.join(notes_lines)) + t_notes.config(state=tk.DISABLED) + + sections = [ + ('attacker_category', 'Нарушитель (категория/уровень)'), + ('threats', 'Угрозы'), + ('incidents', 'Инциденты'), + ('criteria_values', 'Показатели критериев'), + ('criteria_justification', 'Обоснование значений'), + ('org_measures', 'Орг. меры'), + ('tech_measures', 'Тех. меры'), + ] + entries, checks = {}, {} + for key, label in sections: + v = proposals.get(key) + if not v: + continue + row = ttk.Frame(frm) + row.pack(fill=tk.X, pady=(4, 0)) + var = tk.BooleanVar(value=True) + ttk.Checkbutton(row, text=f"{label}:", variable=var).pack(side=tk.LEFT) + checks[key] = var + e = scrolledtext.ScrolledText(frm, height=2, wrap=tk.WORD, font=('', 9)) + e.pack(fill=tk.X) + e.insert('1.0', v) + entries[key] = e + + btn_row = ttk.Frame(frm) + btn_row.pack(fill=tk.X, pady=(8, 0)) + ttk.Button(btn_row, text="✔️ Применить отмеченное", + command=lambda: self._apply_security_ai_proposals(entries, checks, win)).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_row, text="Отмена", command=win.destroy).pack(side=tk.LEFT, padx=2) win.grab_set() except Exception: pass + def _apply_security_ai_proposals(self, entries, checks, win): + """Применить отмеченные предложения ИИ к полям безопасности объекта.""" + def _wanted(key): + return checks.get(key, tk.BooleanVar(value=True)).get() + + def _text(key): + e = entries.get(key) + return e.get('1.0', 'end-1c').strip() if e else '' + + def _fill_listbox(lb, text): + if not lb: + return + lb.delete(0, tk.END) + for line in (text or '').split('\n'): + line = line.strip() + if line: + lb.insert(tk.END, line) + + try: + if _wanted('attacker_category'): + val = _text('attacker_category') + if val and hasattr(self, 'is_attacker_category_combo'): + values = list(self.is_attacker_category_combo['values']) + exact = next((v for v in values if v.lower() == val.lower()), None) + self.is_attacker_category_combo.set(exact or val) + if _wanted('threats'): + _fill_listbox(getattr(self, 'is_threats_listbox', None), _text('threats')) + if _wanted('incidents'): + _fill_listbox(getattr(self, 'is_incidents_listbox', None), _text('incidents')) + if _wanted('criteria_values'): + _fill_listbox(getattr(self, 'is_criteria_values_listbox', None), _text('criteria_values')) + if _wanted('criteria_justification'): + _fill_listbox(getattr(self, 'is_criteria_justification_listbox', None), _text('criteria_justification')) + if _wanted('org_measures'): + _fill_listbox(getattr(self, 'is_org_measures_listbox', None), _text('org_measures')) + if _wanted('tech_measures'): + _fill_listbox(getattr(self, 'is_tech_measures_listbox', None), _text('tech_measures')) + self._collect_is() + self.log("🤖 Предложения ИИ по безопасности применены (по галочкам)") + win.destroy() + messagebox.showinfo("Безопасность", "Отмеченные предложения ИИ применены к объекту.\nПроверьте результат и при необходимости поправьте вручную.") + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось применить: {e}") + def _fill_subject_from_company(self): """Заполнить поля субъекта данными организации (если объект в той же организации).""" if self._current_is_idx is None: