AI_MMI_Analyser/app/ui/dialogs/api_key_dialog.py

522 lines
17 KiB
Python

"""
API 키 설정 다이얼로그
AI 서비스 API 키 입력 및 관리 UI
"""
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QComboBox, QPushButton,
QTabWidget, QWidget, QGroupBox, QMessageBox,
QFrame, QSizePolicy
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QIcon
from app.core.settings import get_settings_manager, AISettings
from app.ai import AIProviderType, get_ai_client
class ProviderTab(QWidget):
"""개별 프로바이더 설정 탭"""
api_key_changed = Signal(str, str) # provider_type, api_key
def __init__(self, provider_type: str, provider_name: str, available_models: list, parent=None):
super().__init__(parent)
self.provider_type = provider_type
self.provider_name = provider_name
self.available_models = available_models
self.setup_ui()
self.load_settings()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(16)
layout.setContentsMargins(20, 20, 20, 20)
# 프로바이더 정보
info_group = QGroupBox(f"{self.provider_name} 설정")
info_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: bold;
border: 1px solid #3d3d3d;
border-radius: 8px;
margin-top: 12px;
padding-top: 12px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 8px;
}
""")
info_layout = QGridLayout(info_group)
info_layout.setSpacing(12)
info_layout.setContentsMargins(16, 24, 16, 16)
# API Key 입력
api_key_label = QLabel("API Key:")
api_key_label.setStyleSheet("font-weight: bold;")
self.api_key_input = QLineEdit()
self.api_key_input.setPlaceholderText("API 키를 입력하세요...")
self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
self.api_key_input.setMinimumHeight(36)
self.api_key_input.setStyleSheet("""
QLineEdit {
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
background: #2d2d2d;
color: #fff;
font-size: 13px;
}
QLineEdit:focus {
border-color: #0078d4;
}
""")
# API Key 보기/숨기기 버튼
self.toggle_visibility_btn = QPushButton("👁")
self.toggle_visibility_btn.setFixedSize(36, 36)
self.toggle_visibility_btn.setCheckable(True)
self.toggle_visibility_btn.setStyleSheet("""
QPushButton {
border: 1px solid #555;
border-radius: 6px;
background: #2d2d2d;
}
QPushButton:checked {
background: #3d3d3d;
}
QPushButton:hover {
background: #404040;
}
""")
self.toggle_visibility_btn.clicked.connect(self.toggle_api_key_visibility)
api_key_row = QHBoxLayout()
api_key_row.addWidget(self.api_key_input)
api_key_row.addWidget(self.toggle_visibility_btn)
info_layout.addWidget(api_key_label, 0, 0)
info_layout.addLayout(api_key_row, 0, 1)
# 모델 선택
model_label = QLabel("모델:")
model_label.setStyleSheet("font-weight: bold;")
self.model_combo = QComboBox()
self.model_combo.addItems(self.available_models)
self.model_combo.setMinimumHeight(36)
self.model_combo.setStyleSheet("""
QComboBox {
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
background: #2d2d2d;
color: #fff;
font-size: 13px;
}
QComboBox:hover {
border-color: #666;
}
QComboBox::drop-down {
border: none;
width: 30px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #888;
margin-right: 10px;
}
QComboBox QAbstractItemView {
background: #2d2d2d;
border: 1px solid #555;
selection-background-color: #0078d4;
}
""")
info_layout.addWidget(model_label, 1, 0)
info_layout.addWidget(self.model_combo, 1, 1)
info_layout.setColumnStretch(1, 1)
layout.addWidget(info_group)
# 테스트 버튼
test_layout = QHBoxLayout()
test_layout.addStretch()
self.test_btn = QPushButton("🔗 연결 테스트")
self.test_btn.setMinimumHeight(40)
self.test_btn.setMinimumWidth(140)
self.test_btn.setStyleSheet("""
QPushButton {
padding: 10px 20px;
border: none;
border-radius: 6px;
background: #0078d4;
color: white;
font-size: 13px;
font-weight: bold;
}
QPushButton:hover {
background: #1084d8;
}
QPushButton:pressed {
background: #006cc1;
}
QPushButton:disabled {
background: #555;
color: #888;
}
""")
self.test_btn.clicked.connect(self.test_connection)
test_layout.addWidget(self.test_btn)
layout.addLayout(test_layout)
# 상태 표시
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #888; font-size: 12px;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.status_label)
layout.addStretch()
def toggle_api_key_visibility(self, checked):
"""API 키 표시/숨기기 토글"""
if checked:
self.api_key_input.setEchoMode(QLineEdit.EchoMode.Normal)
self.toggle_visibility_btn.setText("🔒")
else:
self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
self.toggle_visibility_btn.setText("👁")
def load_settings(self):
"""설정에서 값 로드"""
settings = get_settings_manager().ai_settings
api_key = settings.get_api_key(self.provider_type)
if api_key:
self.api_key_input.setText(api_key)
model = settings.get_model(self.provider_type)
if model:
index = self.model_combo.findText(model)
if index >= 0:
self.model_combo.setCurrentIndex(index)
def save_settings(self):
"""설정 저장"""
settings_manager = get_settings_manager()
settings = settings_manager.ai_settings
settings.set_api_key(self.provider_type, self.api_key_input.text())
settings.set_model(self.provider_type, self.model_combo.currentText())
settings_manager.save()
def test_connection(self):
"""연결 테스트"""
api_key = self.api_key_input.text().strip()
if not api_key:
self.status_label.setText("❌ API 키를 입력하세요")
self.status_label.setStyleSheet("color: #ff5252; font-size: 12px;")
return
self.status_label.setText("⏳ 연결 테스트 중...")
self.status_label.setStyleSheet("color: #ffc107; font-size: 12px;")
self.test_btn.setEnabled(False)
# 테스트 실행 (간단한 동기 테스트)
try:
from app.ai import AIClient, AIProviderType
client = AIClient()
provider_type = AIProviderType(self.provider_type)
model = self.model_combo.currentText()
if client.set_provider(provider_type, api_key, model):
# 간단한 테스트 메시지
response = client.chat("Hello", max_tokens=10)
self.status_label.setText(f"✅ 연결 성공! ({response.model})")
self.status_label.setStyleSheet("color: #4caf50; font-size: 12px;")
else:
self.status_label.setText("❌ 초기화 실패")
self.status_label.setStyleSheet("color: #ff5252; font-size: 12px;")
except Exception as e:
error_msg = str(e)[:50] + "..." if len(str(e)) > 50 else str(e)
self.status_label.setText(f"❌ 오류: {error_msg}")
self.status_label.setStyleSheet("color: #ff5252; font-size: 12px;")
finally:
self.test_btn.setEnabled(True)
def get_api_key(self) -> str:
return self.api_key_input.text().strip()
def get_model(self) -> str:
return self.model_combo.currentText()
class APIKeyDialog(QDialog):
"""API 키 설정 다이얼로그"""
settings_changed = Signal()
# 프로바이더별 정보
PROVIDERS = [
{
"type": "openai",
"name": "OpenAI",
"models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo", "o1-preview", "o1-mini"],
},
{
"type": "openrouter",
"name": "OpenRouter",
"models": [
"openai/gpt-4o", "openai/gpt-4o-mini",
"anthropic/claude-3.5-sonnet", "anthropic/claude-3-opus",
"google/gemini-pro-1.5", "google/gemini-flash-1.5",
"meta-llama/llama-3.1-70b-instruct", "meta-llama/llama-3.1-8b-instruct",
"mistralai/mixtral-8x7b-instruct", "deepseek/deepseek-chat",
],
},
{
"type": "gemini",
"name": "Google Gemini",
"models": ["gemini-2.0-flash-exp", "gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b", "gemini-1.0-pro"],
},
{
"type": "xai",
"name": "xAI (Grok)",
"models": ["grok-beta", "grok-2-1212", "grok-2-vision-1212"],
},
]
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("AI API 키 설정")
self.setMinimumSize(550, 450)
self.setModal(True)
self.provider_tabs = {}
self.setup_ui()
self.load_current_provider()
def setup_ui(self):
self.setStyleSheet("""
QDialog {
background: #1e1e1e;
color: #fff;
}
QLabel {
color: #ddd;
}
QTabWidget::pane {
border: 1px solid #3d3d3d;
border-radius: 8px;
background: #252525;
}
QTabBar::tab {
background: #2d2d2d;
color: #888;
padding: 10px 20px;
margin-right: 2px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
QTabBar::tab:selected {
background: #252525;
color: #fff;
}
QTabBar::tab:hover {
background: #353535;
color: #ccc;
}
""")
layout = QVBoxLayout(self)
layout.setSpacing(16)
layout.setContentsMargins(20, 20, 20, 20)
# 헤더
header = QLabel("🔑 AI API 키 설정")
header.setStyleSheet("""
font-size: 18px;
font-weight: bold;
color: #fff;
padding: 8px 0;
""")
layout.addWidget(header)
# 현재 프로바이더 선택
provider_layout = QHBoxLayout()
provider_label = QLabel("기본 프로바이더:")
provider_label.setStyleSheet("font-weight: bold;")
self.provider_combo = QComboBox()
for provider in self.PROVIDERS:
self.provider_combo.addItem(provider["name"], provider["type"])
self.provider_combo.setMinimumHeight(36)
self.provider_combo.setMinimumWidth(200)
self.provider_combo.setStyleSheet("""
QComboBox {
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
background: #2d2d2d;
color: #fff;
font-size: 13px;
}
QComboBox:hover {
border-color: #666;
}
QComboBox::drop-down {
border: none;
width: 30px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #888;
margin-right: 10px;
}
QComboBox QAbstractItemView {
background: #2d2d2d;
border: 1px solid #555;
selection-background-color: #0078d4;
}
""")
provider_layout.addWidget(provider_label)
provider_layout.addWidget(self.provider_combo)
provider_layout.addStretch()
layout.addLayout(provider_layout)
# 탭 위젯
self.tab_widget = QTabWidget()
for provider in self.PROVIDERS:
tab = ProviderTab(
provider_type=provider["type"],
provider_name=provider["name"],
available_models=provider["models"]
)
self.provider_tabs[provider["type"]] = tab
self.tab_widget.addTab(tab, provider["name"])
layout.addWidget(self.tab_widget)
# 버튼
button_layout = QHBoxLayout()
button_layout.addStretch()
cancel_btn = QPushButton("취소")
cancel_btn.setMinimumHeight(40)
cancel_btn.setMinimumWidth(100)
cancel_btn.setStyleSheet("""
QPushButton {
padding: 10px 24px;
border: 1px solid #555;
border-radius: 6px;
background: #2d2d2d;
color: #ddd;
font-size: 13px;
}
QPushButton:hover {
background: #3d3d3d;
border-color: #666;
}
""")
cancel_btn.clicked.connect(self.reject)
save_btn = QPushButton("저장")
save_btn.setMinimumHeight(40)
save_btn.setMinimumWidth(100)
save_btn.setStyleSheet("""
QPushButton {
padding: 10px 24px;
border: none;
border-radius: 6px;
background: #0078d4;
color: white;
font-size: 13px;
font-weight: bold;
}
QPushButton:hover {
background: #1084d8;
}
QPushButton:pressed {
background: #006cc1;
}
""")
save_btn.clicked.connect(self.save_and_close)
button_layout.addWidget(cancel_btn)
button_layout.addWidget(save_btn)
layout.addLayout(button_layout)
def load_current_provider(self):
"""현재 설정된 프로바이더 로드"""
settings = get_settings_manager().ai_settings
current = settings.get_current_provider()
if current:
index = self.provider_combo.findData(current)
if index >= 0:
self.provider_combo.setCurrentIndex(index)
# 해당 탭으로 전환
for i, provider in enumerate(self.PROVIDERS):
if provider["type"] == current:
self.tab_widget.setCurrentIndex(i)
break
def save_and_close(self):
"""설정 저장 후 닫기"""
settings_manager = get_settings_manager()
# 모든 탭의 설정 저장
for provider_type, tab in self.provider_tabs.items():
tab.save_settings()
# 기본 프로바이더 설정
current_provider = self.provider_combo.currentData()
settings_manager.ai_settings.set_current_provider(current_provider)
settings_manager.save()
# AI 클라이언트 업데이트
self._update_ai_client()
self.settings_changed.emit()
self.accept()
def _update_ai_client(self):
"""저장된 설정으로 AI 클라이언트 업데이트"""
try:
from app.ai import get_ai_client, AIProviderType
settings = get_settings_manager().ai_settings
current_provider = settings.get_current_provider()
if current_provider:
api_key = settings.get_api_key(current_provider)
model = settings.get_model(current_provider)
if api_key:
client = get_ai_client()
provider_type = AIProviderType(current_provider)
client.set_provider(provider_type, api_key, model)
except Exception as e:
print(f"AI 클라이언트 업데이트 실패: {e}")