```
import random
import threading
import time
from typing import Callable, Optional

import flet as ft
from pydantic import BaseModel, Field
from uuid6 import uuid7

from src.chat.chat_event_bus import EventBus
from src.chat.chat_event_topic import Topic


class TreeNode(BaseModel):
    id: str = Field(default_factory=lambda: str(uuid7()))
    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


# ──────────────────────────── Application Service ─────────────────────────────
class TreeService:
    def __init__(self, tree: Tree, bus: EventBus) -> None:
        self._tree = tree
        self._bus = bus
        self._on_user_input: Optional[Callable[[TreeNode], None]] = None
        self._cancel_flag = False
        self._is_processing = False

        # イベント購読
        self._bus.sub(Topic.TREE_NODE_UPDATE, self._handle_update_node)
        self._bus.sub(Topic.TREE_SIBLING_NEXT, self._handle_next_sibling)
        self._bus.sub(Topic.TREE_SIBLING_PREV, self._handle_prev_sibling)
        self._bus.sub(Topic.CHAT_MSG_SEND, self._handle_send_message)
        self._bus.sub(Topic.CHAT_PROCESSING_CANCEL, lambda _: self.cancel_processing())

    def set_on_user_input(self, callback: Callable[[TreeNode], None]) -> None:
        self._on_user_input = callback

    # ─────────── event handlers ───────────
    def _handle_update_node(self, data: dict[str, str]) -> None:
        node_id = data["node_id"]
        node = self._find_node_by_id(node_id)
        if node:
            node.reasoning = data["reasoning"]
            node.response = data["response"]
            # ノード内容更新を通知（特定のノード向け）
            self._bus.pub(Topic.TREE_NODE_CONTENT_UPDATED, {"node": node})

    def _handle_next_sibling(self, parent_id: str) -> None:
        parent = self._find_node_by_id(parent_id)
        if parent:
            parent.next_child()
            # アクティブパス変更を通知
            self._bus.pub(Topic.TREE_ACTIVE_PATH_CHANGED)

    def _handle_prev_sibling(self, parent_id: str) -> None:
        parent = self._find_node_by_id(parent_id)
        if parent:
            parent.prev_child()
            # アクティブパス変更を通知
            self._bus.pub(Topic.TREE_ACTIVE_PATH_CHANGED)

    def _handle_send_message(self, message: str) -> None:
        if self._should_ignore_message(message):
            return

        user_node = self._add_user_node(message.strip())
        # 新しいノード追加を通知
        self._bus.pub(Topic.TREE_NODE_ADDED)
        self._start_processing(user_node)

    def _should_ignore_message(self, message: str) -> bool:
        return self._is_processing or not message.strip()

    def _add_user_node(self, message: str) -> TreeNode:
        user_node = TreeNode(role="user", reasoning="", response=message)
        path = self._tree.active_path()

        if path:
            parent = path[-1]
            parent.children.append(user_node)
            parent.active_index = len(parent.children) - 1
        else:
            self._tree.root_nodes.append(user_node)

        return user_node

    def _start_processing(self, user_node: TreeNode) -> None:
        if not self._on_user_input:
            return

        self._is_processing = True
        self._cancel_flag = False
        self._bus.pub(Topic.CHAT_PROCESSING_START)

        thread = threading.Thread(
            target=self._run_user_input_handler,
            args=(user_node,),
            daemon=True
        )
        thread.start()

    # ─────────── processing control ───────────
    def cancel_processing(self) -> None:
        self._cancel_flag = True

    def _run_user_input_handler(self, user_node: TreeNode) -> None:
        try:
            self._on_user_input(user_node)
        finally:
            self._is_processing = False
            self._bus.pub(Topic.CHAT_PROCESSING_FINISH)

    # ─────────── helpers ───────────
    def _find_node_by_id(
            self, node_id: str, node: Optional[TreeNode] = None
    ) -> Optional[TreeNode]:
        if node is None:
            for root in self._tree.root_nodes:
                found = self._find_node_by_id(node_id, root)
                if found:
                    return found
            return None
        if node.id == node_id:
            return node
        for child in node.children:
            found = self._find_node_by_id(node_id, child)
            if found:
                return found
        return None

    # ─────────── properties ───────────
    @property
    def is_processing(self) -> bool:
        return self._is_processing

    @property
    def cancel_flag(self) -> bool:
        return self._cancel_flag


# ──────────────────────────────── UI Layer ────────────────────────────────────
class ChatCard(ft.Card):
    def __init__(
            self,
            node: TreeNode,
            parent_node: Optional[TreeNode],
            bus: EventBus,
            *args,
            **kwargs,
    ):
        self._node = node
        self._parent = parent_node
        self._bus = bus
        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)

        # 特定のノード更新のみ購読
        self._bus.sub(Topic.TREE_NODE_CONTENT_UPDATED, self._on_node_content_updated)
        self._bus.sub(Topic.TREE_NODE_RESPONSE_STREAMING, self._on_node_response_streaming)

    def _on_node_content_updated(self, data: dict) -> None:
        """特定のノードが更新された時のみ再描画"""
        if data and data.get("node") and data["node"].id == self._node.id:
            self._rebuild()

    def _on_node_response_streaming(self, data: dict) -> None:
        """レスポンスストリーミング中の更新"""
        if data and data.get("node") and data["node"].id == self._node.id:
            # レスポンス部分のみ更新
            if not self._edit_mode:
                self._rebuild()

    # ─────────── build helpers ───────────
    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)]
        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 = 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,
        )
        controls.append(edit_btn)
        controls.extend(self._build_nav_controls())
        return ft.Row(controls, alignment=ft.MainAxisAlignment.START)

    def _build_nav_controls(self) -> list[ft.Control]:
        if not (self._parent and len(self._parent.children) > 1):
            return []
        index = self._parent.children.index(self._node)
        total = len(self._parent.children)
        return [
            ft.IconButton(
                icon=ft.Icons.CHEVRON_LEFT,
                on_click=lambda _: self._bus.pub(Topic.TREE_SIBLING_PREV, self._parent.id),
                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._bus.pub(Topic.TREE_SIBLING_NEXT, self._parent.id),
                disabled=index >= total - 1,
                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()]
        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()

    # ─────────── UI callbacks ───────────
    def _toggle_reasoning(self, _) -> None:
        self._show_reasoning = not self._show_reasoning
        self._rebuild()

    def _enter_edit_mode(self, _) -> None:
        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, _) -> None:
        self._bus.pub(
            Topic.TREE_NODE_UPDATE,
            {
                "node_id": self._node.id,
                "reasoning": self.reasoning_tf.value,
                "response": self.response_tf.value,
            },
        )
        self._edit_mode = False

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

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


# ──────────────────────────── Demo Processing ─────────────────────────────────
def create_demo_processor(service: TreeService, bus: EventBus) -> Callable[[TreeNode], None]:
    __lipsum__ = [
        "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
        "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
        "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
        "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
        "consequat", "duis", "aute", "irure", "reprehenderit", "in", "voluptate", "velit",
        "esse", "cillum", "fugiat", "nulla", "pariatur", "excepteur", "sint", "occaecat",
        "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia", "deserunt",
        "mollit", "anim", "id", "est", "laborum",
    ]

    def process_user_input(user_node: TreeNode) -> None:
        # アシスタントノードを作成
        assistant_node = TreeNode(role="assistant", reasoning="", response="")
        user_node.children.append(assistant_node)
        user_node.active_index = 0

        # 新しいアシスタントノードが追加されたことを通知
        bus.pub(Topic.TREE_NODE_ADDED)

        # 少し待ってからストリーミング開始
        time.sleep(1)
        words = []

        for i in range(random.randint(10, 30)):
            if service.cancel_flag:
                break
            words.append(random.choice(__lipsum__))
            assistant_node.response = " ".join(words).capitalize()
            # レスポンスストリーミング中を通知
            bus.pub(Topic.TREE_NODE_RESPONSE_STREAMING, {"node": assistant_node})
            time.sleep(0.3)

        if not service.cancel_flag and assistant_node.response:
            assistant_node.response += "."
            # 最終的なコンテンツ更新を通知
            bus.pub(Topic.TREE_NODE_CONTENT_UPDATED, {"node": assistant_node})

    return process_user_input


# ────────────────────────── 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])


# ─────────────────────────────── Chat Page ────────────────────────────────────
class ChatPage(ft.Container):
    def __init__(self, tree: Tree, bus: EventBus) -> None:
        super().__init__(expand=True)
        self.tree = tree
        self.bus = bus
        self._is_processing = False

        self.chat_list = ft.ListView(expand=True, spacing=12, controls=[])
        self.message_input = ft.TextField(
            label="送信するメッセージ",
            expand=True,
            on_submit=self.send_message,
            multiline=True,
            min_lines=1,
            max_lines=5,
        )
        self.action_button = ft.IconButton(
            icon=ft.Icons.SEND_ROUNDED, tooltip="メッセージを送る", on_click=self.send_message
        )

        self.chat_sender = ft.Column(
            controls=[ft.Row([self.message_input, self.action_button])]
        )
        self.content = ft.Column(
            expand=True,
            controls=[ft.Container(content=self.chat_list, expand=True), self.chat_sender],
        )

        # より細分化されたイベント購読
        self.bus.sub(Topic.TREE_NODE_ADDED, self._on_node_added)
        self.bus.sub(Topic.TREE_ACTIVE_PATH_CHANGED, self._on_active_path_changed)
        self.bus.sub(Topic.CHAT_PROCESSING_START, self._on_processing_start)
        self.bus.sub(Topic.CHAT_PROCESSING_FINISH, self._on_processing_finish)

    # ─────────── lifecycle ───────────
    def did_mount(self) -> None:
        self.rebuild_chat_list()

    # ─────────── event handlers ───────────
    def _on_node_added(self, _=None) -> None:
        """新しいノードが追加された時のみ再構築"""
        self.rebuild_chat_list()
        self.scroll_to_bottom()

    def _on_active_path_changed(self, _=None) -> None:
        """アクティブパスが変更された時のみ再構築"""
        self.rebuild_chat_list()

    # ─────────── UI updates ───────────
    def rebuild_chat_list(self) -> None:
        self.chat_list.controls.clear()
        path = self.tree.active_path()
        for depth, node in enumerate(path):
            parent = path[depth - 1] if depth > 0 else None
            self.chat_list.controls.append(ChatCard(node=node, parent_node=parent, bus=self.bus))
        self.update()

    def scroll_to_bottom(self):
        if self.chat_list.controls:
            self.chat_list.scroll_to(offset=-1, duration=500, curve=ft.AnimationCurve.EASE_IN_OUT)

    # ─────────── send / cancel ───────────
    def send_message(self, _) -> None:
        if self._is_processing:
            self.bus.pub(Topic.CHAT_PROCESSING_CANCEL)
        else:
            message = self.message_input.value
            if message and message.strip():
                self.bus.pub(Topic.CHAT_MSG_SEND, message)
                self.message_input.value = ""
                self.message_input.focus()
        self.update()

    # ─────────── processing state ───────────
    def _on_processing_start(self, _=None) -> None:
        self._is_processing = True
        self.action_button.icon = ft.Icons.CANCEL
        self.action_button.tooltip = "処理をキャンセル"
        self.message_input.disabled = True
        self.update()

    def _on_processing_finish(self, _=None) -> None:
        self._is_processing = False
        self.action_button.icon = ft.Icons.SEND_ROUNDED
        self.action_button.tooltip = "メッセージを送る"
        self.message_input.disabled = False
        self.message_input.focus()
        self.update()


# ──────────────────────────────── Flet Main ───────────────────────────────────
def main(page: ft.Page) -> None:
    page.title = "TreeNode Demo"
    bus = EventBus()
    tree = build_demo_tree()

    service = TreeService(tree, bus)
    demo_processor = create_demo_processor(service, bus)
    service.set_on_user_input(demo_processor)

    chat_page = ChatPage(tree=tree, bus=bus)
    page.add(chat_page)

    # 初期状態の構築をトリガー
    bus.pub(Topic.TREE_ACTIVE_PATH_CHANGED)


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

Python 3.12+, Flet 0.28.3.
roleがassistantのノードについて、右にノードがなくとも右矢印を押せるようにし、その場合には右に空のノードを追加したうえでそれをアクティブにして、chatを送信した際に行っているストリーミングをその新ノードに受けて。チャットを送信したときとは違い、下方向にノードは追加しなくていい。