discussion history improvemnts

This commit is contained in:
2026-02-21 19:08:15 -05:00
parent 1d9c5a9b07
commit c0535bc9a6
4 changed files with 235 additions and 99 deletions

150
gui.py
View File

@@ -287,36 +287,38 @@ class ConfirmDialog:
DISC_ROLES = ["User", "AI", "Vendor API", "System"]
def _parse_history_entries(history: list[str]) -> list[dict]:
def _parse_history_entries(history: list[str], roles: list[str] | None = None) -> list[dict]:
"""
Convert the raw TOML string array into a flat list of {role, content} dicts.
Convert the raw TOML string array into a flat list of {role, content, collapsed} dicts.
Each TOML string is one excerpt (may contain multiple role blocks separated
by lines like "Role:" or "[Role]"). We detect the common patterns:
"User:\n..." "AI:\n..." "[User]\n..." "[AI]\n..."
and split accordingly. Unrecognised text becomes a User entry.
roles: list of known role strings (defaults to module DISC_ROLES if None).
"""
import re
known = roles if roles is not None else DISC_ROLES
entries: list[dict] = []
role_pattern = re.compile(
r'^(?:\[)?(' + '|'.join(re.escape(r) for r in DISC_ROLES) + r')(?:\])?:?\s*$',
r'^(?:\[)?(' + '|'.join(re.escape(r) for r in known) + r')(?:\])?:?\s*$',
re.IGNORECASE | re.MULTILINE,
)
for excerpt in history:
# Find all role header positions
splits = [(m.start(), m.end(), m.group(1).capitalize()) for m in role_pattern.finditer(excerpt)]
splits = [(m.start(), m.end(), m.group(1)) for m in role_pattern.finditer(excerpt)]
if not splits:
# No role headers found - treat whole excerpt as User content
text = excerpt.strip()
if text:
entries.append({"role": "User", "content": text})
entries.append({"role": "User", "content": text, "collapsed": False})
continue
# Extract content between headers
for idx, (start, end, role) in enumerate(splits):
next_start = splits[idx + 1][0] if idx + 1 < len(splits) else len(excerpt)
content = excerpt[end:next_start].strip()
# Normalise role capitalisation to match DISC_ROLES
matched = next((r for r in DISC_ROLES if r.lower() == role.lower()), role)
entries.append({"role": matched, "content": content})
# Normalise role capitalisation to match known list
matched = next((r for r in known if r.lower() == role.lower()), role)
entries.append({"role": matched, "content": content, "collapsed": False})
return entries
class App:
@@ -330,7 +332,10 @@ class App:
self.config.get("discussion", {}).get("history", [])
)
self.disc_entries: list[dict] = _parse_history_entries(self.history)
self.disc_roles: list[str] = list(
self.config.get("discussion", {}).get("roles", list(DISC_ROLES))
)
self.disc_entries: list[dict] = _parse_history_entries(self.history, self.disc_roles)
ai_cfg = self.config.get("ai", {})
self.current_provider: str = ai_cfg.get("provider", "gemini")
@@ -455,7 +460,7 @@ class App:
if dpg.does_item_exist(tag):
entry["content"] = dpg.get_value(tag)
self.history = self._disc_serialize()
self.config["discussion"] = {"history": self.history}
self.config["discussion"] = {"history": self.history, "roles": self.disc_roles}
self.config["ai"] = {
"provider": self.current_provider,
"model": self.current_model,
@@ -711,32 +716,47 @@ class App:
return
dpg.delete_item("disc_scroll", children_only=True)
for i, entry in enumerate(self.disc_entries):
collapsed = entry.get("collapsed", False)
preview = entry["content"].replace("\n", " ")[:60]
if len(entry["content"]) > 60:
preview += "..."
with dpg.group(parent="disc_scroll"):
with dpg.group(horizontal=True):
dpg.add_button(
tag=f"disc_toggle_{i}",
label="+" if collapsed else "-",
width=24,
callback=self._make_disc_toggle_cb(i),
)
dpg.add_combo(
tag=f"disc_role_{i}",
items=DISC_ROLES,
items=self.disc_roles,
default_value=entry["role"],
width=120,
width=160,
callback=self._make_disc_role_cb(i),
)
dpg.add_button(
label="Insert Before",
callback=self._make_disc_insert_cb(i),
if collapsed:
dpg.add_button(
label="Ins",
width=36,
callback=self._make_disc_insert_cb(i),
)
dpg.add_button(
label="Del",
width=36,
callback=self._make_disc_remove_cb(i),
)
dpg.add_text(preview, color=(160, 160, 150))
with dpg.group(tag=f"disc_body_{i}", show=not collapsed):
dpg.add_input_text(
tag=f"disc_content_{i}",
default_value=entry["content"],
multiline=True,
width=-1,
height=100,
callback=self._make_disc_content_cb(i),
on_enter=False,
)
dpg.add_button(
label="Remove",
callback=self._make_disc_remove_cb(i),
)
dpg.add_input_text(
tag=f"disc_content_{i}",
default_value=entry["content"],
multiline=True,
width=-1,
height=100,
callback=self._make_disc_content_cb(i),
on_enter=False,
)
dpg.add_separator()
def _make_disc_role_cb(self, idx: int):
@@ -764,8 +784,62 @@ class App:
self._rebuild_disc_list()
return cb
def _make_disc_toggle_cb(self, idx: int):
def cb():
if idx < len(self.disc_entries):
tag = f"disc_content_{idx}"
if dpg.does_item_exist(tag):
self.disc_entries[idx]["content"] = dpg.get_value(tag)
self.disc_entries[idx]["collapsed"] = not self.disc_entries[idx].get("collapsed", False)
self._rebuild_disc_list()
return cb
def cb_disc_collapse_all(self):
for i, entry in enumerate(self.disc_entries):
tag = f"disc_content_{i}"
if dpg.does_item_exist(tag):
entry["content"] = dpg.get_value(tag)
entry["collapsed"] = True
self._rebuild_disc_list()
def cb_disc_expand_all(self):
for entry in self.disc_entries:
entry["collapsed"] = False
self._rebuild_disc_list()
def _rebuild_disc_roles_list(self):
if not dpg.does_item_exist("disc_roles_scroll"):
return
dpg.delete_item("disc_roles_scroll", children_only=True)
for i, role in enumerate(self.disc_roles):
with dpg.group(horizontal=True, parent="disc_roles_scroll"):
dpg.add_button(
label="x", width=24,
callback=self._make_disc_remove_role_cb(i),
)
dpg.add_text(role)
def _make_disc_remove_role_cb(self, idx: int):
def cb():
if idx < len(self.disc_roles):
self.disc_roles.pop(idx)
self._rebuild_disc_roles_list()
self._rebuild_disc_list()
return cb
def cb_disc_add_role(self):
if not dpg.does_item_exist("disc_new_role_input"):
return
name = dpg.get_value("disc_new_role_input").strip()
if name and name not in self.disc_roles:
self.disc_roles.append(name)
dpg.set_value("disc_new_role_input", "")
self._rebuild_disc_roles_list()
self._rebuild_disc_list()
def cb_disc_append_entry(self):
self.disc_entries.append({"role": "User", "content": ""})
default_role = self.disc_roles[0] if self.disc_roles else "User"
self.disc_entries.append({"role": default_role, "content": "", "collapsed": False})
self._rebuild_disc_list()
def cb_disc_clear(self):
@@ -785,13 +859,13 @@ class App:
def cb_append_message_to_history(self):
msg = dpg.get_value("ai_input")
if msg:
self.disc_entries.append({"role": "User", "content": msg})
self.disc_entries.append({"role": "User", "content": msg, "collapsed": False})
self._rebuild_disc_list()
def cb_append_response_to_history(self):
resp = self.ai_response
if resp:
self.disc_entries.append({"role": "AI", "content": resp})
self.disc_entries.append({"role": "AI", "content": resp, "collapsed": False})
self._rebuild_disc_list()
@@ -971,9 +1045,22 @@ class App:
):
with dpg.group(horizontal=True):
dpg.add_button(label="+ Entry", callback=self.cb_disc_append_entry)
dpg.add_button(label="-All", callback=self.cb_disc_collapse_all)
dpg.add_button(label="+All", callback=self.cb_disc_expand_all)
dpg.add_button(label="Clear All", callback=self.cb_disc_clear)
dpg.add_button(label="Save", callback=self.cb_disc_save)
dpg.add_separator()
with dpg.collapsing_header(label="Roles", default_open=False):
with dpg.child_window(tag="disc_roles_scroll", height=96, border=True):
pass
with dpg.group(horizontal=True):
dpg.add_input_text(
tag="disc_new_role_input",
hint="New role name",
width=-72,
)
dpg.add_button(label="Add", callback=self.cb_disc_add_role)
dpg.add_separator()
with dpg.child_window(tag="disc_scroll", height=-1, border=False):
pass
@@ -1100,6 +1187,7 @@ class App:
self._build_ui()
theme.load_from_config(self.config)
self._rebuild_disc_list()
self._rebuild_disc_roles_list()
self._fetch_models(self.current_provider)
while dpg.is_dearpygui_running():