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