import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js'; export class ApiKeyHeader extends LitElement { static properties = { apiKey: { type: String }, isLoading: { type: Boolean }, errorMessage: { type: String }, selectedProvider: { type: String }, }; static styles = css` :host { display: block; transform: translate3d(0, 0, 0); backface-visibility: hidden; transition: opacity 0.25s ease-out; } :host(.sliding-out) { animation: slideOutUp 0.3s ease-in forwards; will-change: opacity, transform; } :host(.hidden) { opacity: 0; pointer-events: none; } @keyframes slideOutUp { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } } * { font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; cursor: default; user-select: none; box-sizing: border-box; } .container { width: 285px; min-height: 260px; padding: 18px 20px; background: rgba(0, 0, 0, 0.3); border-radius: 16px; overflow: visible; position: relative; display: flex; flex-direction: column; align-items: center; } .container::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 16px; padding: 1px; background: linear-gradient(169deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.5) 100%); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: destination-out; mask-composite: exclude; pointer-events: none; } .close-button { position: absolute; top: 10px; right: 10px; width: 14px; height: 14px; background: rgba(255, 255, 255, 0.1); border: none; border-radius: 3px; color: rgba(255, 255, 255, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; z-index: 10; font-size: 14px; line-height: 1; padding: 0; } .close-button:hover { background: rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.9); } .close-button:active { transform: scale(0.95); } .title { color: white; font-size: 16px; font-weight: 500; /* Medium */ margin: 0; text-align: center; flex-shrink: 0; } .form-content { display: flex; flex-direction: column; align-items: center; width: 100%; margin-top: auto; } .error-message { color: rgba(239, 68, 68, 0.9); font-weight: 500; font-size: 11px; height: 14px; text-align: center; margin-bottom: 4px; } .api-input { width: 100%; height: 34px; background: rgba(255, 255, 255, 0.1); border-radius: 10px; border: none; padding: 0 10px; color: white; font-size: 12px; font-weight: 400; /* Regular */ margin-bottom: 6px; text-align: center; user-select: text; cursor: text; } .api-input::placeholder { color: rgba(255, 255, 255, 0.6); } .api-input:focus { outline: none; } .provider-select { width: 100%; height: 34px; background: rgba(255, 255, 255, 0.1); border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.2); padding: 0 10px; color: white; font-size: 12px; font-weight: 400; margin-bottom: 6px; text-align: center; cursor: pointer; -webkit-appearance: none; -moz-appearance: none; appearance: none; background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2714%27%20height%3D%278%27%20viewBox%3D%270%200%2014%208%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%3E%3Cpath%20d%3D%27M1%201l6%206%206-6%27%20stroke%3D%27%23ffffff%27%20stroke-width%3D%271.5%27%20fill%3D%27none%27%20fill-rule%3D%27evenodd%27/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: right 10px center; background-size: 12px; padding-right: 30px; } .provider-select:hover { background-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.3); } .provider-select:focus { outline: none; background-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.4); } .provider-select option { background: #1a1a1a; color: white; padding: 5px; } .action-button { width: 100%; height: 34px; background: rgba(255, 255, 255, 0.2); border: none; border-radius: 10px; color: white; font-size: 12px; font-weight: 500; /* Medium */ cursor: pointer; transition: background 0.15s ease; position: relative; overflow: visible; } .action-button::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 10px; padding: 1px; background: linear-gradient(169deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.5) 100%); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: destination-out; mask-composite: exclude; pointer-events: none; } .action-button:hover { background: rgba(255, 255, 255, 0.3); } .action-button:disabled { opacity: 0.5; cursor: not-allowed; } .or-text { color: rgba(255, 255, 255, 0.5); font-size: 12px; font-weight: 500; /* Medium */ margin: 10px 0; } .provider-label { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 400; margin-bottom: 4px; width: 100%; text-align: left; } `; constructor() { super(); this.dragState = null; this.wasJustDragged = false; this.apiKey = ''; this.isLoading = false; this.errorMessage = ''; this.validatedApiKey = null; this.selectedProvider = 'openai'; this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseUp = this.handleMouseUp.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleInput = this.handleInput.bind(this); this.handleAnimationEnd = this.handleAnimationEnd.bind(this); this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this); this.handleProviderChange = this.handleProviderChange.bind(this); this.checkAndRequestPermissions = this.checkAndRequestPermissions.bind(this); } reset() { this.apiKey = ''; this.isLoading = false; this.errorMessage = ''; this.validatedApiKey = null; this.selectedProvider = 'openai'; this.requestUpdate(); } async handleMouseDown(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') { return; } e.preventDefault(); const { ipcRenderer } = window.require('electron'); const initialPosition = await ipcRenderer.invoke('get-header-position'); this.dragState = { initialMouseX: e.screenX, initialMouseY: e.screenY, initialWindowX: initialPosition.x, initialWindowY: initialPosition.y, moved: false, }; window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('mouseup', this.handleMouseUp, { once: true }); } handleMouseMove(e) { if (!this.dragState) return; const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX); const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY); if (deltaX > 3 || deltaY > 3) { this.dragState.moved = true; } const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX); const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY); const { ipcRenderer } = window.require('electron'); ipcRenderer.invoke('move-header-to', newWindowX, newWindowY); } handleMouseUp(e) { if (!this.dragState) return; const wasDragged = this.dragState.moved; window.removeEventListener('mousemove', this.handleMouseMove); this.dragState = null; if (wasDragged) { this.wasJustDragged = true; setTimeout(() => { this.wasJustDragged = false; }, 200); } } handleInput(e) { this.apiKey = e.target.value; this.errorMessage = ''; console.log('Input changed:', this.apiKey?.length || 0, 'chars'); this.requestUpdate(); this.updateComplete.then(() => { const inputField = this.shadowRoot?.querySelector('.apikey-input'); if (inputField && this.isInputFocused) { inputField.focus(); } }); } handleProviderChange(e) { this.selectedProvider = e.target.value; this.errorMessage = ''; console.log('Provider changed to:', this.selectedProvider); this.requestUpdate(); } handlePaste(e) { e.preventDefault(); this.errorMessage = ''; const clipboardText = (e.clipboardData || window.clipboardData).getData('text'); console.log('Paste event detected:', clipboardText?.substring(0, 10) + '...'); if (clipboardText) { this.apiKey = clipboardText.trim(); const inputElement = e.target; inputElement.value = this.apiKey; } this.requestUpdate(); this.updateComplete.then(() => { const inputField = this.shadowRoot?.querySelector('.apikey-input'); if (inputField) { inputField.focus(); inputField.setSelectionRange(inputField.value.length, inputField.value.length); } }); } handleKeyPress(e) { if (e.key === 'Enter') { e.preventDefault(); this.handleSubmit(); } } async handleSubmit() { if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) { console.log('Submit blocked:', { wasJustDragged: this.wasJustDragged, isLoading: this.isLoading, hasApiKey: !!this.apiKey.trim(), }); return; } console.log('Starting API key validation...'); this.isLoading = true; this.errorMessage = ''; this.requestUpdate(); const apiKey = this.apiKey.trim(); let isValid = false; try { const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider); if (isValid) { console.log('API key valid – checking system permissions…'); const permissionResult = await this.checkAndRequestPermissions(); if (permissionResult.success) { console.log('All permissions granted – starting slide-out animation'); this.startSlideOutAnimation(); this.validatedApiKey = this.apiKey.trim(); this.validatedProvider = this.selectedProvider; } else { this.errorMessage = permissionResult.error || 'Permission setup required'; console.log('Permission setup incomplete:', permissionResult); } } else { this.errorMessage = 'Invalid API key - please check and try again'; console.log('API key validation failed'); } } catch (error) { console.error('API key validation error:', error); this.errorMessage = 'Validation error - please try again'; } finally { this.isLoading = false; this.requestUpdate(); } } async validateApiKey(apiKey, provider = 'openai') { if (!apiKey || apiKey.length < 15) return false; if (provider === 'openai') { if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false; try { console.log('Validating OpenAI API key...'); const response = await fetch('https://api.openai.com/v1/models', { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, }); if (response.ok) { const data = await response.json(); const hasGPTModels = data.data && data.data.some(m => m.id.startsWith('gpt-')); if (hasGPTModels) { console.log('OpenAI API key validation successful'); return true; } else { console.log('API key valid but no GPT models available'); return false; } } else { const errorData = await response.json().catch(() => ({})); console.log('API key validation failed:', response.status, errorData.error?.message || 'Unknown error'); return false; } } catch (error) { console.error('API key validation network error:', error); return apiKey.length >= 20; // Fallback for network issues } } else if (provider === 'gemini') { // Gemini API keys typically start with 'AIza' if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false; try { console.log('Validating Gemini API key...'); // Test the API key with a simple models list request const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`); if (response.ok) { const data = await response.json(); if (data.models && data.models.length > 0) { console.log('Gemini API key validation successful'); return true; } } console.log('Gemini API key validation failed'); return false; } catch (error) { console.error('Gemini API key validation network error:', error); return apiKey.length >= 20; // Fallback } } return false; } async checkAndRequestPermissions() { if (!window.require) return { success: true }; const { ipcRenderer } = window.require('electron'); try { const permissions = await ipcRenderer.invoke('check-system-permissions'); console.log('[Permissions] Current status:', permissions); if (!permissions.needsSetup) return { success: true }; if (!permissions.microphone) { console.log('[Permissions] Requesting microphone permission…'); const micResult = await ipcRenderer.invoke('request-microphone-permission'); if (!micResult.success) { await ipcRenderer.invoke('open-system-preferences', 'microphone'); return { success: false, error: 'Please grant microphone access in System Preferences', }; } } if (!permissions.screen) { console.log('[Permissions] Screen-recording permission needed'); await ipcRenderer.invoke('open-system-preferences', 'screen-recording'); return { success: false, error: 'Please grant screen recording access in System Preferences', }; } return { success: true }; } catch (err) { console.error('[Permissions] Error checking/requesting permissions:', err); return { success: false, error: 'Failed to check permissions' }; } } startSlideOutAnimation() { this.classList.add('sliding-out'); } handleUsePicklesKey(e) { e.preventDefault(); if (this.wasJustDragged) return; console.log('Requesting Firebase authentication from main process...'); if (window.require) { window.require('electron').ipcRenderer.invoke('start-firebase-auth'); } } handleClose() { console.log('Close button clicked'); if (window.require) { window.require('electron').ipcRenderer.invoke('quit-application'); } } handleAnimationEnd(e) { if (e.target !== this) return; if (this.classList.contains('sliding-out')) { this.classList.remove('sliding-out'); this.classList.add('hidden'); if (this.validatedApiKey) { if (window.require) { window.require('electron').ipcRenderer.invoke('api-key-validated', { apiKey: this.validatedApiKey, provider: this.validatedProvider || 'openai' }); } this.validatedApiKey = null; this.validatedProvider = null; } } } connectedCallback() { super.connectedCallback(); this.addEventListener('animationend', this.handleAnimationEnd); } disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener('animationend', this.handleAnimationEnd); } render() { const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim(); console.log('Rendering with provider:', this.selectedProvider); return html`

Choose how to power your AI

${this.errorMessage}
Select AI Provider:
(this.errorMessage = '')} ?disabled=${this.isLoading} autocomplete="off" spellcheck="false" tabindex="0" />
or
`; } } customElements.define('apikey-header', ApiKeyHeader);