GUI変更:ソース:PaintGUI.py

PySide6を使って作っています。
マークダウンにはコードの部分のみ対応し、そのほかの部分には対応していません。

PaintLabelBox:文字描画の最もコアな部分を担っています。

PaintButton:ボタンで今回はコピーボタンに使用しています。
PaintChatTitleBox:タイトルとコピーボタンのオブジェクトです。
PaintTitelAndContents:タイトルとコンテンツのオブジェクトです。
PaintChatNode:チャットノードです。ここでプログラムコードを認識して特別な表示にしています。
PaintChatThread:PaintChatNodeを配列管理して、チャットのスレッドにしています。
PaintArrowSlider:スライダーです。
ChatPaintWidget:PaintChatThread、PaintArrowSliderを統合しています。
ChatWidget:ChatPaintWidgetにタイトルや入力欄をチャットとして使えるようにしています。

AppendNodeEvent、AppendTextEvent、SetLastNodeTextイベントとして受け取るためのクラスです。スレッド建てて。これらで受け取らないと、すべての処理が終わるまで固まってしまいます。

import sys
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QPushButton, QSizePolicy
from PySide6.QtGui import QPainter, QColor, QFont, QFontMetrics, QPen, QBrush, QMouseEvent, QKeyEvent, QWheelEvent
from PySide6.QtCore import Qt, QRect, QPoint, QEvent, QCoreApplication ,Signal, QThread, QObject
import re
import pyperclip

from langchain_core.callbacks import BaseCallbackHandler

from typing import  Any
import threading

class PaintLabelBox():
    def __init__(self, text=""):
        self.rect = QRect(0, 0, 150, 50)
        self.font = QFont("Arial", 15)
        self.span = 1
        self.text = text
        self.align = Qt.AlignLeft
        self.height_fit = True
        self.border = 5
        self.__set_text_rect()

        self.__fit_text()

    def set_font_size(self, size):
        self.font = QFont("Arial", size)

    def __set_text_rect(self):
        self.text_rect = QRect(
            int(self.rect.x() + self.span+self.border),
            int(self.rect.y() + self.span+self.border),
            self.rect.width() - (self.span+self.border) * 2,
            self.rect.height() - (self.span+self.border) * 2)

    def set_hieght_fit(self, fit_flag):
        self.height_fit = fit_flag

    def set_size(self, width, hight):
        if width < ((self.span+self.border) * 2)*2:
            width = ((self.span+self.border) * 2)*2
        if hight < ((self.span+self.border) * 2)*2:
            hight = ((self.span+self.border) * 2)*2

        self.rect.setRect(self.rect.x(), self.rect.y(), width, hight)
        # self.__set_text_rect()
        self.__fit_text()

    def set_width(self, w):
        if w < ((self.span+self.border) * 2)*2:
            w = ((self.span+self.border) * 2)*2
        self.rect.setRect(self.rect.x(), self.rect.y(), w, self.rect.height())
        self.__set_text_rect()
        self.__fit_text()

    def set_position(self, x, y):
        self.rect.setRect(x, y, self.rect.width(), self.rect.height())
        self.__fit_text()

    def move(self, x, y):
        self.rect.setRect(self.rect.x() + x, self.rect.y() + y,
                          self.rect.width(), self.rect.height())
        self.__set_text_rect()

    def set_text(self, text):
        self.text = text
        self.__fit_text()

    def get_text(self):
        return self.text

    def append_text(self, text):
        self.text += text
        self.__fit_text()

    def width(self):
        return self.rect.width()

    def height(self):
        return self.rect.height()

    def x(self):
        return self.rect.x()

    def y(self):
        return self.rect.y()

    #  テキストをwidthで改行する
    def set_span(self, span):
        self.span = span

    def set_border(self, bsize):
        self.border = bsize

    def get_border(self):
        return self.border

    def __fit_text(self):
        metrics = QFontMetrics(self.font)
        lines = self.text.split("\n")
        result_text = ""

        for line in lines:
            text_width = metrics.horizontalAdvance(line)
            # 初めの大きさの確認で問題なければ何もしない。
            if text_width < self.text_rect.width():
                result_text += "\n" + line
                continue
            result_text += "\n" + self.__add_new_line(metrics, line)
        self.fit_text = result_text[1:]
        self.__fit_height()

    def __add_new_line(self, metrics, line):
        if len(line) <= 1:
            return line
        text_width = metrics.horizontalAdvance(line)
        num = len(line)
        # 分割位置の計算
        sep_num = int(self.text_rect.width()/text_width * num)
        line_buf = line[:sep_num]
        text_width = metrics.horizontalAdvance(line_buf)
        pre_text = line_buf
        if text_width < self.text_rect.width():
            # 分割後のサイズが小さいとき一文字ずつ追加して大きさを超えるところを探す。

            while text_width < self.text_rect.width():
                pre_text = line_buf
                sep_num += 1
                line_buf = line[:sep_num]
                text_width = metrics.horizontalAdvance(line_buf)
            # ここを抜けたという事は行き過ぎなので一つ戻す。
            sep_num -= 1
            pass
        elif self.text_rect.width() < text_width:
            # 分割後のサイズが大きいとき一文字減らして追加して大きさを以下になるところを探す。
            while self.text_rect.width() < text_width:
                sep_num -= 1
                line_buf = line[:sep_num]
                text_width = metrics.horizontalAdvance(line_buf)
                pre_text = line_buf
            pass
        else:

            pre_text = line_buf
            # 同じとき完了
        result_text = pre_text
        # 残った文字列が表示領域より大きいとき再帰的の呼び出す。
        line_buf = line[sep_num:]
        text_width = metrics.horizontalAdvance(line_buf)
        if self.text_rect.width() < text_width:

            result_text += "\n" + self.__add_new_line(metrics, line_buf)
        else:
            result_text += "\n" + line_buf
        return result_text

    def __fit_height(self):
        if self.height_fit:
            metrics = QFontMetrics(self.font)
            line_height = metrics.boundingRect(self.fit_text).height()  # 行の高さ
            line_num = len(self.fit_text.split("\n"))
            self.rect = QRect(self.rect.x(), self.rect.y(),
                              self.rect.width(),
                              (line_height+metrics.leading())*line_num +
                              (self.span+self.border)*2)
            self.__set_text_rect()

    def set_align(self, pos_st):

        if "left" == pos_st.lower():
            self.align = Qt.AlignLeft
        elif "center" == pos_st.lower():
            self.align = Qt.AlignCenter
        elif "right" == pos_st.lower():
            self.align = Qt.AlignRight

    def get_align(self):
        return self.align

    def draw(self, obj):
        painter = QPainter(obj)
        painter.setFont(self.font)

        if 0 == self.border:
            painter.setPen(QPen(QColor(255, 255, 255), self.border))  # 枠線
        else:
            painter.setPen(QPen(QColor(0, 0, 0), self.border))  # 枠線
        painter.setBrush(QBrush(QColor(255, 255, 255)))  # 塗りつぶし
        painter.drawRect(self.rect)
        painter.setPen(QPen(QColor(0, 0, 0), self.border))  # 枠線

        painter.drawText(self.text_rect, self.align, self.fit_text)


class PaintButton(PaintLabelBox):
    NO_STATE = 0
    NOW_CLICKED = 1
    NOW_HOVER = 2

    def __init__(self, text=""):
        super().__init__( text)
        self.rect = QRect(50, 50, 100, 50)
        self.state = self.NO_STATE
        self.set_align("center")
        self.nostate_color = QColor(192, 192, 192)
        self.clicked_color = QColor(255, 0, 0)
        self.hover_color = QColor(224, 224, 224)

    def check_point_in(self, pos):
        if self.rect.contains(pos):
            return True
        else:
            return False

    def mouse_down(self, fpos):
        pos = QPoint(fpos.x(),fpos.y())

        if self.rect.contains(pos):
            self.state = self.NOW_CLICKED
            return True
        else:
            return False

    def mouse_up(self, fpos):
        pos = QPoint(fpos.x(), fpos.y())
        self.state = self.NO_STATE

    def mouse_hover(self, fpos):
        pos = QPoint(fpos.x(), fpos.y())
        if self.rect.contains(pos):

            if self.NO_STATE == self.state:
                self.state = self.NOW_HOVER
        else:
            self.state = self.NO_STATE

    def set_back_color(self, color):
        self.nostate_color = color

    def set_clicked_back_color(self, color):
        self.clicked_color = color

    def set_hover_back_color(self, color):
        self.hover_color = color

    def draw(self, obj):
        painter = QPainter(obj)

        if self.NO_STATE == self.state:
            painter.setPen(QPen(QColor(0, 0, 0), 2))  # 青色の枠線
            painter.setBrush(QBrush(self.nostate_color))  # 赤色の塗りつぶし
        if self.NOW_CLICKED == self.state:
            painter.setPen(QPen(QColor(0, 0, 0), 2))  # 青色の枠線
            painter.setBrush(QBrush(self.clicked_color))  # 赤色の塗りつぶし
        if self.NOW_HOVER == self.state:
            painter.setPen(QPen(QColor(0, 0, 0), 2))  # 青色の枠線
            painter.setBrush(QBrush(self.hover_color))  # 赤色の塗りつぶし

        painter.drawRect(self.rect)
        painter.setPen(QPen(QColor(0, 0, 0), 2))  # 青色の枠線
        painter.drawText(self.rect, self.get_align(), self.text)


class PaintChatTitleBox():
    def __init__(self, title_text):
        self.button = PaintButton("copy")
        self.title = PaintLabelBox(title_text)

    def mouse_down(self, pos):
        return self.button.mouse_down(pos)

    def mouse_up(self, pos):
        self.button.mouse_up(pos)

    def mouse_hover(self, pos):
        self.button.mouse_hover(pos)

    def set_title_text(self, text):
        self.title.set_text(text)

    def get_title_text(self):
        self.title.get_text()

    def set_font_size(self, size):
        if size < 7:
            size = 7
        self.title.set_font_size(size)
        self.button.set_font_size(size-2)

    def set_title_size(self, width, height):
        self.title.set_size(width, height)
        self.button.set_position(self.title.x() + self.title.width() -
                                 self.button.width() - 1,
                                 self.title.y() + 1)

    def set_button_size(self, width, height):
        self.button.set_size(width, height)

    def set_position(self, x, y):
        self.title.set_position(x, y)
        self.button.set_position(x + self.title.width() -
                                 self.button.width() - 1, y + 1)

    def move(self, x, y):
        self.title.move(x, y)
        self.button.move(x, y)

    def height(self):
        return self.title.height()

    def width(self):
        return self.title.width()

    def x(self):
        return self.title.x()

    def y(self):
        return self.title.y()

    def check_point_in(self, pos):
        return self.button.check_point_in(pos)

    def draw(self, obj):
        self.title.draw(obj)
        self.button.draw(obj)


class PaintTitelAndContents():
    def __init__(self, title=" ", contents=""):
        self.title = PaintChatTitleBox(title)
        self.contents = PaintLabelBox(contents)

    def mouse_down(self, pos):
        return self.title.mouse_down(pos)
        

    def mouse_up(self, pos):
        self.title.mouse_up(pos)

    def mouse_hover(self, pos):
        self.title.mouse_hover(pos)

    def set_contents_height_fit(self, fit_flag):
        self.contents.set_hieght_fit(fit_flag)

    def set_title(self, text):
        self.title.set_title_text(text)

    def get_title(self):
        self.title.get_title_text()

    def set_contents(self, text):
        self.contents.set_text(text)

    def get_contents_text(self):
        return self.contents.get_text()

    def append_text(self, text):
        self.contents.append_text(text)

    def set_title_font_size(self, size):
        if size < 7:
            size = 7
        self.title.set_font_size(size)

    def set_contents_font_size(self, size):
        if size < 7:
            size = 7
        self.contents.set_font_size(size)

    def set_contents_height(self, height):
        self.contents.set_size(self.contents.width(), height)

    def add_contents_height(self, height):
        self.contents.set_size(self.contents.width(),
                               self.contents.height() + height)

    def get_contents_height(self):
        return self.contents.height()

    def get_title_height(self):
        return self.title.height()

    def set_width(self, width):

        self.title.set_title_size(width, self.title.height())
        self.contents.set_size(width, self.contents.height())
        self.contents.set_position(self.title.x(),
                                   self.title.y() + self.title.height())

    def set_position(self, x, y):
        self.title.set_position(x, y)
        self.contents.set_position(self.title.x(),
                                   self.title.y() + self.title.height())

    def set_contents_border(self, bsize):
        self.contents.set_border(bsize)

    def get_contents_border(self):
        return self.contents.get_border()

    def move(self, x, y):
        self.title.move(x, y)
        self.contents.move(x, y)

    def height(self):
        return self.title.height() + self.contents.height()

    def width(self):
        return self.title.width()

    def x(self):
        return self.title.x()

    def y(self):
        return self.title.y()

    def check_point_in(self, pos):
        return self.title.check_point_in(pos)

    def draw(self, obj):
        self.title.draw(obj)
        self.contents.draw(obj)


class PaintChatNode():
    NOMAL_BLOCK = 0
    PROGRAM_BLOCK = 1

    def __init__(self,
                 title=" ",
                 contents=" "):
        self.main = PaintTitelAndContents(title, "")
        self.blok_type = self.NOMAL_BLOCK
        self.main.set_contents_height_fit(False)
        self.contents_text = ""
        self.contents = []
        self.extract_code_block(contents)

    def mouse_down(self, pos):

        if self.main.mouse_down(pos):
            pyperclip.copy(self.contents_text)
#            print("self.main.get_text()", self.contents_text)

        for obj in self.contents:
            if PaintTitelAndContents is type(obj):
                if obj.mouse_down(pos):
                    pyperclip.copy(obj.get_contents_text())
#                    print("obj.get_text()", obj.get_contents_text())
            #copy

    def mouse_up(self, pos):
        self.main.mouse_up(pos)
        for obj in self.contents:
            if PaintTitelAndContents is type(obj):
                obj.mouse_up(pos)

    def mouse_hover(self, pos):
        self.main.mouse_hover(pos)

    def set_title(self, text):
        self.main.set_title(text)

    def set_contents(self, text):
        self.contents = []
        self.extract_code_block(text)
        self.main.set_contents(text)

    def append_text(self, text):
        self.main.append_text(text)
        text = self.main.get_contents_text()
        self.contents = []
        self.extract_code_block(text)

    def set_position(self, x, y):
        self.main.set_position(x, y)
        for obj in self.contents:
            if PaintTitelAndContents is type(obj):
                self.__set_titelandcontents__width_and_position(obj)
            else:
                self.__set_label_width_and_position(obj)
        self.__fit_height()

    def __set_label_width_and_position(self, label):
        label.set_width(self.width() - self.main.get_contents_border()*2)
        # x位置の設定 yは後で合わせる
        label.set_position(self.x() + self.main.get_contents_border(),
                           self.y() + self.main.get_contents_border())

    def __set_titelandcontents__width_and_position(self, tc):
        tc.set_width(self.width() - self.main.get_contents_border()*4)
        # x位置の設定 yは後で合わせる
        tc.set_position(self.x() + self.main.get_contents_border()*2,
                        self.y() + self.main.get_contents_border())

    def set_width(self, width):
        self.main.set_width(width)

        for obj in self.contents:
            if PaintTitelAndContents is type(obj):
                self.__set_titelandcontents__width_and_position(obj)
            else:
                self.__set_label_width_and_position(obj)
        self.__fit_height()

    def set_contents_height(self, height):
        self.main.set_contents_height(height)

    def height(self):
        return self.main.height()

    def width(self):
        return self.main.width()

    def x(self):
        return self.main.x()

    def y(self):
        return self.main.y()

    def append_contents_ojbect(self, obj):
        self.contents.append(obj)
        if 0 == len(self.contents):
            obj.set_position(self.main.x(), self.main.get_title_height())
        self.main.add_contents_height(obj.height())

    def extract_code_block(self, text):  # コードブロックの抽出
        self.contents_text = text
        lines = text.split("\n")

        program_name = ""
        text_buf = ""
        for line in lines:
            ""
            line = line.rstrip()
            if self.NOMAL_BLOCK == self.blok_type:
                # nomal blockの時
                # 正規表現でコードブロックを検索
                match = re.search(r'^```(.*?)', line, re.DOTALL)
                if match:
                    self.blok_type = self.PROGRAM_BLOCK
                    program_name = line[3:]
                    # ここまでのテキストデータをラベルとして追加
                    text_buf = text_buf.rstrip()
                    label = PaintLabelBox(text_buf)
                    label.set_border(0)

                    self.__set_label_width_and_position(label)
                    self.append_contents_ojbect(label)
                    # 初期化
                    self.last_text = text_buf
                    text_buf = ""
                    continue
            else:
                # purogram blockの時
                match = re.search(r'```$', line, re.DOTALL)  # 正規表現でコードブロックを検索

                if match:
                    self.blok_type = self.NOMAL_BLOCK
                    # ここまでのテキストデータをプログラムとして追加
                    text_buf = text_buf.rstrip()
                    program = PaintTitelAndContents(program_name, text_buf)
                    # program = program.get_main_box()
                    self.__set_titelandcontents__width_and_position(program)
                    self.append_contents_ojbect(program)
                    # 初期化
                    program_name = ""
                    self.last_text = text_buf
                    text_buf = ""
                    continue
            text_buf += line + "\n"

        if self.PROGRAM_BLOCK == self.blok_type:
            # プログラムブロックを終了する前に終了した
            # program block
            text_buf = text_buf.rstrip()
            program = PaintTitelAndContents(program_name, text_buf)
            self.__set_titelandcontents__width_and_position(program)

            self.append_contents_ojbect(program)
            self.blok_type = self.NOMAL_BLOCK
        else:
            # ノーマルブロックの状態で終了した。
            # nomal block
            text_buf = text_buf.rstrip()
            label = PaintLabelBox(text_buf)
            label.set_border(0)
            self.__set_label_width_and_position(label)

            self.append_contents_ojbect(label)

        self.last_text = text_buf
        self.__fit_height()

    def __fit_height(self):
        h = self.main.get_title_height()
        count = 0
        for obj in self.contents:
            count += 1
            obj.set_position(obj.x(),
                             self.y() + h +
                             self.main.get_contents_border() * count)

            h += obj.height()

        self.set_contents_height(h-self.main.get_title_height() +
                                 self.main.get_contents_border()*count*2)

    def draw(self, obj):
        self.main.draw(obj)
        for pobj in self.contents:
            pobj.draw(obj)


class PaintChatThread():
    def __init__(self):
        self.nodes = []
        self.draw_area = QRect(0, 0, 100, 1000)
        self.draw_start_index = 0
        self.node_space = 10

    def mouse_down(self, pos):
        pos = QPoint(self.draw_area.x() + pos.x(),
                     self.draw_area.y() + pos.y())
        for node in self.nodes:
            node.mouse_down(pos)

    def mouse_up(self, pos):
        pos = QPoint(self.draw_area.x() + pos.x(),
                     self.draw_area.y() + pos.y())
        for node in self.nodes:
            node.mouse_up(pos)

    def mouse_hover(self, pos):
        pos = QPoint(self.draw_area.x() + pos.x(),
                     self.draw_area.y() + pos.y())
        for node in self.nodes:
            node.mouse_hover(pos)

    def append(self, node):
        self.nodes.append(node)
        if 1 < len(self.nodes):
#            self.nodes[-1].set_position(
#                self.nodes[-2].x(),
#                self.nodes[-2].y() + self.nodes[-2].height() + self.node_space * (len(self.nodes)-1))
            self.nodes[-1].set_position(
                self.nodes[-2].x(),
                self.nodes[-2].y() + self.nodes[-2].height() + self.node_space)
            
    def append_node(self, title, contents):
        

        node = PaintChatNode(title, contents)
        if 0 < len(self.nodes):
            n_bottom = self.nodes[-1].y() + self.nodes[-1].height()
            da_bottom = self.draw_area_y() + self.draw_area_height()
            width = self.width()
            node.set_width(width)

        self.append(node)
        if 1 < len(self.nodes):
#            print("n_bottom",  n_bottom)
#            print("da_bottom", self.draw_area_y() + self.draw_area_height())
            if n_bottom == da_bottom:
                self.set_draw_area_y(self.nodes[-1].y() + self.nodes[-1].height() - self.draw_area_height())
            elif n_bottom <= da_bottom:
                post_bottom = self.draw_area_y() + self.draw_area_height()
                if da_bottom <= post_bottom:
                    self.set_draw_area_y(self.nodes[-1].y() + self.nodes[-1].height() - self.draw_area_height())
    
    def append_text(self, text):
        pre_draw_bottom = self.draw_area_y() + self.draw_area_height()
        pre_contentts_height = self.height()
        if 0 < len(self.nodes):
            self.nodes[-1].append_text(text)

        self.__set_draw_start_index()
        if pre_draw_bottom == pre_contentts_height:
            self.set_draw_area_y(self.height()-self.draw_area_height())
        elif pre_contentts_height <= pre_draw_bottom:
            post_bottom = self.draw_area_y() + self.draw_area_height()
            if pre_draw_bottom <= post_bottom:
                self.set_draw_area_y(self.height()-self.draw_area_height())
    
    def set_last_node_text(self, text):
        pre_draw_bottom = self.draw_area_y() + self.draw_area_height()
        pre_contentts_height = self.height()
        if 0 < len(self.nodes):
            
            self.nodes[-1].set_contents(text)
        self.__set_draw_start_index()
        if pre_draw_bottom == pre_contentts_height:
            self.set_draw_area_y(self.height()-self.draw_area_height())

    def height(self):
        if 0 < len(self.nodes):
            return self.nodes[-1].y() + self.nodes[-1].height()
        else:
            return 0

    def width(self):
        if 0 < len(self.nodes):
            return self.nodes[-1].width()
        else:
            return 0

    def draw_area_height(self):
        return self.draw_area.height()

    def draw_area_y(self):
        return self.draw_area.y()

    def set_draw_area_y(self, y):

        self.set_draw_area(self.draw_area.x(),
                           y,
                           self.draw_area.width(),
                           self.draw_area.height())

    def set_draw_area(self, x, y, width, height):
        pre_y = self.draw_area.y()
        if y < 0:
            y = 0
        self.draw_area.setRect(x, y, width, height)
        reverse = True
        if pre_y < self.draw_area.y():
            reverse = False
        self.__set_draw_start_index(reverse)

    def set_width(self, w):
        y_deff_buf = -1

        y_buf = -1
        change_flag = True
        for i in range(len(self.nodes)):
            # 一番上の 描画 ノードとの関係を固定
            if 0 == i:
                y_deff_buf = self.draw_area.y() - self.nodes[i].y()
                change_flag = True
            self.nodes[i].set_width(w)
            if 0 < i:
                self.nodes[i].set_position(
                    self.nodes[i - 1].x(),
                    self.nodes[i - 1].y() + self.nodes[i-1].height() + self.node_space)
                # 一番上の 描画 ノードとの関係を固定
                if 0 <= self.draw_area.y() - self.nodes[i].y():
                    y_deff_buf = self.draw_area.y() - self.nodes[i].y() 
                    change_flag = True

            if change_flag:
                y_buf = self.nodes[i].y()
            change_flag = False
        # 描画範囲を設定
        if self.nodes[-1].y()+self.nodes[-1].height() - self.draw_area.height() < y_buf + y_deff_buf:
            y_deff_buf = (self.nodes[-1].y()+self.nodes[-1].height() - self.draw_area.height())
            y_buf = 0
        if  y_buf + y_deff_buf < 0:
            y_buf = 0
            y_deff_buf = 0
        self.draw_area.setRect(
            self.draw_area.x(),
            y_buf + y_deff_buf,
            w,
            self.draw_area.height())

    def move_vertical(self, y):
        self.draw_area.setRect(
            self.draw_area.x(),
            self.draw_area.y() + y,
            self.draw_area.width(),
            self.draw_area.height())

        self.__set_draw_start_index(y < 0)

    def __set_draw_start_index(self, reverse=False):
        if 0 < len(self.nodes):
            if len(self.nodes) <= self.draw_start_index:
                self.draw_start_index = len(self.nodes) -1

            if reverse:
                for i in range(self.draw_start_index, -1, -1):
                    if self.draw_area.y() < self.nodes[i].y() +self.nodes[i].height():
                        if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
                            self.draw_start_index = i

                    if self.nodes[i].y() + self.nodes[i].height() < self.draw_area.y():
                        break
            else:
                for i in range(self.draw_start_index, len(self.nodes)):
                    if self.draw_area.y() < self.nodes[i].y() + self.nodes[i].height():
                        if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
                            self.draw_start_index = i
                            return

    def draw(self, obj):
        if 0 < len(self.nodes):            
            for i in range(self.draw_start_index, len(self.nodes)):
                if self.draw_area.y() < self.nodes[i].y() + self.nodes[i].height():
                    if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
                        # draw area内のノードだけ描画
                        pre_x = self.nodes[i].x()
                        pre_y = self.nodes[i].y()
                        self.nodes[i].set_position(self.nodes[i].x()-self.draw_area.x(),
                                                   self.nodes[i].y()-self.draw_area.y())
                        self.nodes[i].draw(obj)
                        self.nodes[i].set_position(pre_x,
                                                   pre_y)

                if self.draw_area.y() + self.draw_area.height() < self.nodes[i].y():
                    return

    def get_draw_bottom(self):
        return self.draw_area.x() + self.draw_area.height()

class PaintArrowSlider():
    NO_STATE = 0
    CLICK_UP = 1
    CLICK_DOWN = 2
    CLICK_SLIDER = 3
    CLICK_SLIDER_UP = 4
    CLICK_SLIDER_DOWN = 5
    def __init__(self):
        self.up_button = PaintButton("▲")
        self.down_button = PaintButton("▼")
        self.slider = PaintButton(" ")
        self.slider.set_hieght_fit(False)
        self.state = self.NO_STATE
        self.slider_min = 10
        self.rect = QRect(0, 0, 10, 100)
        self.border = 0
        self.y_position_ratio = 1
        self.slider_height_ratio = 1
        self.set_up_down_button_size(30, 30)
        self.slider.set_width(30)
        self.set_slider_height(1)
        self.set_slider_position(0)
        self.pre_mouse_position = QPoint(0, 0)
        self.slider.set_clicked_back_color(QColor(224,224,224))
    def x(self):
        return self.up_button.x()

    def y(self):
        return self.up_button.y()

    def width(self):
        return self.up_button.width()

    def height(self):
        return self.rect.height()

    def set_up_down_button_size(self, width, height):
        now_height = self.height()

        self.rect = QRect(self.rect.x(),
                          self.rect.y(),
                          width,
                          self.rect.height())
        self.up_button.set_size(width, height)
        self.down_button.set_size(width, height)
        if now_height < height * 3:
            now_height = height*3

        self.set_height(now_height)

    def set_slider_position(self, pos_ratio):
        if 1 < pos_ratio:
            pos_ratio = 1
        if pos_ratio < 0:
            pos_ratio = 0
        self.y_position_ratio = pos_ratio
        self.slider.set_position(
            self.slider.x(),
            self.up_button.height() + (self.down_button.y() -
                                       (self.up_button.y() +
                                        self.up_button.height() +
                                        self.slider.height()))
            * self.y_position_ratio)

    def set_position(self, x, y):
        self.up_button.set_position(x, y)
        # self.slider.set_position(x, y)
        self.down_button.set_position(
            x,
            self.rect.height() - self.down_button.height())
        self.slider.set_position(x, y)
        self.set_slider_position(self.y_position_ratio)
        self.rect = QRect(x, y, self.rect.width(), self.rect.height())

    def mouse_down(self, pos):
        pos = QPoint(pos.x(),pos.y())
       

        if self.up_button.mouse_down(pos):
            self.state = self.CLICK_UP
        elif self.down_button.mouse_down(pos):
            self.state = self.CLICK_DOWN
        elif self.slider.mouse_down(pos):
            self.state = self.CLICK_SLIDER
            self.pre_mouse_position = pos
        elif self.rect.contains(pos):
            if pos.y() < self.slider.y():
                self.state = self.CLICK_SLIDER_UP
            elif self.slider.y() + self.slider.height() < pos.y():
                self.state = self.CLICK_SLIDER_DOWN
        return self.state
    
    def mouse_up(self, pos):
        self.state = self.NO_STATE
        self.up_button.mouse_up(pos)
        self.down_button.mouse_up(pos)
        self.slider.mouse_up(pos)

    def mouse_hover(self, pos):
        self.up_button.mouse_hover(pos)
        self.down_button.mouse_hover(pos)
        self.slider.mouse_hover(pos)

    def mouse_move(self, pos):
        vm = QPoint(int(pos.x() - self.pre_mouse_position.x()),
                    int(pos.y() - self.pre_mouse_position.y()))
        if self.state == self.CLICK_SLIDER:
            if vm.y() < 0:
                if self.up_button.y() + self.up_button.height() <\
                      self.slider.y() + vm.y():
                    self.slider.set_position(
                        self.slider.x(),
                        self.slider.y() + vm.y())
                else:
                    self.slider.set_position(
                        self.slider.x(),
                        self.up_button.y() + self.up_button.height())
            else:
                if self.slider.y() + self.slider.height() + vm.y() <\
                      self.down_button.y():
                    self.slider.set_position(
                        self.slider.x(),
                        self.slider.y() + vm.y())
                else:
                    self.slider.set_position(
                        self.slider.x(),
                        self.down_button.y()-self.slider.height())
            self.pre_mouse_position = pos

            buf = self.slider.y()-(self.up_button.y()+self.up_button.height() )
            self.y_position_ratio = (
                buf/
                (self.down_button.y()  -
                    (self.slider.height() +
                     self.up_button.y() +
                     self.up_button.height()))
                )
            return vm

    def get_state(self):
        return self.state
    
    def set_width(self, width):
        self.rect = QRect(self.rect.x(), self.rect.y(),
                          width, self.rect.height())
        self.up_button.set_size(width, self.up_button.height())
        self.down_button.set_size(width, self.down_button.height())
        self.slider.set_size(width, self.slider.height())

    def set_height(self, height):
        self.rect = QRect(self.rect.x(), self.rect.y(),
                          self.rect.width(), height)
        ratio = self.get_slider_ratio()
        self.down_button.set_position(self.down_button.x(),
                                      height-self.down_button.height())
        slider_area_height = self.down_button.y() - (self.up_button.height() + self.up_button.y())
        self.slider.set_size(self.slider.width(), int(slider_area_height*ratio))

        self.set_slider_position(self.y_position_ratio)

    def set_rect(self, x , y, width, height):
        self.set_position(x, y)
        self.set_width(width)
        self.set_height(height)

    def get_rect(self):
        return self.rect

    def get_slider_position_ratio(self):
        return self.y_position_ratio

    def get_slider_ratio(self):
        s = self.up_button.y() + self.up_button.height() - self.down_button.y()
        return s / self.slider.height()

    def set_slider_height(self, ratio):
        if 1 <= ratio:
            ratio = 1
        s = self.down_button.y() - (self.up_button.y() + self.up_button.height())
        s = s * ratio
        self.slider.set_size(self.slider.width(), int(s))

        if int(s) < self.slider_min:
            self.slider.set_size(self.slider.width(), self.slider_min)
        self.slider_height_ratio = ratio

    def __draw_back(self, obj):
        # スライダー部分の背景
        painter = QPainter(obj)
        painter.setPen(QPen(QColor(128, 128, 128), self.border))  # 枠線
        painter.setBrush(QBrush(QColor(128, 128, 128)))  # 塗りつぶし

        rect = QRect(
            self.up_button.x() + 1,
            self.up_button.y()+self.up_button.height(),
            self.up_button.width() - 4,
            self.down_button.y() - (self.up_button.y() + self.up_button.height()))
        # ボタン類の描画
        painter.drawRect(rect)

    def draw(self, obj):
        self.__draw_back(obj)
        self.up_button.draw(obj)
        self.down_button.draw(obj)
        self.slider.draw(obj)


class ChatPaintWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Graphics Text with Size and Newline - Debug Version")
        self.font = QFont("Arial", 50)
        self.name = QLabel("")
        self.name.setMaximumWidth(1500)
        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.name)
        self.setLayout(self.layout)
        self.chat_thread = PaintChatThread()
        self.slider = PaintArrowSlider()

        self.update_font_size(20)
        self.chat_thread.append_node(f"ai", "作業指示をしてください。") #これがないとウィンドが表示されず終了してしまう。

        # テスト用
#        for i in range(100):
#            self.chat_thread.append_node(f"user1_{i}", "testデータです。aaa1aaaa2aaaaaa3aaaaa4aaa5aaaaaa6aaaaa7aaaa8aa")
#            self.chat_thread.append_node(f"ai1_{i}", "わかりました。\n```python\nprint('test')\na=2\nb=4\n```\n 終わりです。sssssssssssssssssssssssssssssssssssssssssssssssssssssss")
#            self.chat_thread.append_node(f"user2_{i}", "どうなっているの?\nok")

    def paintEvent(self, event):

        self.chat_thread.draw(self)
        self.slider.draw(self)

    def __page_up(self, deff):
        self.chat_thread.move_vertical(-deff)
        if self.chat_thread.draw_area_y() < 0:
            self.chat_thread.set_draw_area_y(0)
        self.slider.set_slider_position(self.chat_thread.draw_area_y()/(self.chat_thread.height() - self.chat_thread.draw_area_height()))

    def __page_down(self, deff):
        self.chat_thread.move_vertical(deff)
        if self.chat_thread.height() < self.chat_thread.draw_area_y() + self.chat_thread.draw_area_height():
            self.chat_thread.set_draw_area_y(
                self.chat_thread.height() - self.chat_thread.draw_area_height())
        self.slider.set_slider_position(self.chat_thread.draw_area_y()/(self.chat_thread.height() - self.chat_thread.draw_area_height()))

    def mousePressEvent(self, event: QMouseEvent):

        self.chat_thread.mouse_down(event.position())
        res = self.slider.mouse_down(event.position())

        dh = self.chat_thread.draw_area_height()

        if res == self.slider.CLICK_UP:
            self.__page_up(int(dh/10))

        elif res == self.slider.CLICK_DOWN:
            self.__page_down(int(dh/10))

        elif res == self.slider.CLICK_SLIDER_UP:
            self.__page_up(int(dh/10*9))

        elif res == self.slider.CLICK_SLIDER_DOWN:
            self.__page_down(int(dh/10*9))

        self.chat_thread.height()
        self.chat_thread.get_draw_bottom()
        self.update()

    def mouseReleaseEvent(self, event: QMouseEvent):
        self.chat_thread.mouse_up(event.position())
        self.slider.mouse_up(event.position())
        self.update()

    def mouseMoveEvent(self, event: QMouseEvent):
        self.chat_thread.mouse_hover(event.position())
        self.slider.mouse_hover(event.position())
        self.slider.mouse_move(event.position())
        res = self.slider.get_state()

        if res == self.slider.CLICK_SLIDER:

            pr = self.slider.get_slider_position_ratio()

            self.chat_thread.set_draw_area_y(int((self.chat_thread.height() - self.chat_thread.draw_area_height())*pr))
        self.update()

    def keyPressEvent(self, event: QKeyEvent):
        key = event.key()
        dh = self.chat_thread.draw_area_height()

        if key == Qt.Key_PageUp:
            self.__page_up(int(dh/10*9))
        elif key == Qt.Key_PageDown:
            self.__page_down(int(dh/10*9))
        elif key == Qt.Key_Up:
            self.__page_up(int(dh/10))
            # 上矢印キーが押されたときの処理
        elif key == Qt.Key_Down:
            self.__page_down(int(dh/10))

        self.chat_thread.height()
        self.chat_thread.get_draw_bottom()
        self.update()

    def wheelEvent(self, event: QWheelEvent):
        # event.angleDelta().y() は、垂直方向の回転量を返します。
        # 正の値は上方向への回転、負の値は下方向への回転を示します。
        delta = event.angleDelta().y()
        dh = self.chat_thread.draw_area_height()

        # 回転量に応じて処理を行います。
        if delta > 0:
            # 上方向への回転時の処理
            self.__page_up(int(dh/10))
        else:

            self.__page_down(int(dh/10))
            # 下方向への回転時の処理
        self.chat_thread.height()
        self.chat_thread.get_draw_bottom()
        self.update()

    def update_font_size(self, font_size=None):

        if font_size is None:
            font_size = self.slider.value()
        self.font.setPointSize(font_size)
        self.update()

    def __update_slider(self):
        self.slider.set_height(self.height())

        self.slider.set_position(self.width()-self.slider.width(), 0)
        self.slider.set_slider_height(self.chat_thread.draw_area_height()/self.chat_thread.height())
        self.slider.get_slider_position_ratio()
        self.slider.set_slider_position(self.chat_thread.draw_area_y()/(self.chat_thread.height() - self.chat_thread.draw_area_height()))
        self.slider.get_slider_ratio()

    def resizeEvent(self, event):
        da_y = self.chat_thread.draw_area_y()
        if self.chat_thread.height() < self.height():
            da_y = 0
        else:
            if self.chat_thread.height() <= da_y + self.height():
                da_y = self.chat_thread.height() - self.height()
            elif self.chat_thread.height() <= da_y + self.chat_thread.draw_area_height():
                da_y = self.chat_thread.height() - self.height()
        self.chat_thread.height()
        self.chat_thread.set_draw_area(
            -10, da_y, self.width()-self.slider.width() - 10, self.height())
        if self.chat_thread.width() != self.width()-self.slider.width()-20:
            self.chat_thread.set_width(self.width()-self.slider.width()-20)

        self.__update_slider()
        self.update()

    def append_node(self, title, text):
        self.chat_thread.append_node(title, text)
        self.__update_slider()
        self.update()

    def append_text(self, text):
        self.chat_thread.append_text(text)
        self.__update_slider()
        self.update()

    def set_last_node_text(self, text):
        self.chat_thread.set_last_node_text(text)
        self.__update_slider()
        self.update()


class ChatWidget(QWidget):
    update_signal = Signal(str, str)  # title, text用

    def __init__(self, agent):
        super().__init__()

        self.chat_thread = ChatPaintWidget()
        # title
        self.title = QLabel("PaintGUI\r\n")
        self.title.setFixedHeight(30)
        self.title.setFont(QFont("Arial", 30))
        self.title.setAlignment( Qt.AlignCenter)
        self.sep1 = QLabel("-------------------------------------------------------------------------------------------------")
        self.sep1.setFixedHeight(20)
        self.sep1.setAlignment(Qt.AlignCenter)
        font = QFont("Arial", 20)
        font.setBold(True) # 太字に設定
        self.sep1.setFont(font)
        self.sep2 = QLabel("-------------------------------------------------------------------------------------------------")
        self.sep2.setFixedHeight(20)
        self.sep2.setAlignment(Qt.AlignCenter)
        self.sep2.setFont(font)

        # input 入力欄
        self.input_edit = QTextEdit()
        self.input_edit.setMaximumHeight(100)
        self.send_button = QPushButton("Send")
        self.send_button.clicked.connect(self.send_message)

        # Layout for input area
        input_layout = QHBoxLayout()
        input_layout.addWidget(self.input_edit)
        input_layout.addWidget(self.send_button)

        # Main layout
        main_layout = QVBoxLayout(self)
        main_layout.addWidget(self.title)
        main_layout.addWidget(self.sep1)
        main_layout.addWidget(self.chat_thread)
        main_layout.addWidget(self.sep2)
        main_layout.addLayout(input_layout)
        self.update_signal.connect(self._append_node_handler)
        self.agent = agent
        self.thread = None

    def send_message(self):
        message = self.input_edit.toPlainText()
        if message:
#############################################################
            self.chat_thread.append_node("user", message)
            self.input_edit.clear()

            # ワーカースレッドを作成して処理を移す
            self.thread = QThread()
            self.worker = AppendNodeWorker(self.agent, message)
            self.worker.moveToThread(self.thread)

            # スレッドが開始したらワーカーのrunを実行
            self.thread.started.connect(self.worker.run)
            # 処理が終わったら結果を受け取る
            self.worker.finished.connect(self.on_response_received)
            # スレッド終了時にスレッドを削除
            self.worker.finished.connect(self.thread.quit)
            self.worker.finished.connect(self.worker.deleteLater)
            self.thread.finished.connect(self.thread.deleteLater)

            # スレッド開始
            self.thread.start()
    def on_response_received(self, response):
        # レスポンスを受け取って表示
        #self.append_node("agent", response)
        pass
    def append_node(self, title, text):
        #self.chat_thread.append_node(title, text)
        self.update_signal.emit(title, text)
#        QCoreApplication.postEvent(self, AppendNodeEvent(title, text))
    def _append_node_handler(self, title, text):
        # 実際の更新処理
        self.chat_thread.append_node(title, text)
        self.update()  # QWidgetのupdateメソッド
        QApplication.processEvents()  # イベント即時処理

    def update(self):
        super().update()
        QApplication.processEvents()  # イベントを即座に処理
        
    def append_text(self, text):
#        self.chat_thread.append_text(text)
#        self.update()
        QCoreApplication.postEvent(self, AppendTextEvent(text))

    def set_last_node_text(self, text):
        # self.chat_thread.set_last_node_text(text)
        QCoreApplication.postEvent(self, SetLastNodeText(text))
        self.update()

    def set_agent(self, agent):
        self.agent = agent
        # self.update()

    def customEvent(self, event):
        if event.type() == AppendNodeEvent.EventType:
            self.chat_thread.append_node(event.name, event.text)
            self.update()
        elif  event.type() == AppendTextEvent.EventType:
            self.chat_thread.append_text(event.text)
            self.update()
        elif   event.type() == SetLastNodeText.EventType:
            self.chat_thread.set_last_node_text(event.text)
            self.update()


class AppendNodeWorker(QObject):
    finished = Signal(str)  # 処理が終わったときに結果を送信するシグナル

    def __init__(self, agent, message):
        super().__init__()
        self.agent = agent
        self.message = message

    def run(self):
        # 重い処理をここで行う
        response = self.agent.get_respons(self.message)
        self.finished.emit(response)  # 結果をシグナルで送信


class AppendNodeEvent(QEvent):
    # EventType = QEvent.registerEventType()
    EventType = QEvent.Type(QEvent.registerEventType())  # Use QEvent.Type()

    def __init__(self, name, text):
        super().__init__(AppendNodeEvent.EventType)
        self.name = name
        self.text = text


class AppendTextEvent(QEvent):
    # EventType = QEvent.registerEventType()
    EventType = QEvent.Type(QEvent.registerEventType())  # Use QEvent.Type()

    def __init__(self, text):
        super().__init__(AppendTextEvent.EventType)

        self.text = text

class SetLastNodeText(QEvent):
    # EventType = QEvent.registerEventType()
    EventType = QEvent.Type(QEvent.registerEventType())  # Use QEvent.Type()

    def __init__(self, text):
        super().__init__(SetLastNodeText.EventType)

        self.text = text


class MyStreamlitCallbackHandler(BaseCallbackHandler):

    def __init__(self, container):
        self.container = container

    def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
        self.container.append_text(token)
        # super().on_llm_new_token(token, kwargs)


g_app = QApplication(sys.argv)
g_main_widget = QWidget()
g_main_layout = QVBoxLayout()
#    main_layout = QHBoxLayout(main_widget)
g_my_widget = ChatWidget(None)
g_my_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # 重要な変更点

g_my_widget.setMinimumHeight(400)
g_my_widget.setFixedWidth(1000)
# スライダーとラベルのためのウィジェット

g_main_layout.addWidget(g_my_widget)

g_main_layout.addStretch(0)

g_my_widget.setLayout(g_main_layout)


def excute(agent):
    global g_my_widget
    global g_app
    g_my_widget.set_agent(agent)
    g_my_widget.show()

    sys.exit(g_app.exec())

def set_agent(agent):
    g_my_widget.set_agent(agent)
def update():
    g_my_widget.update()
def append_node(name,text):
    g_my_widget.append_node(name, text)

def set_last_node_text(text):
    g_my_widget.set_last_node_text( text)

def get_stc_handler():
    global g_my_widget
    return MyStreamlitCallbackHandler(g_my_widget)
def get_mywidget():
    return g_my_widget