Files
amayo/AEditor/src/components/GeminiSettings.vue
Shni 4cf99a6f91 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.
2025-11-04 04:40:39 -06:00

239 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>