Ollamaのモデルの比較用プログラム:ソースコード:GUI:PaintGUI.py

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
import time
import inspect
from tools.exception import InterruptedException as InterruptedException


class PaintLabelBox():
    def __init__(self, text=""):
        self.rect = QRect(0, 0, 150, 50)
        self.font = QFont("Arial", 15)
        self.font_name = "Arial" # Store font name
        self.font_size = 15 # フォントサイズを保持
        self.span = 1
        self.text = text
        self.align = Qt.AlignLeft
        self.height_fit = True
        self.border = 5
        self.fit_text_lines = []
        self.__set_text_rect()

        self.__fit_text()

    def set_font_size(self, size):
        self.font_size = size
        self.font = QFont(self.font_name, self.font_size)
        self.__fit_text() # フォントサイズ変更後に再計算

    def __set_text_rect(self):
        if self.rect.width() < ((self.span+self.border) * 2) or self.rect.height() < ((self.span+self.border) * 2): # 最小サイズチェック
            return
        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() # サイズ変更時に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.__set_text_rect() # 位置変更時にもtext_rectを更新
        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 get_last_line_text(self):
        if 0 < len(self.fit_text_lines) :
            return self.fit_text_lines[-1]
        return ""
    
    def append_text(self, text):
        self.text += text
        #print("DEBUG: text", text)
        self.__fit_text_last_line(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):
        if not hasattr(self, 'text_rect') or self.text_rect.width() <= 0: # text_rectが未初期化または不正な場合は何もしない
            return
        self.fit_text_lines = []
        metrics = QFontMetrics(self.font)
        
        lines = self.text.split("\n")
        result_text = ""

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

    def __add_new_line(self, metrics, line):

        if self.text_rect.width() <= 0: # text_rectの幅が不正なら元の行を返す
            return 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
            # 同じとき完了
        self.fit_text_lines[-1] = pre_text
        result_text = pre_text
        if 0 < sep_num:#文字数が0の時はこの先の処理を行ってはいけない。
        # 残った文字列が表示領域より大きいとき再帰的の呼び出す。
            line_buf = line[sep_num:]
            text_width = metrics.horizontalAdvance(line_buf)

            if self.text_rect.width() < text_width:
                #print(f"DEBUG: PaintLabelBox.__add_new_line line_buf",metrics, line_buf)
                #print(f"DEBUG: PaintLabelBox.__add_new_line text_width",self.text_rect.width(), text_width)
                result_text += "\n" + self.__add_new_line(metrics, line_buf)
#                result_text += self.__add_new_line(metrics, line_buf)
                self.fit_text_lines.append(line_buf)
            else:
                result_text += "\n" + line_buf
                self.fit_text_lines.append(line_buf)
        else: # sep_num <= 0 の場合
            result_text = line
            self.fit_text_lines[-1] = line
        return result_text
    
    def __fit_text_last_line(self,append_text):
        if not hasattr(self, 'text_rect') or self.text_rect.width() <= 0:# # text_rectが未初期化または不正な場合は何もしない
            #print("DEBUG: self.height_fit, text_rect.height", self.height_fit,self.text_rect.height())
            return        
        metrics = QFontMetrics(self.font)
        
        if len(append_text) <= 0:
            return

        if 0 == len(self.fit_text_lines):
            self.fit_text_lines.append("")

        line = self.fit_text_lines[-1] + append_text
        result_text = ""

        text_width = metrics.horizontalAdvance(line)

        # 初めの大きさの確認で問題なければ何もしない。
        if text_width < self.text_rect.width():
            # 開業を付与することでresult_text[1:]との整合性をとる。
 #           result_text +=  "\n" + line
            result_text +=  "\n" + line
            self.fit_text_lines[-1] = line
        
        else:            
#            result_text += "\n" + self.__add_new_line(metrics, line)
            result_text +=  "\n" + self.__add_new_line(metrics, line)
        # 必ず開業ふぁふよされるので最初の開業を刑したものを付け加える。
        #print("result_text", result_text)
        self.fit_text += result_text[1:]
        
        self.__fit_height()

    def __fit_height(self):
        if self.height_fit:
            metrics = QFontMetrics(self.font)
#            original_height = self.rect.height()            
            line_height = metrics.boundingRect(self.fit_text).height()  # 行の高さ
#            line_num = len(self.fit_text.split("\n"))
            line_num = len(self.fit_text_lines)
            new_height = (line_height+metrics.leading())*line_num + (self.span+self.border)*2
            self.rect = QRect(self.rect.x(), self.rect.y(),
                              self.rect.width(),
                              new_height)
#            final_height = self.rect.height()
#            if final_height < original_height:
#                if 100 < len(self.text) and final_height < 100:
#                    print(f"DEBUG WARNING: Height DECREASED in {self.__class__.__name__}.{inspect.currentframe().f_code.co_name}")
#                    print(f"  Original: {original_height}, Final: {final_height}, Diff: {final_height - original_height}")
#                    print("  Call Stack:")
#                    for frame_info in inspect.stack():
#                        print(f"    File: {frame_info.filename}, Line: {frame_info.lineno}, Function: {frame_info.function}")
#                        if frame_info.code_context:
#                            print(f"      Code: {frame_info.code_context[0].strip()}")

            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):
#        original_contents_height = self.contents.height()        
        self.contents.set_size(self.contents.width(), height)
#        final_contents_height = self.contents.height()
#        if final_contents_height < original_contents_height:
#            if 100 < len(self.contents.get_text()) and final_contents_height < 100:
#                print(f"DEBUG WARNING: Contents height DECREASED in {self.__class__.__name__}.{inspect.currentframe().f_code.co_name}")
#                print(f"  Target: self.contents, Original: {original_contents_height}, Final: {final_contents_height}, Diff: {final_contents_height - original_contents_height}")
#                print(f"  Called with height: {height}")
#                print("  Call Stack:")
#                for frame_info in inspect.stack():
#                    print(f"    File: {frame_info.filename}, Line: {frame_info.lineno}, Function: {frame_info.function}")
#                    if frame_info.code_context:
#                        print(f"      Code: {frame_info.code_context[0].strip()}")

    def add_contents_height(self, height):
#        original_contents_height = self.contents.height()        
        self.contents.set_size(self.contents.width(),
                               self.contents.height() + height)
#        final_contents_height = self.contents.height()
#        if final_contents_height < original_contents_height: # heightが負の場合など
#            if 100 < len(self.contents.get_text()) and final_contents_height < 100:
#                print(f"DEBUG WARNING: Contents height DECREASED in {self.__class__.__name__}.{inspect.currentframe().f_code.co_name} after adding {height}")
#                print(f"  Target: self.contents, Original: {original_contents_height}, Final: {final_contents_height}, Diff: {final_contents_height - original_contents_height}")
#                print("  Call Stack:")
#                for frame_info in inspect.stack():
#                    print(f"    File: {frame_info.filename}, Line: {frame_info.lineno}, Function: {frame_info.function}")
#                    if frame_info.code_context:
#                        print(f"      Code: {frame_info.code_context[0].strip()}")
    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()

    # Add this method to PaintTitelAndContents
    def set_font_size_for_all_elements(self, new_size):
        self.title.set_font_size(new_size) 
        self.contents.set_font_size(new_size)

    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.current_font_size = 15 # Default initial font size for new nodes
        self.contents = []

        # 差分更新用の状態変数
        self._is_streaming = False
        self._streaming_buffer = ""
        self._streaming_program_name = ""
        self._last_content_was_code_title_during_streaming = False # ストリーミング中にコードタイトルが最後だったか

        # Apply this default to the main component and its children
        self.main.set_font_size_for_all_elements(self.current_font_size)
        self.extract_code_block(contents)

    def set_font_size_for_all_elements(self, new_size):
        self.current_font_size = new_size # Keep track

        self.main.set_font_size_for_all_elements(new_size) # Propagate to main PaintTitelAndContents

        for content_item in self.contents:
            if isinstance(content_item, PaintTitelAndContents):
                content_item.set_font_size_for_all_elements(new_size)
            elif isinstance(content_item, PaintLabelBox): # Includes PaintButton
                content_item.set_font_size(new_size)
        self.__fit_height() # Recalculate layout

    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._is_streaming = False
        self._streaming_buffer = ""
        self._streaming_program_name = ""
        self.blok_type = self.NOMAL_BLOCK # 全文処理開始時はノーマル

        self.extract_code_block(text)
        self.main.set_contents(text)

    def append_text(self, text_segment):
        start_time = time.perf_counter()
#        self.main.append_text(text)
#        current_full_text = self.main.get_contents_text() # Get the full text after appending
#        # Re-extract and re-layout all content blocks based on the new full text
#        #self.contents = []
#        self.extract_code_block(current_full_text)

        self.contents_text += text_segment # 全文を更新
#        self.extract_code_block(self.contents_text)
        
        self._extract_and_append_incremental(text_segment) # 差分処理
        # self.main の更新は finalize_streaming で行う
        self.__fit_height() # 追記の都度、高さ調整

        end_time = time.perf_counter()
        processing_time = (end_time - start_time) * 1000 # ミリ秒に変換
        print(f"DEBUG: PaintChatNode.append_text executed in {processing_time:.3f} ms. Appended text length: {len(text_segment)}")

    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):
#        original_main_contents_height = self.main.get_contents_height()
        self.main.set_contents_height(height)
#        final_main_contents_height = self.main.get_contents_height()
#        if final_main_contents_height < original_main_contents_height:
#            if 100 < len(self.main.contents.get_text()) and final_main_contents_height < 100:
#                print(f"DEBUG WARNING: Main contents height DECREASED in {self.__class__.__name__}.{inspect.currentframe().f_code.co_name}")
#                print(f"  Target: self.main.contents, Original: {original_main_contents_height}, Final: {final_main_contents_height}, Diff: {final_main_contents_height - original_main_contents_height}")
#                print(f"  Called with height: {height}")
#                print("  Call Stack:")
#                for frame_info in inspect.stack():
#                    print(f"    File: {frame_info.filename}, Line: {frame_info.lineno}, Function: {frame_info.function}")
#                    if frame_info.code_context:
#                        print(f"      Code: {frame_info.code_context[0].strip()}")

    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_object(self, obj, contents_buf = None):
        #print("DEBUG: append_contents_object")
        if None is contents_buf:
            contents_buf = []
            contents_buf.append(obj)
        else:
            contents_buf.append(obj)
        
        if 0 == len(contents_buf):
            obj.set_position(self.main.x(), self.main.get_title_height())
        self.main.add_contents_height(obj.height())

    def _prepare_content_object(self, obj):
        # フォント設定
        if hasattr(obj, 'set_font_size_for_all_elements'):
            obj.set_font_size_for_all_elements(self.current_font_size)
        elif hasattr(obj, 'set_font_size'):
            obj.set_font_size(self.current_font_size)
        # 幅とX位置設定 (Y位置は __fit_height で調整)
        if isinstance(obj, PaintTitelAndContents):
            self.__set_titelandcontents_width_and_position(obj)
        elif isinstance(obj, PaintLabelBox):
            self.__set_label_width_and_position(obj)
        return obj
    
    def _extract_and_append_incremental(self, appended_text_segment):
        if not self._is_streaming:
            self._is_streaming = True
            # self.blok_type は直前の状態を引き継ぐ
            # _streaming_buffer は空で開始 (finalizeでクリアされるため)
            # _streaming_program_name も同様

        text_to_process = self._streaming_buffer + appended_text_segment
        self._streaming_buffer = "" # 処理するので一旦クリア
        #改行コードで終わっている。
        end_is_new_line = appended_text_segment.endswith(('\n', '\r\n'))
        lines = text_to_process.split('\n')
        #print("self.blok_type",self.blok_type)
        #print("DEBUG: appended_text_segment",end_is_new_line,appended_text_segment)
#        print("DEBUG: _extract_and_append_incremental len lines" ,len(lines))
        for i ,line in enumerate(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:]
                    program = PaintTitelAndContents(program_name, "")
                    # program = program.get_main_box()
                    self._prepare_content_object(program)
                    self.append_contents_object(program, self.contents)

                    continue


            else:
                # purogram blockの時
                match = re.search(r'```$', line, re.DOTALL)  # 正規表現でコードブロックを検索

                if match:
                    label = PaintLabelBox("")
                    self._prepare_content_object(label)
                    label.set_border(0)
                    self.append_contents_object(label, self.contents)
                    continue
            #text_buf += line + "\n"
            if None is self.contents or 0 == len(self.contents):
                self.blok_type = self.NOMAL_BLOCK
                self.contents=[]
                # ここまでのテキストデータをラベルとして追加
                label = PaintLabelBox("")
                self._prepare_content_object(label)
                label.set_border(0)
                self.append_contents_object(label, self.contents)
                # 初期化
            # print("DEBUG; line ",line)
            
            if i == len(lines) - 1:
#                print("DEBUG: _extract_and_append_incremental line" ,line)
                self.contents[-1].append_text(line)
                pass
#                if end_is_new_line:
#                    self.contents[-1].append_text("\n")
            else:
#                print("DEBUG: _extract_and_append_incremental i lne(lines)" ,i, len(lines))
                self.contents[-1].append_text(line + "\n")
                
            self._prepare_content_object(self.contents)
        self.__fit_height()

    def extract_code_block(self, text):  # コードブロックの抽出
        contents_buf = []
        self.blok_type = self.NOMAL_BLOCK
        self.contents_text = text # Store the original full text for copy-paste
        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_font_size(self.current_font_size) # Apply current node font size
                    label.set_border(0)

                    self.__set_label_width_and_position(label)
                    self.append_contents_object(label, contents_buf)
                    # 初期化
                    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()
                    program.set_font_size_for_all_elements(self.current_font_size) # Apply current node font size
                    self.__set_titelandcontents_width_and_position(program)
                    self.append_contents_object(program, contents_buf)
                    # 初期化
                    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)
            program.set_font_size_for_all_elements(self.current_font_size) # Apply current node font size
            self.__set_titelandcontents_width_and_position(program)

            self.append_contents_object(program, contents_buf)
            self.blok_type = self.NOMAL_BLOCK
        else:
            # ノーマルブロックの状態で終了した。
            # nomal block
            text_buf = text_buf.rstrip()
            label = PaintLabelBox(text_buf)
            label.set_font_size(self.current_font_size) # Apply current node font size
            label.set_border(0)
            self.__set_label_width_and_position(label)

            self.append_contents_object(label, contents_buf)
        self.contents = contents_buf
        self.last_text = text_buf
        self.__fit_height()

    def __fit_height(self):
#        original_main_height = self.main.height()
        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() # 各コンテンツの高さを加算

        #new_contents_height = h - self.main.get_title_height() + self.main.get_contents_border() * count * 2
        self.set_contents_height(h-self.main.get_title_height() +
                                 self.main.get_contents_border()*count*2)
        #if 0 == new_contents_height:
        #print(f"h:{h}, self.main.get_title_height() {self.main.get_title_height()}, self.main.get_contents_border() {self.main.get_contents_border()}, count:{count}")

#        final_main_height = self.main.height()
#        if final_main_height < original_main_height:
#            if 100 < len(self.main.contents.get_text()) and final_main_height < 100:
#
#                print(f"DEBUG WARNING: Main height DECREASED in {self.__class__.__name__}.{inspect.currentframe().f_code.co_name}")
#                print(f"  Target: self.main, Original: {original_main_height}, Final: {final_main_height}, Diff: {final_main_height - original_main_height}")
#                
#                print("  Call Stack:")
#                for frame_info in inspect.stack():
#                    print(f"    File: {frame_info.filename}, Line: {frame_info.lineno}, Function: {frame_info.function}")
#                    if frame_info.code_context:
#                        print(f"      Code: {frame_info.code_context[0].strip()}")

    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)
        else:
            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
        if 0 < len(self.nodes):
            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):
                    # ノードiの下端が描画エリアの上端よりも下にあるか(つまり、ノードiが描画エリア内またはエリアより下にあるか)
                    if self.draw_area.y() < self.nodes[i].y() +self.nodes[i].height():
                        # ノードiの上端が描画エリアの下端よりも上にあるか(つまり、ノードiが描画エリア内に部分的にでも表示されているか)
                        if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
                            # 条件を満たせば、このノードを描画開始インデックスとする
                            self.draw_start_index = i

                    # ノードiの下端が描画エリアの上端よりも上にある場合(つまり、ノード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)):
                    # ノードiの下端が描画エリアの上端よりも下にあるか
                    if self.draw_area.y() < self.nodes[i].y() + self.nodes[i].height():
                        # ノードiの上端が描画エリアの下端よりも上にあるか
                        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):
        self.is_drawing = False # draw処理中かどうかを示すフラグ        
        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.setContentsMargins(0,0,0,0) # レイアウトのマージンを0に
        # self.layout.setSpacing(0) # レイアウト内のスペーシングを0に
        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", "作業指示をしてください。") # 初期メッセージは comparison_gui 側で制御

        # テスト用
#        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):
        start_time = time.perf_counter()
        if not self.is_drawing:
            self.is_drawing = True
            try:
                self.chat_thread.draw(self)
                self.slider.draw(self)
            finally:
                self.is_drawing = False
        end_time = time.perf_counter()
        processing_time = (end_time - start_time) * 1000 # ミリ秒に変換
        # paintEventは非常に頻繁に呼ばれるため、閾値を設けてログ出力を制御するのも良いでしょう
        # if processing_time > 10: # 例えば10ms以上かかった場合のみ出力
#        print(f"DEBUG: ChatPaintWidget.paintEvent executed in {processing_time:.3f} ms")
    
    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:
            # Fallback to a default if no font_size is provided
            font_size = 15 # Default font size

        self.font.setPointSize(font_size) # For the widget itself, if needed

        # Propagate to all nodes
        if hasattr(self, 'chat_thread') and hasattr(self.chat_thread, 'nodes'):
            for node in self.chat_thread.nodes:
                if hasattr(node, 'set_font_size_for_all_elements'):
                    node.set_font_size_for_all_elements(font_size)
        self.update()

    def __update_slider(self):
        self.slider.set_rect(self.width()-self.slider.width(), 0, self.slider.width(), self.height())
        
        if (0 == self.chat_thread.height()):
            self.slider.set_slider_height(0)
        else:
            self.slider.set_slider_height(self.chat_thread.draw_area_height()/self.chat_thread.height())
        self.slider.get_slider_position_ratio()
        # print(f"self.chat_thread.height() {self.chat_thread.height()}")
        if (self.chat_thread.height() - self.chat_thread.draw_area_height()) <=0:
            self.slider.set_slider_position(0)
        else:            
            self.slider.set_slider_position(self.chat_thread.draw_area_y()/max(1,(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 __resize(self):
        self.resizeEvent(None) # QResizeEvent を渡す必要はない
       
    def append_node(self, title, text):
        self.chat_thread.append_node(title, text)
        self.__resize()

    def append_text(self, text):
        # 最後のノードにテキストを追記
        self.chat_thread.append_text(text)
        if self.is_drawing:
            return 
        self.__resize() # テキスト追加で高さが変わる可能性があるので再計算

    def set_last_node_text(self, text):
        # 最後のノードのテキストを置き換え
        self.chat_thread.set_last_node_text(text)
        while self.is_drawing:
            time.sleep(0.1)
        self.__resize() # テキスト置換で高さが変わる可能性があるので再計算

    def new_streaming_node(self, title):
        # ストリーミング表示用の新しい空ノードを追加
        self.chat_thread.append_node(title, "") # 初期テキストは空
        self.__resize()

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.send_button.setEnabled(False) # ボタンを無効化
            self.input_edit.setReadOnly(True)  # 入力欄を読み取り専用に

#############################################################
            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):
        # レスポンスを受け取って表示
        # AIAgent.get_respons の中で表示は更新されているはずなので、
        # ここではUIの制御(ボタンの再有効化など)を行います。
        # response引数はAppendNodeWorker.finishedから渡されますが、
        # 現在の設計では表示には使用しません。
        self.send_button.setEnabled(True) # ボタンを再度有効化
        self.input_edit.setReadOnly(False) # 入力欄を編集可能に戻す
        self.input_edit.setFocus() # 入力欄にフォーカスを戻す (任意)

    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 new_streaming_node(self, title):
        # ChatPaintWidgetの新しいメソッドを呼び出す
        # これはUIスレッドから直接呼び出されることを想定
        self.chat_thread.new_streaming_node(title)

    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
        self.token_buffer = ""
        self.last_update_time = time.time()
        self.update_interval = 0.1 # 更新間隔を秒単位で指定 (例: 0.1秒)
        #self._is_cancel = False
    def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
        #print("MyStreamlitCallbackHandler self._is_cancel",self._is_cancel)
        InterruptedException.append_response(token)
        if InterruptedException.is_cancelled():
            InterruptedException.set_cancel(True)
            raise BaseException("test exception")
            raise InterruptedException("LLM stream cancelled by worker flag.")
            raise "is canceld "
        #print("token",token)
#        self.token_buffer.append(token)
        self.token_buffer += token 
        current_time = time.time()
        # #print("DEBUG on_llm_new_token",token)
        if current_time - self.last_update_time >= self.update_interval:
            if self.token_buffer:
                self.container.append_text(self.token_buffer)
                self.token_buffer = ""
            self.last_update_time = current_time
        
    def on_llm_end(self, response: Any, **kwargs: Any) -> None:
        """LLMのストリーミングが終了したときに呼び出される"""
        if self.token_buffer: # バッファに残りがあれば全て送信
            self.container.append_text(self.token_buffer)
            self.token_buffer = ""
#    @classmethod
#    def set_cancel(cls, cancel_flag):
#        cls._is_cancel = cancel_flag
#        print("MyStreamlitCallbackHandler self.set_cancel",cls._is_cancel)
    # 必要に応じて他のコールバックメソッド (on_chain_endなど) にも同様のフラッシュ処理を追加
    # def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None:
    #     self.flush_token_buffer()


_g_app_instance = None
_g_my_widget_instance = None

def _get_app_instance():
    global _g_app_instance
    # QApplication.instance() を使って既存のインスタンスを取得、なければ作成
    _g_app_instance = QApplication.instance()
    if _g_app_instance is None:
        _g_app_instance = QApplication(sys.argv)
    return _g_app_instance

def _get_my_widget_instance(agent=None):
    global _g_my_widget_instance

    if _g_my_widget_instance is None:
        _g_my_widget_instance = ChatWidget(agent)
        # ChatWidget のデフォルト設定 (必要に応じて)
        _g_my_widget_instance.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        _g_my_widget_instance.setMinimumHeight(400)

        # _g_my_widget_instance.setFixedWidth(1000) # 幅は親ウィジェットに追従させる方が良い場合もある
    elif agent is not None:
        _g_my_widget_instance.set_agent(agent)
    return _g_my_widget_instance

def excute(agent):
    app = _get_app_instance() # QApplicationインスタンスを取得または作成
    my_widget = _get_my_widget_instance(agent)

    # ChatWidget を表示するための親ウィジェットとレイアウト (オプション)
    # main_widget_for_excute = QWidget()
    # main_layout_for_excute = QVBoxLayout(main_widget_for_excute)
    # main_layout_for_excute.addWidget(my_widget)
    # main_widget_for_excute.show()
    # もしChatWidgetが直接トップレベルウィンドウとして機能するなら上記は不要
    my_widget.show()

    # ComparisonGui.py がメインのイベントループを持つため、ここでは app.exec() を呼び出さない
    # paintgui.py が単体で実行される場合は、if __name__ == '__main__': の中で app.exec() を呼び出す

def set_agent(agent):
    my_widget = _get_my_widget_instance(agent)
    # my_widget.set_agent(agent) # _get_my_widget_instance 内で処理される

def update():
    my_widget = _get_my_widget_instance()
    my_widget.update()

def append_node(name,text):
    my_widget = _get_my_widget_instance()
    my_widget.append_node(name, text)

def set_last_node_text(text):
    my_widget = _get_my_widget_instance()
    my_widget.set_last_node_text(text)

def new_streaming_node(title):
    my_widget = _get_my_widget_instance()
    if hasattr(my_widget, 'new_streaming_node'):
        my_widget.new_streaming_node(title)

def get_stc_handler():
    my_widget = _get_my_widget_instance()
    return MyStreamlitCallbackHandler(my_widget)

def get_mywidget():
    return _get_my_widget_instance()

if __name__ == '__main__':
    app = _get_app_instance() # QApplicationインスタンスを取得または作成
    # ChatWidget を含むメインウィンドウのセットアップ
    main_widget = QWidget()
    main_layout = QVBoxLayout(main_widget)
    # from Agents.AIAgent import AIAgent # テスト用にインポート
    # test_agent = AIAgent("DummyAgentForPaintGUI", "System prompt for dummy", [])
    # my_widget = _get_my_widget_instance(test_agent)
    my_widget = _get_my_widget_instance(None) # エージェントなしで初期化
    main_layout.addWidget(my_widget)
    # main_layout.addStretch(0) # 必要に応じて
    
    main_widget.setWindowTitle("PaintGUI Standalone")
    main_widget.setGeometry(100, 100, 1000, 700) # 適当なサイズ
    main_widget.show()
    
    sys.exit(app.exec())

AIによる説明
PaintGUI ライブラリ リファレンスマニュアル

このドキュメントは、Pythonを用いたGUIライブラリ PaintGUI の使用方法を説明します。PySide6 をベースに、チャット風のインターフェースでテキストとコードブロックを表示する機能を提供します。

1. モジュール構成

PaintGUI は以下のクラスから構成されます。

  • PaintLabelBox: テキストを表示するラベルボックス。テキストの自動改行、フォントサイズ変更、位置調整、枠線設定などをサポートします。
  • PaintButton: ボタンとして機能するラベルボックス。マウスイベントに応じた状態変化(通常、ホバー、クリック)と、背景色の変更をサポートします。
  • PaintChatTitleBox: タイトルとコピーボタンを組み合わせたコンポーネント。タイトルテキストの設定、フォントサイズ変更、サイズ調整、位置調整をサポートします。コピーボタンを押すと、タイトルテキストがクリップボードにコピーされます。
  • PaintTitelAndContents: タイトル(PaintChatTitleBox)とコンテンツ(PaintLabelBox)を組み合わせたコンポーネント。コンテンツの高さ自動調整、テキストの追加、フォントサイズ変更、幅と位置の調整をサポートします。
  • PaintChatNode: チャットメッセージノードを表すコンポーネント。タイトル、コンテンツ、コードブロックを管理します。コードブロックは““`で囲まれたテキストとして認識されます。ストリーミング表示にも対応しています。
  • PaintChatThread: チャットメッセージノードを管理するコンテナ。ノードの追加、描画領域の管理、スクロール機能を提供します。
  • PaintArrowSlider: スクロールバーとして機能するコンポーネント。上下ボタンとスライダーで描画領域を制御します。
  • ChatPaintWidget: PaintChatThreadPaintArrowSlider を統合したウィジェット。マウスイベント、キーボードイベント、ホイールイベントに対応し、描画領域の更新を行います。
  • ChatWidget: ChatPaintWidget を含むメインウィジェット。メッセージ入力欄と送信ボタンを提供し、AIAgent (別途定義が必要) と連携してチャット機能を実現します。
  • AppendNodeWorker: AIAgent との通信をバックグラウンドで行うワーカースレッド。
  • AppendNodeEvent, AppendTextEvent, SetLastNodeText: ChatWidget でカスタムイベント処理を行うためのイベントクラス。
  • MyStreamlitCallbackHandler: Langchain のコールバックハンドラ。LLMからのストリーミング応答を処理し、ChatPaintWidget にテキストを追加します。

2. 主要クラスの詳細

2.1 PaintLabelBox

  • __init__(self, text=""): コンストラクタ。初期テキストを設定します。
  • set_font_size(self, size): フォントサイズを設定します。
  • set_size(self, width, hight): 幅と高さを設定します。
  • set_width(self, w): 幅を設定します。
  • set_position(self, x, y): 位置を設定します。
  • move(self, x, y): 指定した分だけ移動します。
  • set_text(self, text): テキストを設定します。
  • append_text(self, text): テキストを追加します。
  • set_span(self, span): 改行幅を設定します。
  • set_border(self, bsize): 枠線の太さを設定します。
  • draw(self, obj): 指定されたペイントデバイス(obj)に描画します。

2.2 PaintButton

PaintLabelBox を継承したボタンクラス。

  • __init__(self, text=""): コンストラクタ。
  • check_point_in(self, pos): 指定された座標がボタン内にあるか判定します。
  • mouse_down(self, fpos): マウスボタン押下イベントを処理します。
  • mouse_up(self, fpos): マウスボタン解放イベントを処理します。
  • mouse_hover(self, fpos): マウスホバーイベントを処理します。
  • set_back_color(self, color): 通常時の背景色を設定します。
  • set_clicked_back_color(self, color): クリック時の背景色を設定します。
  • set_hover_back_color(self, color): ホバー時の背景色を設定します。
  • draw(self, obj): 指定されたペイントデバイス(obj)に描画します。

2.3 PaintChatNode

チャットメッセージノードを表すクラス。

  • __init__(self, title=" ", contents=" "): コンストラクタ。タイトルとコンテンツを設定します。
  • set_font_size_for_all_elements(self, new_size): 全ての要素のフォントサイズを変更します。
  • mouse_down(self, pos): マウスボタン押下イベントを処理します。
  • mouse_up(self, pos): マウスボタン解放イベントを処理します。
  • mouse_hover(self, pos): マウスホバーイベントを処理します。
  • set_title(self, text): タイトルを設定します。
  • set_contents(self, text): コンテンツを設定します。
  • append_text(self, text_segment): コンテンツにテキストを追加します。(ストリーミング対応)
  • extract_code_block(self, text): テキストからコードブロックを抽出し、PaintTitelAndContents オブジェクトとしてコンテンツに追加します。
  • draw(self, obj): 指定されたペイントデバイス(obj)に描画します。

2.4 ChatPaintWidget

PaintChatThreadPaintArrowSlider を統合したウィジェット。

  • __init__(self): コンストラクタ。
  • paintEvent(self, event): ペイントイベントを処理します。
  • mousePressEvent(self, event): マウスボタン押下イベントを処理します。
  • mouseReleaseEvent(self, event): マウスボタン解放イベントを処理します。
  • mouseMoveEvent(self, event): マウス移動イベントを処理します。
  • keyPressEvent(self, event): キーボードイベントを処理します。
  • wheelEvent(self, event): ホイールイベントを処理します。
  • update_font_size(self, font_size=None): フォントサイズを変更します。
  • append_node(self, title, text): 新しいノードを追加します。
  • append_text(self, text): 最後のノードにテキストを追加します。
  • set_last_node_text(self, text): 最後のノードのテキストを置き換えます。
  • new_streaming_node(self, title): ストリーミング表示用の新しいノードを追加します。
  • resizeEvent(self, event): サイズ変更イベントを処理します。

2.5 ChatWidget

ChatPaintWidget を含むメインウィジェット。

  • __init__(self, agent): コンストラクタ。AIAgent インスタンスを受け取ります。
  • send_message(self): メッセージ送信ボタンが押されたときに呼び出されます。
  • append_node(self, title, text): 新しいノードを追加します。(シグナル経由)
  • append_text(self, text): 最後のノードにテキストを追加します。(カスタムイベント経由)
  • set_last_node_text(self, text): 最後のノードのテキストを置き換えます。(カスタムイベント経由)
  • new_streaming_node(self, title): ストリーミング表示用の新しいノードを追加します。
  • customEvent(self, event): カスタムイベントを処理します。

2.6 MyStreamlitCallbackHandler

Langchain のコールバックハンドラ。

  • __init__(self, container): コンストラクタ。ChatPaintWidget インスタンスを受け取ります。
  • on_llm_new_token(self, token, **kwargs): LLMから新しいトークンを受け取ったときに呼び出されます。
  • on_llm_end(self, response, **kwargs): LLMからのストリーミングが終了したときに呼び出されます。

3. 関数

  • excute(agent): GUI を実行します。AIAgent インスタンスを受け取ります。
  • set_agent(agent): AIAgent を設定します。
  • update(): GUI を更新します。
  • append_node(name, text): 新しいノードを追加します。
  • set_last_node_text(text): 最後のノードのテキストを更新します。
  • new_streaming_node(title): ストリーミング用の新しいノードを作成します。
  • get_stc_handler(): MyStreamlitCallbackHandler のインスタンスを取得します。
  • get_mywidget(): ChatWidget のインスタンスを取得します。

4. 使用例

# from Agents.AIAgent import AIAgent # 適切なエージェントクラスをインポート
# agent = AIAgent(...) # エージェントインスタンスを作成

import paintgui

# paintgui.excute(agent) # エージェントを指定して実行

# または、エージェントなしで実行し、後で設定する
paintgui.excute(None)
# ... 後でエージェントを設定 ...
# paintgui.set_agent(agent)

# テキストを追加
paintgui.append_node("user", "Hello, world!")
paintgui.append_node("ai", "Hello to you too!")

# ストリーミング表示を開始
paintgui.new_streaming_node("ai_streaming")
# ... ストリーミングテキストを少しずつ追加 ...
paintgui.get_mywidget().append_text("This is a streaming ")
paintgui.get_mywidget().append_text("response.")

# 最後のノードのテキストを更新
paintgui.set_last_node_text("Updated text.")

# GUIの更新
paintgui.update()

5. 注意点

  • このライブラリは PySide6 に依存します。インストールが必要です。
  • ストリーミング表示は、MyStreamlitCallbackHandlerAIAgentget_respons メソッドの連携によって実現されます。get_respons メソッドは、トークン単位で応答を返す必要があります。
  • エラー処理は簡略化されています。本番環境ではより詳細なエラー処理を実装する必要があります。
  • 高さ調整に関する警告メッセージは、デバッグ用です。本番環境ではコメントアウトすることを推奨します。

このマニュアルが、PaintGUI ライブラリを使用する際の助けになれば幸いです。 より詳細な情報や具体的な使用方法については、ソースコードを参照してください。