AI Agent:GUI:PaintGUI.py:ソースコード

import sys
import os
import traceback
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QPushButton, QSizePolicy, QFileDialog, QSpinBox
from PySide6.QtGui import QPainter, QColor, QFont, QPen, QBrush, QMouseEvent, QKeyEvent, QWheelEvent, QTextLayout, QTextOption, QTextDocument, QTextCursor, QTextBlock
from PySide6.QtCore import Qt, QRect, QPoint, QEvent, QCoreApplication ,Signal, QThread, QObject, QPointF
import re
import pyperclip
from typing import Dict, Any
from langchain_core.callbacks import BaseCallbackHandler
from typing import  Any
import threading
import time
import inspect
import json
from copy import deepcopy
from tools.exception import InterruptedException as InterruptedException
from tools.program_called_command_list import WORK_SPACE_DIR
from Agents.AIAgent import AIAgent


import requests
from pathlib import Path # pathlib.Path をインポート
import base64
class PaintBaseObject():

    widget_area = None
    def __init__(self):
        pass

    @classmethod
    def set_widget_area(cls, rect):
        cls.widget_area = rect
    @classmethod
    def check_rect_in(cls, rect):
#        if cls.draw_area.y() < rect.y() + rect.height():
#            if rect.y() < cls.draw_area.y() + cls.draw_area.height():
#                return True
        if 0 < rect.y() + rect.height():
            if rect.y() < cls.widget_area.y() + cls.widget_area.height():
                return True
        return False
    
class PaintLabelBox(PaintBaseObject):
    def __init__(self, text=""):
        self.rect = QRect(0, 0, 150, 50)
        self.font = QFont("Arial", 14)
        self.font_name = "Arial" # Store font name
        self.font_size = 14 # フォントサイズを保持
        self.span = 1
        self.text = text
        self.align = Qt.AlignLeft
        self.height_fit = True
        self.border = 5
        self.fit_text_lines = []

        self.text_posy_list = []
        self.draw_text = ""
        self.draw_text_rect = QRect(0, 0, 0, 0)
        self.draw_border_rect = QRect(0, 0, 0, 0)
        self.document_text= QTextDocument()
        self.document_text.setDefaultFont(self.font)
        self.document_text.setPlainText(self.text)
        self.blocks = []
        self.block_bottoms = []
        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.document_text.setDefaultFont(self.font)
        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.document_text.setTextWidth(self.text_rect.width())
 
        
        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()
        st=time.time()
        self.document_text.setTextWidth(self.text_rect.width())

        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.document_text.setPlainText(self.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
        cursor =  QTextCursor(self.document_text)
        cursor.movePosition(QTextCursor.End)
        cursor.insertText(text)


        self.__fit_text_last_line(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()

    def check_rect_in(self):
        if PaintBaseObject.check_rect_in(self.rect):
            return True
        return False
    #  テキストを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):
        
        #self.document_text.setTextWidth(self.text_rect.width())
        #self.blocks, self.block_bottoms = self.build_block_cache(self.document_text)
                

        self.__fit_height()
     
    def __fit_text_last_line(self,append_text):

        self.__fit_height()

    def __fit_height(self):
        if self.height_fit:
            #self.__set_text_rect()
            #self.document_text.setTextWidth(self.text_rect.width())
            #self.blocks, self.block_bottoms = self.build_block_cache(self.document_text)
             
            height_total = self.document_text.size().height()

            new_height = height_total + (self.span+self.border)*2
            self.rect = QRect(self.rect.x(), self.rect.y(),
                              self.rect.width(),
                              new_height)

            self.__set_text_rect()

            #self.__set_position_y_list()
    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 simpole_draw_text(self, painter: QPainter):
        painter.save()
        painter.translate(self.text_rect.x(), self.text_rect.y())
        self.document_text.drawContents(painter)
        painter.restore()
    def draw(self, obj):
        if False == PaintBaseObject.check_rect_in(self.rect):
            return
        
        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.save()
        painter.setClipRect(PaintBaseObject.widget_area)

        painter.drawRect(self.rect)

        painter.setPen(QPen(QColor(0, 0, 0), self.border)) 

        #self.draw_visible_text_only(painter)

        self.simpole_draw_text(painter)

        painter.restore()

class PaintButton(PaintLabelBox, PaintBaseObject):
    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):
        if False == PaintBaseObject.check_rect_in(self.rect):
            return        
        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.save()
        painter.setClipRect(PaintBaseObject.widget_area)
        painter.drawRect(self.rect)
        painter.setPen(QPen(QColor(0, 0, 0), 2))  # 青色の枠線
        painter.drawText(self.rect, self.get_align(), self.text)

        painter.restore()

class PaintChatTitleBox(PaintBaseObject):
    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):
        if False == self.title.check_rect_in():
            return        
        self.title.draw(obj)
        self.button.draw(obj)


class PaintTitelAndContents(PaintBaseObject):
    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()

    # 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(PaintBaseObject):
    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 # ストリーミング中にコードタイトルが最後だったか

        # プログラム名の状態変数
        self._program_name_in_progress = False
        self._program_name_buffer = ""

        # 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.contents_text += text_segment # 全文を更新
        self._extract_and_append_incremental(text_segment) # 差分処理
        self.__fit_height() # 追記の都度、高さ調整

        end_time = time.perf_counter()
        processing_time = (end_time - start_time) * 1000 # ミリ秒に変換

    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.__update_label_position(obj)
        self.__fit_height()

    def __update_label_position(self, label):
        label.set_position(self.x() + self.main.get_contents_border(),
                           self.y() + self.main.get_contents_border())
        
    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_object(self, obj, contents_buf = None):
        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

        text_to_process = self._streaming_buffer + appended_text_segment
        self._streaming_buffer = "" # 処理するので一旦クリア
        #改行コードで終わっている。
        lines = text_to_process.split('\n')

        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:]
                    if len(lines) - 1 == i:
                        if "" == program_name:
                            self._program_name_in_progress = True
                            self._program_name_buffer=""
                            continue
                    program = PaintTitelAndContents(program_name, "")
                    # program = program.get_main_box()
                    self._prepare_content_object(program)
                    self.append_contents_object(program, self.contents)
                    continue

            else:
                if self._program_name_in_progress:
                    # ストリーミング中にコードタイトルが最後だった場合
                    # ここでプログラム名を設定
                    self._program_name_buffer += line

                    if 0 == i and 1 == len(lines):


                        if appended_text_segment.endswith("\n") or appended_text_segment.endswith("\r\n"):
                            pass
                        else:

                            continue

                    program = PaintTitelAndContents(self._program_name_buffer, "")
                    # program = program.get_main_box()
                    self._prepare_content_object(program)
                    self.append_contents_object(program, self.contents)                    
                    self._program_name_in_progress = False
                    self._program_name_buffer = ""
                    continue
                # purogram blockの時
                match = re.search(r'```$', line, re.DOTALL)  # 正規表現でコードブロックを検索

                if match:
                    label = PaintLabelBox("")
                    self._prepare_content_object(label)
                    label.set_border(0)
                    #label.set_span(26)
                    self.append_contents_object(label, self.contents)

                    self.blok_type = self.NOMAL_BLOCK
                    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)
                #label.set_span(26)
                self.append_contents_object(label, self.contents)
                # 初期化
            
            if i == len(lines) - 1:
                self.contents[-1].append_text(line)
                pass
            else:
                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)
                    #label.set_span(26)
                    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)
            #label.set_span(26)
            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):
        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(PaintBaseObject):
    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)
            pass
        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 _reset_nodes_position(self):
        self.set_width(self.width())
        #
        #if 0 < len(self.nodes):
        #    for i in range(len(self.nodes)):
        #        if 0 == i:
        #            self.nodes[i].set_position(
        #                self.draw_area.x(),
        #                self.draw_area.y())
        #        else:
        #            self.nodes[i].set_position(
        #                self.nodes[i - 1].x(),
        #                self.nodes[i - 1].y() + self.nodes[i-1].height() + self.node_space)
        #    self.draw_area.setRect(
        #        self.draw_area.x(),
        #        self.draw_area.y(),
        #        self.draw_area.width(),
        #        self.nodes[-1].y()+self.nodes[-1].height() - self.draw_area.y())
            
    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.addWidget(self.name)
        self.setLayout(self.layout)
        self.chat_thread = PaintChatThread()
        self.slider = PaintArrowSlider()
        self.update_font_size(20)

    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 # ミリ秒に変換
    
    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
        #self.chat_thread.set_font_size(font_size)
        # 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.chat_thread._reset_nodes_position()
        self.__update_slider()
        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)
        
#        PaintBaseObject.set_widget_area(QRect(self.x(), self.y(), self.width(), self.height()))
        PaintBaseObject.set_widget_area(QRect(0, 0, self.width(), self.height()))
        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()
       
        self.setAcceptDrops(True)   # ← これが必須
        # --- Font settings ---
        self.current_default_font_size = 14
        self.current_label_font_size = 12
        self.current_button_font_size = 13
        self.current_groupbox_font_size = 14
        self.default_font_name = "Arial"    

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


        #image inpput
        self.image_input_layout = QHBoxLayout()
        self.image_path_label = QLabel("Image (optional): None")
        self.image_input_layout.addWidget(self.image_path_label)
        self.browse_image_button = QPushButton("Browse Image")
        self.browse_image_button.clicked.connect(self.browse_image)
        self.image_input_layout.addWidget(self.browse_image_button)
        self.clear_image_button = QPushButton("Clear Image")
        self.clear_image_button.clicked.connect(self.clear_image)
        self.image_input_layout.addWidget(self.clear_image_button)
        self.selected_image_path = []        
        self.selected_pdf_path = []
        self.selected_text_path = []
        self.selected_word_path = []
        self.file_paths=[]
        # input 入力欄
        self.input_edit = QTextEdit()
        self.input_edit.setMaximumHeight(100)
        self.send_button = QPushButton("Send")
        self.send_button.clicked.connect(self.send_message)

        self.stop_button = QPushButton("Stop")
        self.stop_button.clicked.connect(self.stop_message)
        self.stop_button.setEnabled(False) # Initially disabled
        
        # フォントサイズ変更UI
        self.font_control_layout = QHBoxLayout()
        font_size_label = QLabel("Global Font Size:")
        self.font_control_layout.addWidget(font_size_label)
        self.font_size_spinbox = QSpinBox()
        self.font_size_spinbox.setRange(8, 30) # フォントサイズの範囲
        self.font_size_spinbox.setValue(self.current_default_font_size)
        self.font_control_layout.addWidget(self.font_size_spinbox)
        apply_font_button = QPushButton("Apply Font Size")
        apply_font_button.clicked.connect(self.apply_global_font_size)
        self.font_control_layout.addWidget(apply_font_button)


        # Layout for input area
        input_layout = QHBoxLayout()
        input_layout.addWidget(self.input_edit)
        input_layout.addWidget(self.send_button)
        input_layout.addWidget(self.stop_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(self.image_input_layout)
        main_layout.addLayout(input_layout)
        main_layout.addLayout(self.font_control_layout)
        
        self.update_signal.connect(self._append_node_handler)
        self.agent = agent
        self.thread = None

        # 動作確認用
        # スレッド作成
        #self.append_node("System", "ChatWidget initialized.")
        #self._thread = QThread()
        #self.worker = TextWorker()
        #self.worker.moveToThread(self._thread)
#
        ## シグナル接続
        #self._thread.started.connect(self.worker.run)
        ##self.worker.text_generated.connect(self.add_text)
#
        #self._thread.start()
    #def# add_text(self, text):
    #    self.fit_text_lines.append(text)
    #    self.update()  # 再描画


    def browse_image(self):
        # WORK_SPACE_DIR が設定されていればそこから開始、なければホームディレクトリ
        start_dir = WORK_SPACE_DIR if WORK_SPACE_DIR and os.path.isdir(WORK_SPACE_DIR) else os.path.expanduser("~")

        # 複数ファイル選択を許可
        filePaths, _ = QFileDialog.getOpenFileNames(self, "Select Images", start_dir, "Images (*.png *.jpg *.jpeg *.bmp *.gif)")
        if filePaths:
            self.add_images(filePaths)

    def add_file(self, file_paths: list):
        """画像パスのリストを受け取り、UIを更新するヘルパーメソッド"""
        self.selected_image_path.extend(file_paths)
        # 重複を排除し、ソートして一貫性を保つ
        self.selected_image_path = sorted(list(set(self.selected_image_path)))
        
        if len(self.selected_image_path) == 1:
            self.image_path_label.setText(f"file: {os.path.basename(self.selected_image_path[0])}")
        elif 1 < len(self.selected_image_path):
            self.image_path_label.setText(f"files: {len(self.selected_image_path)} files selected")
        else: # 0件の場合
            self.image_path_label.setText("file (optional): None")

    def dragEnterEvent(self, event):
        # ドラッグされたデータにURL(ファイルパス)が含まれているかチェック
        if event.mimeData().hasUrls():
            event.acceptProposedAction() # ドロップ操作を受け入れる
        else:
            event.ignore() # 受け入れない

    def dropEvent(self, event):
        # ドロップされたファイルパスを取得
        urls = event.mimeData().urls()

        self.file_paths = []
        for url in urls:
            if url.isLocalFile():
                file_path = url.toLocalFile()
                print("type file_path",type(file_path))
                print("type url",type(url))
                print("file_path",file_path)
                print("url",url)
                self.file_paths.append(file_path)

        if self.file_paths:
            self.add_file(self.file_paths)

    def clear_image(self):
        self.selected_image_path = []
        self.image_path_label.setText("Image (optional): 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.chat_thread.append_node(self.agent.get_name(),"")
            self.input_edit.clear()
            
            # ワーカースレッドを作成して処理を移す
            self.thread = QThread()
            self.worker = AppendNodeWorker(self.agent, message,
                                           self.file_paths)
            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.stop_button.setEnabled(True)
            # スレッド開始
            self.thread.start()


    def stop_message(self):
        print("Stop All Models button clicked.")
        InterruptedException.set_cancel(True)
            
        if self.worker:
            self.worker.cancel() # 各ワーカーにキャンセルを通知
        
        # UIの更新
        self.send_button.setEnabled(True)
        #self.clear_outputs_button.setEnabled(True)
        self.stop_button.setEnabled(False)
        
        # 実行キューをクリアし、処理インデックスをリセット
        #self.model_task_queue.clear()
        #self.processing_model_index = -1 
        
        #QMessageBox.information(self, "Stopped", "Processing of all models has been requested to stop.")


    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() # 入力欄にフォーカスを戻す (任意)
        self.stop_button.setEnabled(False)

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


    def _apply_current_font_settings(self):
        default_font = QFont(self.default_font_name, self.current_default_font_size)
        label_font = QFont(self.default_font_name, self.current_label_font_size)
        button_font = QFont(self.default_font_name, self.current_button_font_size)
        groupbox_font = QFont(self.default_font_name, self.current_groupbox_font_size, QFont.Bold)
        #self.enable_warmup = True

        self.setFont(default_font)
        if hasattr(self, 'agent_combo'): # 初期化順序によるエラー回避

            self.input_edit.setFont(default_font)
            self.image_path_label.setFont(label_font)
            self.browse_image_button.setFont(button_font)
            self.clear_image_button.setFont(button_font)
            self.stop_button.setFont(button_font) # Stop button font            
            self.send_button.setFont(button_font)
            #self.clear_outputs_button.setFont(button_font)

        if hasattr(self, 'font_size_spinbox'): # フォント変更UIが初期化されていれば
            self.font_control_layout.itemAt(0).widget().setFont(label_font) # Global Font Size Label
            self.font_size_spinbox.setFont(default_font)
            # self.font_control_layout.itemAt(2).widget().setFont(button_font) # Apply Font Button
            # self.global_settings_layout.itemAt(0).widget().setFont(label_font)
            self.font_control_layout.itemAt(2).widget().setFont(button_font) 
        #self.update_model_views_font()
        self.chat_thread.update_font_size(self.current_default_font_size)

    def apply_global_font_size(self):
        new_base_size = self.font_size_spinbox.value()
        self.current_default_font_size = new_base_size
        # 他のフォントサイズもベースサイズに基づいて調整する(例)
        self.current_label_font_size = max(8, int(new_base_size * 0.85)) # ラベルは少し小さく
        self.current_button_font_size = max(8, int(new_base_size * 0.9))  # ボタンも調整
        self.current_groupbox_font_size = new_base_size # グループボックスは同じか少し大きく

        self._apply_current_font_settings()
        
        #self.update_model_views_font() # 各モデルビューのフォントも更新



g_mime_map = {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".bmp": "image/bmp",
    ".tif": "image/tiff",
    ".tiff": "image/tiff",
    ".svg": "image/svg+xml",
    ".webp": "image/webp",
    ".emf": "image/x-emf",
    ".wmf": "application/x-msmetafile",
}




class AppendNodeWorker(QObject):
    finished = Signal(str)  # 処理が終わったときに結果を送信するシグナル
    image_count=0
    def _encode_image_to_data_url(self, image_path_or_url: str) -> str:
        """画像パスまたはURLからBase64エンコードされたデータURL文字列を生成する"""
        if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"):
            # import requests # AIAgent.py の冒頭で import 済みのはず
            response = requests.get(image_path_or_url, timeout=10)
            response.raise_for_status()
            image_data = response.content
            mime_type = response.headers.get('Content-Type', 'image/jpeg')
        elif Path(image_path_or_url).exists():
            import os # ローカルインポートで良いか、クラス冒頭でimportするか検討
            with open(image_path_or_url, "rb") as image_file:
                image_data = image_file.read()
            _, ext = os.path.splitext(image_path_or_url.lower())
            mime_type = g_mime_map.get(ext)
        else:
            raise FileNotFoundError(f"Image path or URL not found or not accessible: {image_path_or_url}")


        base64_encoded_data = base64.b64encode(image_data).decode('utf-8')
        result={
            "type":"image",
            "mime_type":mime_type,
            "base64":base64_encoded_data
        }
        AppendNodeWorker.image_count += 1
        return result
   
    
    def __init__(self, agent, message, file_paths):
        super().__init__()
        self.agent = agent
        self.message = message

        self.file_paths = file_paths
    def run(self):
        # 重い処理をここで行う
        AIAgent.set_callbacks([get_stc_handler()])
        response = self.agent.get_response(self.message, self.file_paths )
        self.finished.emit(response)  # 結果をシグナルで送信

        pass
    def cancel(self):
        print(f"Worker {self.model_index}: Cancellation requested.")
        InterruptedException.set_cancel(True)
        pass

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秒)
        # LangChain の新仕様に対応するため追加
        self.parent_run_id = None
        self.run_id = None
        # LangChain が callback handler を「コンテナ」として扱うため必要
        #self.handlers = []
        #self.inheritable_handlers = []
        #self.inheritable_tags = []
        #self.inheritable_metadata = []

    def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
        #print("newtoken type", type(token))
        if isinstance(token, dict):
        #    token = "".join(token)
            print("erro newtoken is dict", token,len(token))
        if isinstance(token, list):
            if len(token) == 1:
                 if isinstance(token[0], dict):
                     if "text" in token[0]:
                        token=token[0]["text"]
                        print("newtoken list dict")
                 else: 
                    token = "".join(token)
                    print("newtoken list")
        InterruptedException.append_response(token)
        if InterruptedException.is_cancelled():
            InterruptedException.set_cancel(False)
            raise BaseException("test exception")
            raise InterruptedException("LLM stream cancelled by worker flag.")
            raise "is canceld "
        #print("newtoken", token,len(token))
            print("newtoken", token,len(token))

        self.token_buffer += token 
        current_time = time.time()
        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 = ""

    def on_tool_start(self, serialized, input_str, *, run_id, parent_run_id = None, tags = None, metadata = None, inputs = None, **kwargs):
        tool_name = serialized.get("name")
        if tool_name:
            print(f"ツール '{tool_name}' が開始されました。")
        return super().on_tool_start(serialized, input_str, run_id=run_id, parent_run_id=parent_run_id, tags=tags, metadata=metadata, inputs=inputs, **kwargs)
#---------------
##動作確認用ワーカースレッド
#class TextWorker(QObject):
#    text_generated = Signal(str)   # メインスレッドに送るシグナル
#    running = True
#
#    def run(self):
#        count = 0
#        while self.running:
#            count += 1
#            #self.text_generated.emit(f"Line {count}")
#            append_text(f"Line {count}\n")
#            QThread.sleep(0.01)  # 1秒ごとに追加
#
    
_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):
    try:
        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.setGeometry(100, 100, 1000, 700) # 適当なサイズ
        my_widget.show()
#    except ImportError as e:
#        print(f" {e}")
        sys.exit(app.exec())
    except Exception as e:        
        print(f" {e}")
    # 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 append_text(text):
    my_widget = _get_my_widget_instance()
    if None is not my_widget:
        my_widget.append_text(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())