522 lines
17 KiB
Python
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}")
|
|
|