```
import uuid
import asyncio
import threading
import time
import random
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
        self._on_user_input: Optional[Callable[[TreeNode], None]] = None
        self._cancel_flag = False
        self._is_processing = False

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

    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()

    def send_user_message(self, message: str) -> None:
        if self._is_processing or not message.strip():
            return

        # Add user node
        user_node = TreeNode(role="user", reasoning="", response=message.strip())
        path = self.active_path()
        if path:
            path[-1].children.append(user_node)
            # Update active index to the new child
            path[-1].active_index = len(path[-1].children) - 1
        else:
            self._tree.root_nodes.append(user_node)

        self._bus.pub("state_changed")

        # Execute on_user_input if set
        if self._on_user_input:
            self._is_processing = True
            self._cancel_flag = False
            self._bus.pub("processing_started")

            # Run in thread to avoid blocking UI
            thread = threading.Thread(target=self._run_user_input_handler, args=(user_node,))
            thread.daemon = True
            thread.start()

    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("processing_finished")

    @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], 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 processing ─────────────────────────────────
def create_demo_processor(service: TreeService, bus: EventBus) -> Callable[[TreeNode], None]:
    # Lorem ipsum words for generating fake responses
    LOREM_WORDS = [
        "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:
        # Add assistant node
        assistant_node = TreeNode(role="assistant", reasoning="", response="")
        user_node.children.append(assistant_node)
        user_node.active_index = 0
        bus.pub("state_changed")

        # Generate response word by word
        words = []
        for _ in range(random.randint(10, 30)):  # 10-30 words
            if service.cancel_flag:
                break

            word = random.choice(LOREM_WORDS)
            words.append(word)
            assistant_node.response = " ".join(words)

            # Capitalize first letter of first word
            if len(words) == 1:
                assistant_node.response = assistant_node.response.capitalize()

            bus.pub("state_changed")
            time.sleep(0.5)  # 0.5 second delay

        # Add period at the end if not cancelled
        if not service.cancel_flag and assistant_node.response:
            assistant_node.response += "."
            bus.pub("state_changed")

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


class ChatPage(ft.Container):
    def __init__(self, service: TreeService, bus: EventBus):
        super().__init__(expand=True)
        self.service = service
        self.bus = bus
        self._is_processing = False

        # スクロール可能なチャットリスト
        self.chat_list = ft.ListView(
            expand=True, spacing=12, controls=[], auto_scroll=True,
        )

        # メッセージ入力フィールド
        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,
            ]
        )

        # Subscribe to processing events
        self.bus.sub("processing_started", self._on_processing_started)
        self.bus.sub("processing_finished", self._on_processing_finished)

    def did_mount(self):
        self.rebuild_chat_list()

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

    def send_message(self, e):
        if self._is_processing:
            # Cancel processing
            self.service.cancel_processing()
        else:
            # Send message
            message = self.message_input.value
            if message and message.strip():
                self.service.send_user_message(message)
                self.message_input.value = ""
                self.message_input.focus()
        self.update()

    def _on_processing_started(self, _=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_finished(self, _=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()

    def update(self):
        super().update()


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

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

    # Set up the demo processor
    demo_processor = create_demo_processor(service, bus)
    service.set_on_user_input(demo_processor)

    # Create ChatPage instance
    chat_page = ChatPage(service=service, bus=bus)

    # Add ChatPage to the page
    page.add(chat_page)

    def rebuild_body(_=None):
        chat_page.rebuild_chat_list()
        page.update()

    bus.sub("state_changed", rebuild_body)
    bus.pub("state_changed")  # initial load


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

Python 3.12+, Flet 0.28.3.
機能を完全に維持したまま、結合度を下げ、拡張や理解のしやすいコードにリファクタリングして。
また。型指定はビルトインを優先して、単一のファイルに収めて。