```
from __future__ import annotations

import uuid
from collections import defaultdict
from typing import Callable, List, Optional, Dict

import flet as ft
from pydantic import BaseModel, Field


# ────────────────────────────── Domain layer ──────────────────────────────────
class TreeNode(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    role: str
    reasoning: str
    response: str
    children: List[TreeNode] = Field(default_factory=list)
    active_index: int = 0

    @property
    def active_child(self) -> Optional[TreeNode]:
        if not self.children:
            return None
        self._clamp_index()
        return self.children[self.active_index]

    def next_child(self) -> None:
        if self.children:
            self.active_index = min(self.active_index + 1, len(self.children) - 1)

    def prev_child(self) -> None:
        if self.children:
            self.active_index = max(self.active_index - 1, 0)

    def _clamp_index(self) -> None:
        self.active_index = min(max(self.active_index, 0), len(self.children) - 1)


class Tree(BaseModel):
    root_nodes: List[TreeNode] = Field(default_factory=list)
    active_tree_index: int = 0

    def active_path(self) -> List[TreeNode]:
        if not self.root_nodes:
            return []

        self.active_tree_index = min(
            max(self.active_tree_index, 0), len(self.root_nodes) - 1
        )

        path: List[TreeNode] = []
        node: Optional[TreeNode] = self.root_nodes[self.active_tree_index]
        while node:
            path.append(node)
            node = node.active_child
        return path


# ───────────────────────────── Event bus ──────────────────────────────────────
class EventBus:
    def __init__(self) -> None:
        self._subs: Dict[str, List[Callable]] = defaultdict(list)

    def sub(self, topic: str, fn: Callable) -> None:
        self._subs[topic].append(fn)

    def pub(self, topic: str, data=None) -> None:
        for fn in self._subs.get(topic, []):
            fn(data)


# ──────────────────────────── Application service ─────────────────────────────
class TreeService:
    def __init__(self, tree: Tree, bus: EventBus) -> None:
        self._tree = tree
        self._bus = bus

    def update_text(self, node: TreeNode, reasoning: str, response: str, snack_msg=True) -> None:
        node.reasoning, node.response = reasoning, response
        self._bus.pub("state_changed")
        if snack_msg:
            self._bus.pub("snackbar", "ノードを更新しました")

    def next_sibling(self, parent: TreeNode) -> None:
        parent.next_child()
        self._bus.pub("state_changed")

    def prev_sibling(self, parent: TreeNode) -> None:
        parent.prev_child()
        self._bus.pub("state_changed")

    def active_path(self) -> List[TreeNode]:
        return self._tree.active_path()


# ──────────────────────────────── UI layer ────────────────────────────────────
class ChatCard(ft.Card):
    def __init__(self, node: TreeNode, parent_node: Optional[TreeNode], service: TreeService, *args, **kwargs):
        self._node = node
        self._parent = parent_node
        self._svc = service
        self._edit_mode = False
        self._show_reasoning = False
        self._original_reasoning = ""
        self._original_response = ""
        self.reasoning_tf = ft.TextField(label="Reasoning", multiline=True, expand=True)
        self.response_tf = ft.TextField(label="Response", multiline=True, expand=True)

        super().__init__(content=self._with_padding(self._build_content()), *args, **kwargs)

    def _with_padding(self, content: ft.Control) -> ft.Control:
        return ft.Container(content=content, padding=16)

    def _build_header(self) -> ft.Row:
        title = ft.Text(self._node.role, style=ft.TextThemeStyle.TITLE_SMALL)

        controls: List[ft.Control] = [title, ft.Container(expand=True)]

        # Reasoningボタン（編集モードでない場合のみ表示）
        if not self._edit_mode:
            reasoning_btn = ft.IconButton(
                icon=ft.Icons.PSYCHOLOGY,
                icon_color=ft.Colors.BLUE if self._show_reasoning else None,
                tooltip="Reasoningを隠す" if self._show_reasoning else "Reasoningを表示",
                on_click=self._toggle_reasoning,
                disabled=not self._node.reasoning.strip(),
            )
            controls.append(reasoning_btn)

        edit_btn = self._build_edit_button()
        controls.append(edit_btn)
        controls.extend(self._build_nav_controls())

        return ft.Row(controls, alignment=ft.MainAxisAlignment.START)

    def _build_edit_button(self) -> ft.IconButton:
        return ft.IconButton(
            icon=ft.Icons.EDIT,
            icon_color=ft.Colors.RED if self._edit_mode else None,
            tooltip="キャンセル編集" if self._edit_mode else "編集モードへ",
            on_click=self._cancel_and_exit if self._edit_mode else self._enter_edit_mode,
        )

    def _build_nav_controls(self) -> List[ft.Control]:
        if not (self._parent and 1 < len(self._parent.children)):
            return []

        index = self._parent.active_index
        total = len(self._parent.children)
        return [
            ft.IconButton(
                icon=ft.Icons.CHEVRON_LEFT,
                on_click=lambda _: self._svc.prev_sibling(self._parent),
                disabled=index == 0,
                tooltip="前の兄弟ノードへ",
            ),
            ft.Text(f"{index + 1}/{total}", style=ft.TextThemeStyle.BODY_SMALL),
            ft.IconButton(
                icon=ft.Icons.CHEVRON_RIGHT,
                on_click=lambda _: self._svc.next_sibling(self._parent),
                disabled=total - 1 <= index,
                tooltip="次の兄弟ノードへ",
            ),
        ]

    def _build_edit_controls(self) -> ft.Column:
        buttons = ft.Row([
            ft.Container(expand=True),
            ft.OutlinedButton("キャンセル", on_click=self._cancel_and_exit),
            ft.FilledButton("更新", on_click=self._update_and_exit),
        ], spacing=8)
        return ft.Column([self._build_header(), self.reasoning_tf, self.response_tf, buttons], spacing=8)

    def _build_display_controls(self) -> ft.Column:
        controls: List[ft.Control] = [self._build_header()]

        # Reasoningを表示する場合
        if self._show_reasoning and self._node.reasoning.strip():
            controls.append(ft.Text(
                value=self._node.reasoning,
                style=ft.TextThemeStyle.BODY_MEDIUM,
                selectable=True,
                color=ft.Colors.BLUE_GREY_600
            ))

        if self._node.response:
            controls.append(ft.Text(value=self._node.response, style=ft.TextThemeStyle.BODY_MEDIUM, selectable=True))
        return ft.Column(controls, spacing=4)

    def _build_content(self) -> ft.Control:
        return self._build_edit_controls() if self._edit_mode else self._build_display_controls()

    def _toggle_reasoning(self, _):
        self._show_reasoning = not self._show_reasoning
        self._rebuild()

    def _enter_edit_mode(self, _):
        self._edit_mode = True
        self._original_reasoning = self._node.reasoning
        self._original_response = self._node.response
        self.reasoning_tf.value = self._node.reasoning
        self.response_tf.value = self._node.response
        self._rebuild()

    def _update_and_exit(self, _):
        self._svc.update_text(self._node, self.reasoning_tf.value, self.response_tf.value)
        self._edit_mode = False

    def _cancel_and_exit(self, _):
        self._edit_mode = False
        self.reasoning_tf.value = self._original_reasoning
        self.response_tf.value = self._original_response
        self._rebuild()

    def _rebuild(self):
        self.content = self._with_padding(self._build_content())
        self.update()


# ────────────────────────── Demo tree constructor ─────────────────────────────
def build_demo_tree() -> Tree:
    root = TreeNode(role="system", reasoning="", response="こんにちは！何かお手伝いできることはありますか？")
    a = TreeNode(role="user", reasoning="", response="明日の天気は？")
    b = TreeNode(role="user", reasoning="", response="東京への行き方は？")
    c = TreeNode(role="user", reasoning="", response="おでんの具で一番美味しいのは？")
    root.children, root.active_index = [a, b, c], 1

    b1 = TreeNode(role="assistant", reasoning="[東京への行き方を考える]", response="すみません、よくわかりませんでした。")
    b2 = TreeNode(role="assistant", reasoning="[東京への行き方を考える]", response="好きに行けばいいと思います。")
    b.children = [b1, b2]

    return Tree(root_nodes=[root])


class ChatPage(ft.Container):
    def __init__(self):
        self.chat_list =
        self.chat_sender =  ft.Row(
            [
                ft.TextField(label="送信するメッセージ"),
                ft.IconButton(
                    icon=ft.Icons.SEND_ROUNDED,
                    tooltip="メッセージを送る",
                ),
            ]
        ),
        super().__init__()


# ──────────────────────────────── Flet main ───────────────────────────────────
def main(page: ft.Page):
    page.title = "TreeNode Demo"
    page.scroll = ft.ScrollMode.AUTO

    bus = EventBus()
    service = TreeService(build_demo_tree(), bus)

    body_column = ft.Column(spacing=12)
    page.add(body_column)

    def rebuild_body(_=None):
        body_column.controls.clear()
        path = service.active_path()
        for depth, node in enumerate(path):
            parent = path[depth - 1] if 0 < depth else None
            body_column.controls.append(ChatCard(node=node, parent_node=parent, service=service))
        page.update()

    bus.sub("state_changed", rebuild_body)
    print("snackbar?")
    rebuild_body()


if __name__ == "__main__":
    ft.app(target=main)

```


Python 3.12+, Flet 0.28.3.
現在ページに表示している要素をChatPageという単一のコンテナにまとめたい。いい感じにして。