feat: Add Gemini AI settings and inline action buttons

- Introduced GeminiSettings component for configuring Gemini AI settings including API key, model selection, and inline suggestions.
- Updated App.vue to include GeminiSettings in the view management.
- Enhanced MonacoEditor with AI action buttons for code fixing, explaining, refactoring, and optimizing.
- Implemented responsive design for GeminiSettings and MonacoEditor components.
- Added sidebar button to toggle Gemini settings.
- Integrated API calls for saving and testing Gemini configuration.
This commit is contained in:
Shni
2025-11-04 04:40:39 -06:00
parent 38046e4df8
commit 4cf99a6f91
8 changed files with 1618 additions and 35 deletions

View File

@@ -0,0 +1,238 @@
<template>
<div class="gemini-settings">
<div class="settings-header">
<h3> Configuración de Gemini AI</h3>
<p class="subtitle">Autocompletado inteligente con Google Gemini</p>
<div class="info-box">
<p><strong>💡 Cómo usar Gemini:</strong></p>
<ul>
<li><strong>Inline Suggestions:</strong> Escribe código y aparecerán sugerencias en gris. Presiona <kbd>Tab</kbd> para aceptar.</li>
<li><strong>Modo Thinking:</strong> Activa para código complejo. El modelo "piensa" antes de sugerir (más lento pero más preciso).</li>
</ul>
</div>
</div> <div class="settings-content">
<div class="status-badge" :class="{ active: isConfigured, inactive: !isConfigured }">
<span class="status-dot"></span>
{{ isConfigured ? '✓ Configurado' : '○ No configurado' }}
</div>
<div class="form-group">
<label for="model">Modelo de IA</label>
<select id="model" v-model="selectedModel" class="model-select" @change="hasChanges = true">
<option value="gemini-2.5-flash"> Gemini 2.5 Flash (Rápido)</option>
<option value="gemini-2.5-pro">🚀 Gemini 2.5 Pro (Potente)</option>
<option value="gemini-1.5-flash"> Gemini 1.5 Flash (Legacy)</option>
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="agentMode" @change="hasChanges = true" />
<span class="checkbox-text">
<strong>🧠 Modo Thinking (Experimental)</strong>
<small>El modelo razona internamente antes de sugerir código (más tokens, más lento)</small>
</span>
</label>
</div>
<div class="form-group">
<label class="checkbox-label warning-label">
<input type="checkbox" v-model="inlineSuggestionsEnabled" @change="hasChanges = true" />
<span class="checkbox-text">
<strong> Autocompletado Inline (Alto consumo de tokens)</strong>
<small>
<span class="warning-text"> ADVERTENCIA:</span> Genera sugerencias automáticamente mientras escribes.
Cada pausa de 500ms hace una llamada a la API. Puede consumir muchos tokens rápidamente.
<br><strong>Recomendación:</strong> Usa solo los botones de acción (Fix, Explain, etc.) para ahorrar tokens.
</small>
</span>
</label>
</div>
<div class="form-group">
<label for="apiKey">API Key de Google</label>
<div class="input-wrapper">
<input id="apiKey" v-model="apiKey" :type="showApiKey ? 'text' : 'password'" placeholder="Ingresa tu API key" class="api-key-input" @input="hasChanges = true" />
<button class="toggle-visibility-btn" @click="showApiKey = !showApiKey" type="button">{{ showApiKey ? '👁️' : '👁️‍🗨️' }}</button>
</div>
<span class="helper-text">
<a href="https://aistudio.google.com/app/apikey" target="_blank" class="link">Obtén tu API key gratis aquí</a>
</span>
</div>
<div class="actions">
<button class="btn btn-primary" :disabled="!hasChanges || !apiKey" @click="saveSettings">💾 Guardar</button>
<button v-if="isConfigured" class="btn btn-test" @click="testConnection" :disabled="testing">{{ testing ? ' Probando...' : '🧪 Probar' }}</button>
</div>
<div v-if="message" class="message" :class="messageType">{{ message }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { appDataDir } from '@tauri-apps/api/path';
const apiKey = ref('');
const showApiKey = ref(false);
const selectedModel = ref('gemini-2.5-flash');
const agentMode = ref(false);
const inlineSuggestionsEnabled = ref(false);
const hasChanges = ref(false);
const isConfigured = ref(false);
const testing = ref(false);
const message = ref('');
const messageType = ref<'success' | 'error' | 'info'>('info');
onMounted(async () => {
try {
const dataDir = await appDataDir();
const configJson = await invoke<string>('load_gemini_config', { appDataDir: dataDir });
const config = JSON.parse(configJson);
if (config.api_key) {
apiKey.value = config.api_key;
selectedModel.value = config.model || 'gemini-2.5-flash';
agentMode.value = config.agent_mode || false;
inlineSuggestionsEnabled.value = config.inline_suggestions_enabled || false;
isConfigured.value = true;
localStorage.setItem('gemini_api_key', config.api_key);
localStorage.setItem('gemini_model', config.model);
localStorage.setItem('gemini_agent_mode', config.agent_mode ? 'true' : 'false');
localStorage.setItem('gemini_inline_suggestions', config.inline_suggestions_enabled ? 'true' : 'false');
}
} catch (error) {
console.log('No hay configuración previa');
}
});
async function saveSettings() {
if (!apiKey.value) {
showMessage('Por favor ingresa una API key', 'error');
return;
}
try {
const dataDir = await appDataDir();
await invoke('save_gemini_config', {
apiKey: apiKey.value,
model: selectedModel.value,
appDataDir: dataDir,
agentMode: agentMode.value,
inlineSuggestionsEnabled: inlineSuggestionsEnabled.value
});
localStorage.setItem('gemini_api_key', apiKey.value);
localStorage.setItem('gemini_model', selectedModel.value);
localStorage.setItem('gemini_agent_mode', agentMode.value ? 'true' : 'false');
localStorage.setItem('gemini_inline_suggestions', inlineSuggestionsEnabled.value ? 'true' : 'false');
isConfigured.value = true;
hasChanges.value = false;
showMessage('✅ Configuración guardada', 'success');
} catch (error) {
showMessage(`❌ Error: ${error}`, 'error');
}
}
async function testConnection() {
if (!apiKey.value) return;
testing.value = true;
showMessage('🔍 Probando conexión...', 'info');
try {
const result = await invoke<string[]>('get_gemini_completion', {
text: 'function hello() {\n ',
cursorPosition: 21,
language: 'javascript',
filePath: 'test.js',
apiKey: apiKey.value,
model: selectedModel.value,
agentMode: false // Siempre desactivar thinking en el test para que sea rápido
});
if (result && result.length > 0) {
showMessage(`✅ Funciona! Sugerencia: "${result[0].substring(0, 30)}..."`, 'success');
} else {
showMessage('⚠️ Sin respuesta. Verifica tu API key o aumenta max_output_tokens', 'error');
}
} catch (error) {
showMessage(`❌ Error: ${error}`, 'error');
} finally {
testing.value = false;
}
}
function showMessage(msg: string, type: 'success' | 'error' | 'info') {
message.value = msg;
messageType.value = type;
if (type === 'success') setTimeout(() => message.value = '', 5000);
}
</script>
<style scoped>
.gemini-settings { padding: 30px; max-width: 700px; margin: 0 auto; color: #e0e0e0; background: #1e1e1e; min-height: 100vh; }
.settings-header { margin-bottom: 30px; border-bottom: 2px solid rgba(66, 133, 244, 0.3); padding-bottom: 15px; }
.settings-header h3 { margin: 0 0 8px 0; font-size: 28px; color: #fff; font-weight: 600; background: linear-gradient(135deg, #4285f4 0%, #34a853 50%, #fbbc04 75%, #ea4335 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.subtitle { margin: 0 0 15px 0; color: #b0b0b0; font-size: 15px; }
.info-box { background: rgba(66, 133, 244, 0.1); border: 2px solid rgba(66, 133, 244, 0.3); border-radius: 8px; padding: 16px; margin-top: 15px; }
.info-box p { margin: 0 0 10px 0; color: #fff; font-size: 14px; }
.info-box ul { margin: 0; padding-left: 20px; list-style: none; }
.info-box li { margin: 8px 0; color: #e0e0e0; font-size: 13px; line-height: 1.6; position: relative; padding-left: 8px; }
.info-box li::before { content: "•"; position: absolute; left: -12px; color: #4285f4; font-weight: bold; }
.info-box kbd { background: #2d2d30; padding: 2px 6px; border-radius: 4px; border: 1px solid #3e3e42; font-family: monospace; font-size: 12px; color: #4285f4; }
.settings-content { display: flex; flex-direction: column; gap: 20px; }
.status-badge { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 20px; font-size: 14px; font-weight: 600; width: fit-content; border: 2px solid; }
.status-badge.active { background: rgba(52, 168, 83, 0.25); color: #34a853; border-color: rgba(52, 168, 83, 0.5); }
.status-badge.inactive { background: rgba(158, 158, 158, 0.15); color: #ccc; border-color: rgba(158, 158, 158, 0.3); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.form-group { display: flex; flex-direction: column; gap: 8px; }
.form-group label { font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 8px; }
.checkbox-label { display: flex; align-items: flex-start; gap: 12px; cursor: pointer; padding: 12px; background: #2d2d30; border-radius: 8px; border: 2px solid #3e3e42; transition: all 0.2s; }
.checkbox-label:hover { border-color: #4285f4; background: #353538; }
.checkbox-label.warning-label { border-color: #fbbc04; background: rgba(251, 188, 4, 0.05); }
.checkbox-label.warning-label:hover { border-color: #ffc928; background: rgba(251, 188, 4, 0.1); }
.checkbox-label input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; accent-color: #4285f4; margin-top: 2px; }
.checkbox-text { display: flex; flex-direction: column; gap: 4px; flex: 1; }
.checkbox-text strong { color: #fff; font-size: 14px; }
.checkbox-text small { color: #b0b0b0; font-size: 12px; line-height: 1.4; }
.checkbox-text .warning-text { color: #fbbc04; font-weight: 500; }
.checkbox-text .warning-text::before { content: "⚠️ "; }
.model-select, .api-key-input { padding: 12px 16px; background: #2d2d30; color: #e0e0e0; border: 2px solid #3e3e42; border-radius: 6px; font-size: 14px; transition: all 0.2s; }
.model-select:focus, .api-key-input:focus { outline: none; border-color: #4285f4; box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1); }
.input-wrapper { display: flex; gap: 10px; }
.api-key-input { flex: 1; font-family: 'Consolas', monospace; }
.toggle-visibility-btn { padding: 12px 16px; background: #3e3e42; color: #e0e0e0; border: 2px solid #3e3e42; border-radius: 6px; cursor: pointer; font-size: 18px; transition: all 0.2s; }
.toggle-visibility-btn:hover { background: #4e4e52; border-color: #4285f4; transform: scale(1.05); }
.helper-text { font-size: 13px; color: #b0b0b0; }
.link { color: #4285f4; text-decoration: none; font-weight: 500; }
.link:hover { color: #5a9dff; text-decoration: underline; }
.actions { display: flex; gap: 12px; margin-top: 10px; }
.btn { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: linear-gradient(135deg, #4285f4 0%, #34a853 100%); color: white; }
.btn-primary:hover:not(:disabled) { background: linear-gradient(135deg, #5a9dff 0%, #46ba65 100%); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4); }
.btn-test { background: #2d2d30; color: #e0e0e0; border: 2px solid #4285f4; }
.btn-test:hover:not(:disabled) { background: #3e3e42; transform: translateY(-2px); }
.message { padding: 14px 18px; border-radius: 8px; font-size: 14px; font-weight: 500; border: 2px solid; animation: slideIn 0.3s ease; }
.message.success { background: rgba(52, 168, 83, 0.2); color: #34a853; border-color: rgba(52, 168, 83, 0.5); }
.message.error { background: rgba(234, 67, 53, 0.2); color: #ea4335; border-color: rgba(234, 67, 53, 0.5); }
.message.info { background: rgba(66, 133, 244, 0.2); color: #4285f4; border-color: rgba(66, 133, 244, 0.5); }
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
/* Responsive Design */
@media (max-width: 768px) {
.gemini-settings { padding: 20px; max-width: 100%; }
.settings-header h3 { font-size: 24px; }
.subtitle { font-size: 14px; }
.info-box { padding: 12px; }
.info-box p { font-size: 13px; }
.info-box li { font-size: 12px; }
.actions { flex-direction: column; }
.btn { width: 100%; }
.input-wrapper { flex-direction: column; }
.toggle-visibility-btn { width: 100%; }
}
@media (min-width: 1920px) {
.gemini-settings { max-width: 900px; padding: 40px; }
.settings-header h3 { font-size: 32px; }
.subtitle { font-size: 17px; }
}
</style>