""" 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}")