feat(ui): gemini-style top-anchored scroll, centered chat layout and task drawer sync (closes #49)

This commit is contained in:
2026-08-21 12:43:13 +03:00
parent feff469282
commit 7bf1af7c8f
7 changed files with 649 additions and 440 deletions
+25 -23
View File
@@ -10,16 +10,24 @@ AI-CONTEXT-ANCHORS:
===============================================================================
"""
from typing import Tuple, Dict
from typing import Tuple, Dict, List, Set
from core.connection import get_connection
# ANCHOR[PROMPT_DIFF_BUILDER]
def build_prompt_diff(action: str, section_id: int, item_id: int, content: str = "", prompt_name: str = "main_agent") -> Tuple[str, str]:
"""
Возвращает кортеж (merged_draft_text, diff_html_for_ui).
"""
def build_prompt_diff(
action: str,
section_id: int = None,
item_id: int = None,
content: str = "",
delete_nodes: List[Tuple[int, int]] = None,
prompt_name: str = "main_agent"
) -> Tuple[str, str]:
act = (action or "ADD").upper()
nodes_to_delete: Set[Tuple[int, int]] = set()
if delete_nodes:
nodes_to_delete.update(delete_nodes)
elif act == "DELETE" and section_id is not None and item_id is not None:
nodes_to_delete.add((section_id, item_id))
with get_connection(row_factory=True) as conn:
cursor = conn.cursor()
@@ -32,46 +40,40 @@ def build_prompt_diff(action: str, section_id: int, item_id: int, content: str =
existing_nodes = cursor.fetchall()
nodes_dict = {(r["section_id"], r["item_id"]): r["content"] for r in existing_nodes}
nodes_dict_for_draft = {k: v for k, v in nodes_dict.items() if k not in nodes_to_delete}
# Формируем словарь для чистого текста
nodes_dict_for_draft = {k: v for k, v in nodes_dict.items() if k != (section_id, item_id)} if act == "DELETE" else dict(nodes_dict)
if act != "DELETE":
if act not in ["DELETE", "BATCH_DELETE"] and section_id is not None and item_id is not None:
nodes_dict_for_draft[(section_id, item_id)] = content
draft_lines = []
curr_sec = None
for (s_id, i_id), txt in sorted(nodes_dict_for_draft.items()):
if i_id == 0:
if curr_sec is not None:
draft_lines.append("")
if curr_sec is not None: draft_lines.append("")
draft_lines.append(f"{s_id}. {txt}")
curr_sec = s_id
else:
draft_lines.append(f" {s_id}.{i_id}. {txt}")
merged_prompt = "\n".join(draft_lines)
# Формируем HTML Diff
diff_lines = []
curr_sec = None
display_nodes = dict(nodes_dict)
if act != "DELETE":
if act not in ["DELETE", "BATCH_DELETE"] and section_id is not None and item_id is not None:
display_nodes[(section_id, item_id)] = content
for (s_id, i_id), txt in sorted(display_nodes.items()):
if i_id == 0:
if curr_sec is not None:
diff_lines.append("")
if curr_sec is not None: diff_lines.append("")
diff_lines.append(f"{s_id}. {txt}")
curr_sec = s_id
else:
if s_id == section_id and i_id == item_id:
if act == "DELETE":
line_str = f' <span class="line-through text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300 opacity-80">{s_id}.{i_id}. {txt} [УДАЛЕНИЕ]</span>'
else:
line_str = f' <span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300">{s_id}.{i_id}. {txt}</span>'
if (s_id, i_id) in nodes_to_delete:
line_str = f' <span class="line-through text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300 opacity-80">{s_id}.{i_id}. {txt} [УДАЛЕНИЕ]</span>'
elif act == "ADD" and s_id == section_id and i_id == item_id:
line_str = f' <span class="text-rose-600 font-bold bg-rose-50 px-1.5 py-0.5 rounded border border-rose-300">{s_id}.{i_id}. {txt}</span>'
else:
line_str = f" {s_id}.{i_id}. {txt}"
diff_lines.append(line_str)
diff_html = "\n".join(diff_lines)
return merged_prompt, diff_html
return merged_prompt, "\n".join(diff_lines)
+17 -5
View File
@@ -7,7 +7,7 @@ ROLE: Единый доменный сервис управления систе
===============================================================================
"""
from typing import Tuple, Dict, Any
from typing import Tuple, Dict, Any, List, Optional
from .repository import repo_get_active_prompt, repo_apply_prompt_action, repo_save_full_prompt
from .diff_engine import build_prompt_diff
@@ -22,16 +22,28 @@ def apply_prompt_action(action: str, section_id: int, item_id: int, content: str
repo_apply_prompt_action(action, section_id, item_id, content)
def save_full_prompt_draft(draft_text: str) -> None:
def save_full_prompt_draft(draft_text: str, prompt_name: str = "main_agent") -> None:
"""Сохранить полный черновик промпта."""
repo_save_full_prompt(draft_text)
repo_save_full_prompt(draft_text, prompt_name=prompt_name)
def create_prompt_preview(action: str, section_id: int, item_id: int, content: str = "") -> Tuple[str, str, str]:
def create_prompt_preview(
action: str,
section_id: Optional[int] = None,
item_id: Optional[int] = None,
content: str = "",
delete_nodes: Optional[List[Tuple[int, int]]] = None
) -> Tuple[str, str, str]:
"""
Формирует черновик и diff.
Возвращает (merged_draft, diff_html, baseline_prompt).
"""
baseline = repo_get_active_prompt()
draft, diff_html = build_prompt_diff(action, section_id, item_id, content)
draft, diff_html = build_prompt_diff(
action=action,
section_id=section_id,
item_id=item_id,
content=content,
delete_nodes=delete_nodes
)
return draft, diff_html, baseline