from PySide6.QtWidgets import (QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QListWidget, QListWidgetItem, QWidget,
QTextEdit, QLineEdit)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QPixmap
from core.ui.mainwindow import Ui_MainWindow
from core.agent.agent import Agent
from core.character.character import Character
from core.llm.ollamaapi import OllamaProvider
class CharacterListDialog(QDialog):
"""Диалоговое окно со списком персонажей"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Выбор персонажа")
self.setMinimumSize(500, 400)
self.selected_character = None
layout = QVBoxLayout(self)
# Заголовок
title = QLabel("Выберите персонажа:")
title.setStyleSheet("font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Список персонажей
self.list_widget = QListWidget()
self.list_widget.itemDoubleClicked.connect(self.select_character)
layout.addWidget(self.list_widget)
# Кнопки
btn_layout = QHBoxLayout()
select_btn = QPushButton("Выбрать")
select_btn.clicked.connect(self.select_character)
btn_layout.addWidget(select_btn)
close_btn = QPushButton("Отмена")
close_btn.clicked.connect(self.close)
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
self.load_characters()
def load_characters(self):
"""Загружает список персонажей"""
characters = Agent.get_all_char_info()
for char in characters:
item = QListWidgetItem()
item.setData(Qt.UserRole, char) # Сохраняем данные персонажа
# Создаём кастомный виджет для строки списка
widget = QWidget()
widget.setFixedHeight(60)
widget_layout = QHBoxLayout(widget)
widget_layout.setContentsMargins(5, 5, 5, 5)
# Аватар
avatar_label = QLabel()
avatar_label.setFixedSize(60, 60)
avatar_path = char.get('avatar_path')
if avatar_path:
pixmap = QPixmap(avatar_path)
if not pixmap.isNull():
avatar_label.setPixmap(pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation))
else:
avatar_label.setText("❌")
avatar_label.setAlignment(Qt.AlignCenter)
else:
avatar_label.setText("Нет")
avatar_label.setAlignment(Qt.AlignCenter)
widget_layout.addWidget(avatar_label)
# Имя и ID
name = char.get('name', 'Без имени')
char_id = char.get('id', 'N/A')
info_label = QLabel(f"{name}
ID: {char_id}")
info_label.setTextFormat(Qt.RichText)
widget_layout.addWidget(info_label)
widget_layout.addStretch()
self.list_widget.addItem(item)
self.list_widget.setItemWidget(item, widget)
def select_character(self):
"""Выбирает персонажа и закрывает диалог"""
current_item = self.list_widget.currentItem()
if current_item:
self.selected_character = current_item.data(Qt.UserRole)
self.accept() # Закрывает диалог с кодом Accepted
class GenerationThread(QThread):
"""Поток для генерации ответа (чтобы не блокировать UI)"""
response_ready = Signal(str)
error_occurred = Signal(str)
def __init__(self, agent, user_input):
super().__init__()
self.agent = agent
self.user_input = user_input
def run(self):
try:
response = self.agent.respond(self.user_input)
self.response_ready.emit(response)
except Exception as e:
self.error_occurred.emit(str(e))
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# Состояние приложения
self.current_agent = None
self.current_character = None
self.generation_thread = None
# Настройка UI
self.setup_chat_ui()
# Подключение кнопок
self.ui.pushButton.clicked.connect(self.show_character_list)
self.ui.pushButton_2.clicked.connect(self.show_profile)
self.ui.pushButton_3.clicked.connect(self.show_settings)
def setup_chat_ui(self):
"""Настраивает UI чата"""
# Находим centralwidget и добавляем чат
self.chat_widget = self.ui.centralwidget
# Создаём вертикальный layout для centralwidget
chat_layout = QVBoxLayout(self.chat_widget)
chat_layout.setContentsMargins(10, 70, 10, 10) # Отступы сверху для кнопок
# Область чата (только чтение)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setPlaceholderText("Выберите персонажа для начала чата...")
chat_layout.addWidget(self.chat_display)
# Панель ввода
input_layout = QHBoxLayout()
self.message_input = QLineEdit()
self.message_input.setPlaceholderText("Введите сообщение...")
self.message_input.returnPressed.connect(self.send_message)
input_layout.addWidget(self.message_input)
self.send_button = QPushButton("Отправить")
self.send_button.clicked.connect(self.send_message)
self.send_button.setEnabled(False) # До выбора персонажа недоступна
input_layout.addWidget(self.send_button)
chat_layout.addLayout(input_layout)
def show_character_list(self):
"""Показывает диалог выбора персонажа"""
dialog = CharacterListDialog(self)
if dialog.exec() == QDialog.Accepted and dialog.selected_character:
self.select_character(dialog.selected_character)
def select_character(self, char_data):
"""Выбирает персонажа и загружает его данные"""
char_id = char_data.get('id')
try:
# Загружаем персонажа
self.current_character = Character.load(char_id)
# Создаём агента (используем заглушку для LLM)
# В реальном приложении нужно выбрать модель
self.current_agent = Agent(
self.current_character,
OllamaProvider("qwen3-vl:8b"), # Модель по умолчанию
user_name="Gloomer"
)
# Загружаем память (историю чата)
self.current_agent.load_memory()
self.current_agent.ensure_first_message()
# Обновляем UI
self.update_chat_display()
self.send_button.setEnabled(True)
self.message_input.setEnabled(True)
self.message_input.setFocus()
# Показываем информацию о выбранном персонаже
self.chat_display.append(f"Выбран персонаж: {self.current_character.display_name}\n")
except Exception as e:
self.chat_display.append(f"Ошибка загрузки персонажа: {str(e)}\n")
def update_chat_display(self):
"""Обновляет отображение истории чата"""
if not self.current_agent:
return
self.chat_display.clear()
for msg in self.current_agent.chat_history:
role = msg.get('role', '')
content = msg.get('content', '')
if role == 'user':
self.chat_display.append(f"Вы: {content}\n")
elif role == 'assistant':
self.chat_display.append(f"{self.current_character.name}: {content}\n")
def send_message(self):
"""Отправляет сообщение и получает ответ"""
if not self.current_agent or not self.message_input.text().strip():
return
user_input = self.message_input.text().strip()
self.message_input.clear()
# Показываем сообщение пользователя
self.chat_display.append(f"Вы: {user_input}\n")
self.chat_display.append("Думаю...\n")
# Блокируем ввод на время генерации
self.message_input.setEnabled(False)
self.send_button.setEnabled(False)
# Запускаем генерацию в отдельном потоке
self.generation_thread = GenerationThread(self.current_agent, user_input)
self.generation_thread.response_ready.connect(self.on_response_ready)
self.generation_thread.error_occurred.connect(self.on_error)
self.generation_thread.start()
def on_response_ready(self, response):
"""Обрабатывает готовый ответ от LLM"""
# Удаляем "Думаю..."
cursor = self.chat_display.textCursor()
cursor.movePosition(cursor.Start)
cursor.select(cursor.LineUnderCursor)
cursor.removeSelectedText()
cursor.deleteChar() # Удаляем \n
# Показываем ответ
self.chat_display.append(f"{self.current_character.name}: {response}\n")
# Сохраняем память
self.current_agent.save_memory()
# Разблокируем ввод
self.message_input.setEnabled(True)
self.send_button.setEnabled(True)
self.message_input.setFocus()
def on_error(self, error_msg):
"""Обрабатывает ошибку генерации"""
self.chat_display.append(f"Ошибка: {error_msg}\n")
# Разблокируем ввод
self.message_input.setEnabled(True)
self.send_button.setEnabled(True)
self.message_input.setFocus()
def show_profile(self):
"""Показывает профиль персонажа (заглушка)"""
if self.current_character:
self.chat_display.append(f"Профиль: {self.current_character.display_name}\n")
else:
self.chat_display.append("Сначала выберите персонажа\n")
def show_settings(self):
"""Показывает настройки инференса (заглушка)"""
if self.current_character:
self.chat_display.append(
f"Настройки:\n"
f"- Temperature: {self.current_character.temperature}\n"
f"- Max Tokens: {self.current_character.max_tokens}\n"
)
else:
self.chat_display.append("Сначала выберите персонажа\n")