Remove all AI integrations and OpenAI libraries/functions and so on..

This commit is contained in:
2026-03-09 13:16:18 +01:00
parent f108605bcb
commit 127ac49e9c
41 changed files with 191 additions and 2135 deletions
+2 -1
View File
@@ -3,4 +3,5 @@ SERVER_PORT=6989
HTTPS_PORT=5878
NODE_ENV=development
STRICT_TLS=true
ENABLE_SOURCE_SYNC=false
ENABLE_SOURCE_SYNC=false
ENABLE_VERSION_CHECK=true
+10 -2
View File
@@ -92,10 +92,18 @@ The server listens on port 6989 by default. You can modify this behavior using e
- `SERVER_PORT`: Server listening port (default: 6989)
- `NODE_ENV`: Runtime environment (development/production)
- `ENCRYPTION_KEY`: Encryption key for passwords, SSH keys and passphrases. Supports Docker secrets via /run/secrets/encryption_key`
- `AI_SYSTEM_PROMPT`: System prompt for AI features (example: You are a Linux command generator assistant.)
- `LOG_LEVEL`: Logging level for application and guacd (system/info/verbose/debug/warn/error, default: system)
- `STRICT_TLS`: Enforce TLS certificate validation for outbound integrations like Proxmox and LDAP (default: true)
- `ENABLE_SOURCE_SYNC`: Enable automatic external source synchronization and default official source creation (default: false)
- `ENABLE_SOURCE_SYNC`: Enable source synchronization requests and default official source creation (default: false)
- `ENABLE_VERSION_CHECK`: Allow GitHub version check endpoint (`/api/service/version/check`) (default: true)
- `VITE_ENABLE_EXTERNAL_LINKS`: Allow opening external links from the web UI (default: false)
### Offline Runtime Defaults
- AI assistant features are removed from the productive app runtime.
- External source synchronization is disabled by default (`ENABLE_SOURCE_SYNC=false`).
- External link opening in the web client is disabled by default (`VITE_ENABLE_EXTERNAL_LINKS=false`).
- GitHub version check remains available through `/api/service/version/check` and can be disabled with `ENABLE_VERSION_CHECK=false`.
## Security
+1
View File
@@ -0,0 +1 @@
VITE_ENABLE_EXTERNAL_LINKS=false
+2 -65
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "إلغاء",
"create": "إنشاء",
"save": "حفظ",
"generateAI": "توليد باستخدام الذكاء الاصطناعي",
"generating": "جارٍ التوليد..."
"save": "حفظ"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "المستخدمون",
"authentication": "المصادقة",
"sources": "المصادر",
"ai": "الذكاء الاصطناعي",
"monitoring": "الرصد",
"backup": "التخزين"
},
@@ -725,10 +722,6 @@
"title": "إجراء سريع",
"description": "افتح قائمة الإجراءات السريعة"
},
"aimenu": {
"title": "قائمة الذكاء الاصطناعي",
"description": "افتح قائمة أوامر الذكاء الاصطناعي"
},
"snippets": {
"title": "مقتطفات",
"description": "افتح مربع حوار المقتطفات"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "إعداد مساعد الذكاء الاصطناعي",
"description": "إعداد توليد الأوامر المدعومة بالذكاء الاصطناعي لجلسات الطرفية وإنشاء القصاصات.",
"loading": "جارٍ تحميل إعدادات الذكاء الاصطناعي...",
"enable": {
"title": "تمكين مساعد الذكاء الاصطناعي",
"description": "السماح للمستخدمين بتوليد الأوامر باستخدام الذكاء الاصطناعي"
},
"provider": {
"title": "موفر الذكاء الاصطناعي",
"description": "اختر موفر خدمة الذكاء الاصطناعي الخاص بك"
},
"model": {
"title": "النموذج",
"description": "حدد نموذج الذكاء الاصطناعي المستخدم لتوليد الأوامر"
},
"apiKey": {
"title": "مفتاح API",
"description": "مفتاح API الخاص بـ OpenAI للمصادقة",
"placeholder": "أدخل مفتاح API الخاص بـ OpenAI",
"setPlaceholder": "تم تعيين مفتاح API (اتركه فارغًا للحفاظ على الحالي)"
},
"ollamaUrl": {
"title": "رابط Ollama",
"description": "الرابط حيث يعمل مثيل Ollama الخاص بك",
"placeholder": "http://localhost:11434"
},
"selectProvider": "حدد موفرًا...",
"selectModel": "حدد نموذجًا...",
"loadingModels": "جارٍ تحميل النماذج...",
"noModels": "لا توجد نماذج متاحة",
"saveSettings": "حفظ الإعدادات",
"testConnection": "اختبار الاتصال",
"testing": "جارٍ الاختبار...",
"saveSuccess": "تم حفظ إعدادات الذكاء الاصطناعي بنجاح",
"testSuccess": "نجح اختبار اتصال الذكاء الاصطناعي",
"errors": {
"loadSettings": "فشل في تحميل إعدادات الذكاء الاصطناعي",
"saveSettings": "فشل في حفظ إعدادات الذكاء الاصطناعي",
"testConnection": "فشل في اختبار اتصال الذكاء الاصطناعي"
}
},
"monitoring": {
"loading": "تحميل إعدادات المراقبة...",
"saveSettings": "حفظ الإعدادات",
@@ -1632,20 +1583,6 @@
"error": "حدث خطأ"
}
},
"aiAssistant": {
"title": "مساعد الذكاء الاصطناعي",
"placeholder": "صف ما تريد فعله...",
"hint": "اضغط Enter للتوليد • Esc للإغلاق",
"hintUse": "اضغط على Enter لاستخدام الأمر - Esc للإغلاق",
"closeLabel": "إغلاق مساعد الذكاء الاصطناعي",
"generate": "إنشاء الأمر",
"generating": "توليد الأوامر...",
"generatedCommand": "الأمر المُنشئ",
"useCommand": "استخدام الأمر",
"tryAgain": "حاول مرة أخرى",
"copy": "نسخ إلى الحافظة",
"error": "فشل إنشاء الأمر. يرجى المحاولة مرة أخرى."
},
"snippets": {
"title": "القصاصات",
"noSnippets": "لا توجد قصاصات متاحة",
@@ -1698,4 +1635,4 @@
"ios": "iOS"
}
}
}
}
+2 -65
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Zrušit",
"create": "Vytvořit",
"save": "Uložit",
"generateAI": "Generovat pomocí AI",
"generating": "Generuji..."
"save": "Uložit"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Uživatelé",
"authentication": "Ověření",
"sources": "Zdroje",
"ai": "AI",
"monitoring": "Monitorování",
"backup": "Úložiště"
},
@@ -725,10 +722,6 @@
"title": "Rychlá akce",
"description": "Otevřít menu rychlých akcí"
},
"aimenu": {
"title": "Nabídka AI",
"description": "Otevřít menu AI příkazů"
},
"snippets": {
"title": "Snippety",
"description": "Otevřít dialog snippetů"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "Konfigurace AI asistenta",
"description": "Nakonfigurujte generování příkazů pomocí AI pro terminálové relace a tvorbu snippetů.",
"loading": "Načítání nastavení AI...",
"enable": {
"title": "Povolit AI asistenta",
"description": "Umožnit uživatelům generovat příkazy pomocí AI"
},
"provider": {
"title": "Poskytovatel AI",
"description": "Vyberte poskytovatele AI služby"
},
"model": {
"title": "Model",
"description": "Vyberte AI model pro generování příkazů"
},
"apiKey": {
"title": "API klíč",
"description": "Váš OpenAI API klíč pro ověření",
"placeholder": "Zadejte svůj OpenAI API klíč",
"setPlaceholder": "API klíč je nastaven (ponechte prázdné pro zachování)"
},
"ollamaUrl": {
"title": "Adresa URL společnosti Ollama",
"description": "URL, kde běží vaše instance Ollama",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Vyberte poskytovatele...",
"selectModel": "Vyberte model...",
"loadingModels": "Načítání modelů...",
"noModels": "Žádné modely k dispozici",
"saveSettings": "Uložit nastavení",
"testConnection": "Otestovat připojení",
"testing": "Testuji...",
"saveSuccess": "Nastavení AI úspěšně uloženo",
"testSuccess": "Test připojení AI úspěšný",
"errors": {
"loadSettings": "Nepodařilo se načíst nastavení AI",
"saveSettings": "Nepodařilo se uložit nastavení AI",
"testConnection": "Test připojení AI selhal"
}
},
"monitoring": {
"loading": "Načítání nastavení monitoringu...",
"saveSettings": "Uložit nastavení",
@@ -1632,20 +1583,6 @@
"error": "Došlo k chybě"
}
},
"aiAssistant": {
"title": "AI asistent",
"placeholder": "Popište, co chcete udělat...",
"hint": "Stiskněte Enter pro generování, Esc pro zavření",
"hintUse": "Stiskněte Enter pro použití příkazu, Esc pro zavření",
"closeLabel": "Zavřít AI asistenta",
"generate": "Generovat příkaz",
"generating": "Generuji příkaz...",
"generatedCommand": "Vygenerovaný příkaz",
"useCommand": "Použít příkaz",
"tryAgain": "Zkusit znovu",
"copy": "Kopírovat do schránky",
"error": "Nepodařilo se vygenerovat příkaz. Prosím zkuste to znovu."
},
"snippets": {
"title": "Snippety",
"noSnippets": "Žádné snippety k dispozici",
@@ -1698,4 +1635,4 @@
"ios": "iOS"
}
}
}
}
+2 -65
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Abbrechen",
"create": "erstellen",
"save": "Speichern",
"generateAI": "Mit KI generieren",
"generating": "Wird generiert..."
"save": "Speichern"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Benutzer",
"authentication": "Authentifizierung",
"sources": "Quellen",
"ai": "AI",
"monitoring": "Überwachung",
"backup": "Lagerung"
},
@@ -725,10 +722,6 @@
"title": "Schnelle Aktion",
"description": "Öffne das Schnellaktionsmenü"
},
"aimenu": {
"title": "AI-Menü",
"description": "Öffne das AI-Befehlsmenü"
},
"snippets": {
"title": "Schnipsel",
"description": "Öffne den Schnipsel-Dialog"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "KI-Assistent Konfiguration",
"description": "Konfiguriere die KI-gestützte Befehlsgenerierung für Terminalsitzungen und die Erstellung von Snippets.",
"loading": "AI-Einstellungen laden...",
"enable": {
"title": "KI-Assistent aktivieren",
"description": "Erlaube den Nutzern, Befehle mithilfe von KI zu generieren"
},
"provider": {
"title": "KI-Anbieter",
"description": "Wähle deinen KI-Anbieter"
},
"model": {
"title": "Modell",
"description": "Wähle das KI-Modell aus, das für die Befehlsgenerierung verwendet werden soll"
},
"apiKey": {
"title": "API-Schlüssel",
"description": "Dein OpenAI API-Schlüssel für die Authentifizierung",
"placeholder": "Gib deinen OpenAI API Schlüssel ein",
"setPlaceholder": "API-Schlüssel ist gesetzt (leer lassen, um aktuell zu bleiben)"
},
"ollamaUrl": {
"title": "Ollama URL",
"description": "Die URL, unter der deine Ollama-Instanz läuft",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Wähle einen Anbieter...",
"selectModel": "Wähle ein Modell...",
"loadingModels": "Modelle werden geladen...",
"noModels": "Keine Modelle verfügbar",
"saveSettings": "Einstellungen speichern",
"testConnection": "Verbindung testen",
"testing": "Test läuft...",
"saveSuccess": "AI-Einstellungen erfolgreich gespeichert",
"testSuccess": "AI-Verbindungstest erfolgreich",
"errors": {
"loadSettings": "KI-Einstellungen konnten nicht geladen werden",
"saveSettings": "KI-Einstellungen konnten nicht gespeichert werden",
"testConnection": "AI-Verbindungstest fehlgeschlagen"
}
},
"monitoring": {
"loading": "Laden der Überwachungseinstellungen...",
"saveSettings": "Einstellungen speichern",
@@ -1632,20 +1583,6 @@
"error": "Ein Fehler ist aufgetreten"
}
},
"aiAssistant": {
"title": "KI-Assistent",
"placeholder": "Beschreibe, was du tun möchtest...",
"hint": "Drücke Enter zum Erstellen - Esc zum Schließen",
"hintUse": "Drücke Enter, um den Befehl zu verwenden - Esc zum Schließen",
"closeLabel": "KI-Assistent schließen",
"generate": "Befehl generieren",
"generating": "Befehl generieren...",
"generatedCommand": "Generierter Befehl",
"useCommand": "Befehl verwenden",
"tryAgain": "Nochmal versuchen",
"copy": "In die Zwischenablage kopieren",
"error": "Der Befehl konnte nicht erstellt werden. Bitte versuche es erneut."
},
"snippets": {
"title": "Snippets",
"noSnippets": "Keine Snippets verfügbar",
@@ -1698,4 +1635,4 @@
"ios": "iOS"
}
}
}
}
+1 -64
View File
@@ -279,9 +279,7 @@
"actions": {
"cancel": "Cancel",
"create": "Create",
"save": "Save",
"generateAI": "Generate with AI",
"generating": "Generating..."
"save": "Save"
}
},
"messages": {
@@ -554,7 +552,6 @@
"users": "Users",
"authentication": "Authentication",
"sources": "Sources",
"ai": "AI",
"monitoring": "Monitoring",
"backup": "Storage"
},
@@ -730,10 +727,6 @@
"title": "Quick Action",
"description": "Open the quick action menu"
},
"aimenu": {
"title": "AI Menu",
"description": "Open the AI command menu"
},
"snippets": {
"title": "Snippets",
"description": "Open the snippets dialog"
@@ -941,48 +934,6 @@
}
}
},
"ai": {
"title": "AI Assistant Configuration",
"description": "Configure AI-powered command generation for terminal sessions and snippet creation.",
"loading": "Loading AI settings...",
"enable": {
"title": "Enable AI Assistant",
"description": "Allow users to generate commands using AI"
},
"provider": {
"title": "AI Provider",
"description": "Choose your AI service provider"
},
"model": {
"title": "Model",
"description": "Select the AI model to use for command generation"
},
"apiKey": {
"title": "API Key",
"description": "Your OpenAI API key for authentication",
"placeholder": "Enter your OpenAI API key",
"setPlaceholder": "API key is set (leave blank to keep current)"
},
"ollamaUrl": {
"title": "Ollama URL",
"description": "The URL where your Ollama instance is running",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Select a provider...",
"selectModel": "Select a model...",
"loadingModels": "Loading models...",
"noModels": "No models available",
"saveSettings": "Save Settings",
"testConnection": "Test Connection",
"testing": "Testing...",
"saveSuccess": "AI settings saved successfully",
"testSuccess": "AI connection test successful",
"errors": {
"loadSettings": "Failed to load AI settings",
"saveSettings": "Failed to save AI settings",
"testConnection": "AI connection test failed"
}
},
"monitoring": {
"loading": "Loading monitoring settings...",
"saveSettings": "Save Settings",
@@ -1648,20 +1599,6 @@
"error": "An error occurred"
}
},
"aiAssistant": {
"title": "AI Assistant",
"placeholder": "Describe what you want to do...",
"hint": "Press Enter to generate • Esc to close",
"hintUse": "Press Enter to use command • Esc to close",
"closeLabel": "Close AI Assistant",
"generate": "Generate command",
"generating": "Generating command...",
"generatedCommand": "Generated Command",
"useCommand": "Use Command",
"tryAgain": "Try Again",
"copy": "Copy to clipboard",
"error": "Failed to generate command. Please try again."
},
"snippets": {
"title": "Snippets",
"noSnippets": "No snippets available",
+2 -65
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Cancelar",
"create": "Crea",
"save": "Guarda",
"generateAI": "Generar con IA",
"generating": "Generar..."
"save": "Guarda"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Usuarios",
"authentication": "Autenticación",
"sources": "Fuentes",
"ai": "AI",
"monitoring": "Supervisión",
"backup": "Almacenamiento"
},
@@ -725,10 +722,6 @@
"title": "Acción rápida",
"description": "Abre el menú de acción rápida"
},
"aimenu": {
"title": "Menú AI",
"description": "Abre el menú de comandos AI"
},
"snippets": {
"title": "Fragmentos",
"description": "Abrir el diálogo de fragmentos"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "Configuración del Asistente AI",
"description": "Configura la generación de comandos asistida por IA para las sesiones de terminal y la creación de fragmentos.",
"loading": "Cargando ajustes de IA...",
"enable": {
"title": "Activar el Asistente de IA",
"description": "Permitir a los usuarios generar comandos utilizando IA"
},
"provider": {
"title": "Proveedor de IA",
"description": "Elige tu proveedor de servicios de IA"
},
"model": {
"title": "Modelo",
"description": "Selecciona el modelo de IA que se utilizará para la generación de órdenes"
},
"apiKey": {
"title": "Clave API",
"description": "Tu clave API de OpenAI para la autenticación",
"placeholder": "Introduce tu clave API de OpenAI",
"setPlaceholder": "La clave API está configurada (déjala en blanco para mantenerla actualizada)"
},
"ollamaUrl": {
"title": "URL de Ollama",
"description": "La URL donde se ejecuta tu instancia de Ollama",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Selecciona un proveedor...",
"selectModel": "Selecciona un modelo...",
"loadingModels": "Cargando modelos...",
"noModels": "No hay modelos disponibles",
"saveSettings": "Guardar ajustes",
"testConnection": "Conexión de prueba",
"testing": "Probando...",
"saveSuccess": "Ajustes de IA guardados correctamente",
"testSuccess": "Prueba de conexión AI realizada con éxito",
"errors": {
"loadSettings": "Error al cargar la configuración de la IA",
"saveSettings": "No se ha podido guardar la configuración de la IA",
"testConnection": "Fallo en la prueba de conexión a la IA"
}
},
"monitoring": {
"loading": "Cargando ajustes de monitorización...",
"saveSettings": "Guardar ajustes",
@@ -1632,20 +1583,6 @@
"error": "Se ha producido un error"
}
},
"aiAssistant": {
"title": "Asistente de IA",
"placeholder": "Describe lo que quieres hacer...",
"hint": "Pulsa Intro para generar - Esc para cerrar",
"hintUse": "Pulsa Intro para usar el comando - Esc para cerrar",
"closeLabel": "Cerrar Asistente AI",
"generate": "Generar comando",
"generating": "Generar comando...",
"generatedCommand": "Comando generado",
"useCommand": "Utiliza el comando",
"tryAgain": "Inténtalo de nuevo",
"copy": "Copiar al portapapeles",
"error": "No se ha podido generar el comando. Inténtalo de nuevo."
},
"snippets": {
"title": "Fragmentos",
"noSnippets": "No hay fragmentos disponibles",
@@ -1698,4 +1635,4 @@
"ios": "iOS"
}
}
}
}
+1 -64
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Annuler",
"create": "Créer",
"save": "Sauvegarde",
"generateAI": "Générer avec l'IA",
"generating": "Générer..."
"save": "Sauvegarde"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Utilisateurs",
"authentication": "Authentification",
"sources": "Sources",
"ai": "AI",
"monitoring": "Surveillance",
"backup": "Stockage"
},
@@ -725,10 +722,6 @@
"title": "Action rapide",
"description": "Ouvre le menu d'action rapide"
},
"aimenu": {
"title": "Menu IA",
"description": "Ouvre le menu des commandes AI"
},
"snippets": {
"title": "Bribes",
"description": "Ouvre la boîte de dialogue des snippets"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "Configuration de l'assistant AI",
"description": "Configurer la génération de commandes par IA pour les sessions terminales et la création de snippet.",
"loading": "Chargement des paramètres de l'IA...",
"enable": {
"title": "Activer l'assistant AI",
"description": "Permettre aux utilisateurs de générer des commandes à l'aide de l'IA"
},
"provider": {
"title": "Fournisseur d'IA",
"description": "Choisis ton fournisseur de services d'IA"
},
"model": {
"title": "Modèle",
"description": "Sélectionne le modèle d'IA à utiliser pour la génération des commandes."
},
"apiKey": {
"title": "Clé API",
"description": "Votre clé API OpenAI pour l'authentification",
"placeholder": "Entrez votre clé API OpenAI",
"setPlaceholder": "La clé API est définie (laisser vide pour rester à jour)"
},
"ollamaUrl": {
"title": "Ollama URL",
"description": "L'URL où ton instance d'Ollama est en cours d'exécution",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Sélectionnez un fournisseur...",
"selectModel": "Sélectionnez un modèle...",
"loadingModels": "Chargement des modèles...",
"noModels": "Aucun modèle disponible",
"saveSettings": "Sauvegarder les paramètres",
"testConnection": "Connexion de test",
"testing": "Test...",
"saveSuccess": "Les paramètres de l'IA ont été sauvegardés avec succès",
"testSuccess": "Test de connexion AI réussi",
"errors": {
"loadSettings": "Échec du chargement des paramètres de l'IA",
"saveSettings": "L'enregistrement des paramètres de l'IA a échoué",
"testConnection": "Le test de connexion AI a échoué"
}
},
"monitoring": {
"loading": "Chargement des paramètres de surveillance...",
"saveSettings": "Sauvegarder les réglages",
@@ -1632,20 +1583,6 @@
"error": "Une erreur s'est produite"
}
},
"aiAssistant": {
"title": "Assistant AI",
"placeholder": "Décris ce que tu veux faire...",
"hint": "Appuie sur Enter pour générer - Esc pour fermer",
"hintUse": "Appuyez sur Entrée pour utiliser la commande • Échap pour fermer",
"closeLabel": "Fermer l'assistant AI",
"generate": "Générer une commande",
"generating": "Génération de la commande...",
"generatedCommand": "Commande générée",
"useCommand": "Utiliser la commande",
"tryAgain": "Réessayer",
"copy": "Copier dans le Presse-papier",
"error": "Impossible de générer la commande. Veuillez réessayer."
},
"snippets": {
"title": "Bribes",
"noSnippets": "Pas de snippets disponibles",
+1 -64
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Annullamento",
"create": "Crea",
"save": "Risparmia",
"generateAI": "Genera con l'intelligenza artificiale",
"generating": "Generare..."
"save": "Risparmia"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Utenti",
"authentication": "Autenticazione",
"sources": "Fonti",
"ai": "AI",
"monitoring": "Monitoraggio",
"backup": "Immagazzinamento"
},
@@ -725,10 +722,6 @@
"title": "Azione rapida",
"description": "Apri il menu delle azioni rapide"
},
"aimenu": {
"title": "Menu AI",
"description": "Apri il menu dei comandi di AI"
},
"snippets": {
"title": "Frammenti",
"description": "Apri la finestra di dialogo dei frammenti"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "Configurazione dell'assistente AI",
"description": "Configura la generazione di comandi AI per le sessioni di terminale e la creazione di snippet.",
"loading": "Caricamento impostazioni AI...",
"enable": {
"title": "Attiva l'assistente AI",
"description": "Consenti agli utenti di generare comandi utilizzando l'intelligenza artificiale"
},
"provider": {
"title": "Fornitore di AI",
"description": "Scegli il tuo fornitore di servizi AI"
},
"model": {
"title": "Modello",
"description": "Seleziona il modello di intelligenza artificiale da utilizzare per la generazione dei comandi"
},
"apiKey": {
"title": "Chiave API",
"description": "La tua chiave API OpenAI per l'autenticazione",
"placeholder": "Inserisci la tua chiave API OpenAI",
"setPlaceholder": "La chiave API è impostata (lasciare vuoto per mantenerla aggiornata)"
},
"ollamaUrl": {
"title": "URL Ollama",
"description": "L'URL in cui è in esecuzione l'istanza di Ollama",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Seleziona un fornitore...",
"selectModel": "Seleziona un modello...",
"loadingModels": "Caricamento dei modelli...",
"noModels": "Nessun modello disponibile",
"saveSettings": "Salva le impostazioni",
"testConnection": "Collegamento di prova",
"testing": "Test...",
"saveSuccess": "Impostazioni AI salvate con successo",
"testSuccess": "Test di connessione AI riuscito",
"errors": {
"loadSettings": "Impossibile caricare le impostazioni dell'IA",
"saveSettings": "Impossibile salvare le impostazioni dell'AI",
"testConnection": "Test di connessione AI fallito"
}
},
"monitoring": {
"loading": "Caricamento delle impostazioni di monitoraggio...",
"saveSettings": "Salva le impostazioni",
@@ -1632,20 +1583,6 @@
"error": "Si è verificato un errore"
}
},
"aiAssistant": {
"title": "Assistente AI",
"placeholder": "Descrivi ciò che vuoi fare...",
"hint": "Premi Invio per generare - Esc per chiudere",
"hintUse": "Premi Invio per usare il comando - Esc per chiudere",
"closeLabel": "Chiudi l'assistente AI",
"generate": "Generare un comando",
"generating": "Generazione del comando...",
"generatedCommand": "Comando generato",
"useCommand": "Usa il comando",
"tryAgain": "Riprova",
"copy": "Copia negli appunti",
"error": "Il comando non è stato generato. Riprova."
},
"snippets": {
"title": "Frammenti",
"noSnippets": "Non ci sono snippet disponibili",
+1 -64
View File
@@ -276,9 +276,7 @@
"actions": {
"cancel": "Cancelar",
"create": "Criar",
"save": "Salvar",
"generateAI": "Gerar com IA",
"generating": "Gerando..."
"save": "Salvar"
}
},
"messages": {
@@ -551,7 +549,6 @@
"users": "Usuários",
"authentication": "Autenticação",
"sources": "Fontes",
"ai": "IA",
"monitoring": "Monitoramento",
"backup": "Armazenamento"
},
@@ -727,10 +724,6 @@
"title": "Ação Rápida",
"description": "Abrir o menu de ação rápida"
},
"aimenu": {
"title": "Menu de IA",
"description": "Abrir o menu de comandos de IA"
},
"snippets": {
"title": "Snippets",
"description": "Abrir a caixa de diálogo de snippets"
@@ -939,48 +932,6 @@
}
}
},
"ai": {
"title": "Configuração de Assistente de IA",
"description": "Configure a geração de comandos com IA para sessões de terminal e criação de snippets.",
"loading": "Carregando configurações de IA...",
"enable": {
"title": "Ativar o Assistente de IA",
"description": "Permitir que os usuários gerem comandos usando IA"
},
"provider": {
"title": "Provedor de IA",
"description": "Escolha seu provedor de serviços de IA"
},
"model": {
"title": "Modelo",
"description": "Selecione o modelo de IA para usar na geração de comandos"
},
"apiKey": {
"title": "Chave de API",
"description": "Sua chave de API da OpenAI para autenticação",
"placeholder": "Insira sua chave da API da OpenAI",
"setPlaceholder": "A chave API está definida (deixe em branco para manter a atual)"
},
"ollamaUrl": {
"title": "URL do Ollama",
"description": "A URL onde sua instância do Ollama está rodando",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Escolha um provedor...",
"selectModel": "Escolha um modelo...",
"loadingModels": "Carregando modelos...",
"noModels": "Nenhum modelo disponível",
"saveSettings": "Salvar Configurações",
"testConnection": "Testar Conexão",
"testing": "Testando...",
"saveSuccess": "Configurações de IA salvas com sucesso",
"testSuccess": "Teste de conexão com a IA bem-sucedido",
"errors": {
"loadSettings": "Falha ao carregar as configurações de IA",
"saveSettings": "Falha ao salvar as configurações de IA",
"testConnection": "Teste de conexão com a IA falhou"
}
},
"monitoring": {
"loading": "Carregando configurações de monitoramento...",
"saveSettings": "Salvar Configurações",
@@ -1638,20 +1589,6 @@
"error": "Ocorreu um erro"
}
},
"aiAssistant": {
"title": "Assistente de IA",
"placeholder": "Descreva o que você quer fazer...",
"hint": "Pressione Enter para gerar • Esc para fechar",
"hintUse": "Pressione Enter para usar o comando • Esc para fechar",
"closeLabel": "Fechar Assistente de IA",
"generate": "Gerar comando",
"generating": "Gerando comando...",
"generatedCommand": "Comando Gerado",
"useCommand": "Usar Comando",
"tryAgain": "Tentar Novamente",
"copy": "Copiar para a área de transferência",
"error": "Falha ao gerar o comando. Por favor, tente novamente."
},
"snippets": {
"title": "Snippets",
"noSnippets": "Nenhum snippet disponível",
+1 -64
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Cancelar",
"create": "Criar",
"save": "Salvar",
"generateAI": "Gerar com IA",
"generating": "Gerando..."
"save": "Salvar"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Usuários",
"authentication": "Autenticação",
"sources": "Fontes",
"ai": "IA",
"monitoring": "Monitoramento",
"backup": "Armazenamento"
},
@@ -725,10 +722,6 @@
"title": "Ação rápida",
"description": "Abra o menu de ação rápida"
},
"aimenu": {
"title": "Menu de IA",
"description": "Abrir o menu de comandos de IA"
},
"snippets": {
"title": "Trechos",
"description": "Abrir a caixa de diálogo de snippets"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "Configuração de Assistente de IA",
"description": "Configure a geração de comandos com IA para sessões de terminal e criação de snippets.",
"loading": "Carregando configurações de IA...",
"enable": {
"title": "Ativar o Assistente de IA",
"description": "Permitir que os usuários gerem comandos usando IA"
},
"provider": {
"title": "Provedor de IA",
"description": "Escolha seu provedor de serviços de IA"
},
"model": {
"title": "Modelo",
"description": "Selecione o modelo de IA para usar na geração de comandos"
},
"apiKey": {
"title": "Chave de API",
"description": "Sua chave de API da OpenAI para autenticação",
"placeholder": "Insira sua chave da API da OpenAI",
"setPlaceholder": "A chave API está definida (deixe em branco para manter a atual)"
},
"ollamaUrl": {
"title": "URL do Ollama",
"description": "A URL onde sua instância do Ollama está rodando",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Escolha um provedor...",
"selectModel": "Escolha um modelo...",
"loadingModels": "Carregando modelos...",
"noModels": "Nenhum modelo disponível",
"saveSettings": "Salvar Configurações",
"testConnection": "Testar Conexão",
"testing": "Testando...",
"saveSuccess": "Configurações de IA salvas com sucesso",
"testSuccess": "Teste de conexão com a IA bem-sucedido",
"errors": {
"loadSettings": "Falha ao carregar as configurações de IA",
"saveSettings": "Falha ao salvar as configurações de IA",
"testConnection": "Teste de conexão com a IA falhou"
}
},
"monitoring": {
"loading": "Carregando as configurações de monitoramento...",
"saveSettings": "Salvar configurações",
@@ -1632,20 +1583,6 @@
"error": "Ocorreu um erro"
}
},
"aiAssistant": {
"title": "Assistente de IA",
"placeholder": "Descreva o que você quer fazer...",
"hint": "Pressione Enter para gerar • Esc para fechar",
"hintUse": "Pressione Enter para usar o comando - Esc para fechar",
"closeLabel": "Fechar Assistente de IA",
"generate": "Gerar comando",
"generating": "Geração de comando...",
"generatedCommand": "Comando gerado",
"useCommand": "Usar o comando",
"tryAgain": "Tente novamente",
"copy": "Copiar para a área de transferência",
"error": "Falha ao gerar o comando. Tente novamente."
},
"snippets": {
"title": "Trechos",
"noSnippets": "Nenhum snippet disponível",
+1 -64
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "Отмена",
"create": "Создать",
"save": "Сохранить",
"generateAI": "Сгенерировать с помощью ИИ",
"generating": "Генерация..."
"save": "Сохранить"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "Пользователи",
"authentication": "Аутентификация",
"sources": "Источники",
"ai": "ИИ",
"monitoring": "Мониторинг",
"backup": "Хранение"
},
@@ -725,10 +722,6 @@
"title": "Быстрое действие",
"description": "Открой меню быстрых действий"
},
"aimenu": {
"title": "Меню ИИ",
"description": "Открыть меню команд ИИ"
},
"snippets": {
"title": "Сниппеты",
"description": "Открыть диалог сниппетов"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "Настройка ИИ-ассистента",
"description": "Настройте генерацию команд с помощью ИИ для терминала и создания сниппетов.",
"loading": "Загрузка настроек ИИ...",
"enable": {
"title": "Включить ИИ-ассистента",
"description": "Разрешить пользователям генерировать команды с помощью ИИ"
},
"provider": {
"title": "Провайдер ИИ",
"description": "Выберите поставщика ИИ-сервиса"
},
"model": {
"title": "Модель",
"description": "Выберите модель ИИ для генерации команд"
},
"apiKey": {
"title": "Ключ API",
"description": "Ключ API OpenAI для аутентификации",
"placeholder": "Введите ваш ключ API OpenAI",
"setPlaceholder": "Ключ API задан (оставьте пустым, чтобы сохранить текущий)"
},
"ollamaUrl": {
"title": "URL Ollama",
"description": "Адрес, по которому запущен ваш экземпляр Ollama",
"placeholder": "http://localhost:11434"
},
"selectProvider": "Выберите провайдера...",
"selectModel": "Выберите модель...",
"loadingModels": "Загрузка моделей...",
"noModels": "Нет доступных моделей",
"saveSettings": "Сохранить настройки",
"testConnection": "Проверить подключение",
"testing": "Проверка...",
"saveSuccess": "Настройки ИИ успешно сохранены",
"testSuccess": "Проверка подключения к ИИ прошла успешно",
"errors": {
"loadSettings": "Не удалось загрузить настройки ИИ",
"saveSettings": "Не удалось сохранить настройки ИИ",
"testConnection": "Проверка подключения к ИИ не удалась"
}
},
"monitoring": {
"loading": "Загрузка настроек мониторинга...",
"saveSettings": "Сохранить настройки",
@@ -1632,20 +1583,6 @@
"error": "Произошла ошибка"
}
},
"aiAssistant": {
"title": "ИИ-ассистент",
"placeholder": "Опишите, что вы хотите сделать...",
"hint": "Нажмите Enter для генерации • Esc для закрытия",
"hintUse": "Нажмите Enter, чтобы использовать команду - Esc, чтобы закрыть",
"closeLabel": "Закрыть ИИ-ассистент",
"generate": "Создай команду",
"generating": "Генерирую команду...",
"generatedCommand": "Сгенерированная команда",
"useCommand": "Используйте команду",
"tryAgain": "Попробуй еще раз",
"copy": "Копируй в буфер обмена",
"error": "Не удалось сгенерировать команду. Пожалуйста, попробуй еще раз."
},
"snippets": {
"title": "Сниппеты",
"noSnippets": "Нет доступных сниппетов",
+1 -64
View File
@@ -274,9 +274,7 @@
"actions": {
"cancel": "取消",
"create": "创建",
"save": "保存",
"generateAI": "用 AI 生成",
"generating": "生成中..."
"save": "保存"
}
},
"messages": {
@@ -549,7 +547,6 @@
"users": "用户",
"authentication": "身份验证",
"sources": "来源",
"ai": "AI",
"monitoring": "监控",
"backup": "存储"
},
@@ -725,10 +722,6 @@
"title": "快速行动",
"description": "打开快速操作菜单"
},
"aimenu": {
"title": "AI 菜单",
"description": "打开 AI 命令菜单"
},
"snippets": {
"title": "代码片段",
"description": "打开代码片段对话框"
@@ -935,48 +928,6 @@
}
}
},
"ai": {
"title": "AI 助手配置",
"description": "配置 AI 驱动的命令生成功能,用于终端会话和代码片段。",
"loading": "加载 AI 设置中...",
"enable": {
"title": "启用 AI 助手",
"description": "允许用户使用 AI 生成命令"
},
"provider": {
"title": "AI 提供商",
"description": "选择 AI 服务提供商"
},
"model": {
"title": "AI 模型",
"description": "选择用于生成命令的 AI 模型"
},
"apiKey": {
"title": "API 密钥",
"description": "您的 OpenAI API 密钥(用于身份验证)",
"placeholder": "输入您的 OpenAI API 密钥",
"setPlaceholder": "API 密钥已设置(留空表示保留当前值)"
},
"ollamaUrl": {
"title": "Ollama 地址",
"description": "Ollama 实例的访问地址",
"placeholder": "http://localhost:11434"
},
"selectProvider": "选择服务提供商...",
"selectModel": "选择模型...",
"loadingModels": "加载模型中...",
"noModels": "无可用模型",
"saveSettings": "保存设置",
"testConnection": "测试连接",
"testing": "测试中...",
"saveSuccess": "AI 设置保存成功",
"testSuccess": "AI 连接测试成功",
"errors": {
"loadSettings": "加载 AI 设置失败",
"saveSettings": "保存 AI 设置失败",
"testConnection": "AI 连接测试失败"
}
},
"monitoring": {
"loading": "加载监控数据中...",
"saveSettings": "保存设置",
@@ -1632,20 +1583,6 @@
"error": "发生错误"
}
},
"aiAssistant": {
"title": "AI 助手",
"placeholder": "描述您想要执行的操作...",
"hint": "按 Enter 生成 • 按 Esc 关闭",
"hintUse": "按 Enter 使用命令 - 按 Esc 关闭",
"closeLabel": "关闭 AI 助手",
"generate": "生成命令",
"generating": "生成命令...",
"generatedCommand": "生成命令",
"useCommand": "使用命令",
"tryAgain": "再试一次",
"copy": "复制到剪贴板",
"error": "生成命令失败。请重试。"
},
"snippets": {
"title": "代码片段",
"noSnippets": "无可用代码片段",
-49
View File
@@ -1,49 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
import { getRequest } from "@/common/utils/RequestUtil.js";
export const AIContext = createContext({});
export const useAI = () => {
const context = useContext(AIContext);
if (!context) throw new Error("useAI must be used within an AIProvider");
return context;
};
export const AIProvider = ({ children }) => {
const [aiSettings, setAISettings] = useState({ enabled: false, provider: null, model: null, configured: false });
const [loading, setLoading] = useState(true);
const loadAISettings = async () => {
try {
setLoading(true);
const settings = await getRequest("ai");
const configured = settings.enabled && settings.provider && settings.model
&& (settings.provider !== "openai" || settings.hasApiKey);
setAISettings({
enabled: settings.enabled,
provider: settings.provider,
model: settings.model,
configured,
});
} catch (error) {
setAISettings({ enabled: false, provider: null, model: null, configured: false });
} finally {
setLoading(false);
}
};
const isAIAvailable = () => aiSettings.enabled && aiSettings.configured;
useEffect(() => {
loadAISettings();
}, []);
return (
<AIContext.Provider value={{ aiSettings, loading, isAIAvailable, loadAISettings }}>
{children}
</AIContext.Provider>
);
};
+13 -16
View File
@@ -5,7 +5,6 @@ import { ServerProvider } from "@/common/contexts/ServerContext.jsx";
import { IdentityProvider } from "@/common/contexts/IdentityContext.jsx";
import { ToastProvider } from "@/common/contexts/ToastContext.jsx";
import { PreferencesProvider } from "@/common/contexts/PreferencesContext.jsx";
import { AIProvider } from "@/common/contexts/AIContext.jsx";
import { KeymapProvider } from "@/common/contexts/KeymapContext.jsx";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
@@ -35,21 +34,19 @@ export default () => {
<PreferencesWrapper>
<StateStreamProvider>
<KeymapProvider>
<AIProvider>
<ServerProvider>
<IdentityProvider>
<SnippetProvider>
<ScriptProvider>
<SessionProvider>
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</SessionProvider>
</ScriptProvider>
</SnippetProvider>
</IdentityProvider>
</ServerProvider>
</AIProvider>
<ServerProvider>
<IdentityProvider>
<SnippetProvider>
<ScriptProvider>
<SessionProvider>
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</SessionProvider>
</ScriptProvider>
</SnippetProvider>
</IdentityProvider>
</ServerProvider>
</KeymapProvider>
</StateStreamProvider>
</PreferencesWrapper>
+35 -38
View File
@@ -5,7 +5,6 @@ import { ServerProvider } from "@/common/contexts/ServerContext.jsx";
import { IdentityProvider } from "@/common/contexts/IdentityContext.jsx";
import { ToastProvider } from "@/common/contexts/ToastContext.jsx";
import { PreferencesProvider } from "@/common/contexts/PreferencesContext.jsx";
import { AIProvider } from "@/common/contexts/AIContext.jsx";
import { KeymapProvider } from "@/common/contexts/KeymapContext.jsx";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
@@ -85,45 +84,43 @@ const AppContent = () => {
<PreferencesWrapper>
<StateStreamProvider>
<KeymapProvider>
<AIProvider>
<ServerProvider>
<IdentityProvider>
<SnippetProvider>
<ScriptProvider>
<SessionProvider>
<QuickActionProvider>
<div className="app-wrapper">
<TitleBar />
<ConnectionErrorBanner />
<div className="content-wrapper">
<div
className={`left-pane${isLeftPaneCollapsed ? " collapsed" : ""}${isLeftPaneVisible ? " open" : ""}`}
ref={leftPaneRef}
>
<Suspense fallback={<Loading />}>
<Sidebar onToggleCollapse={() => setIsLeftPaneCollapsed(prev => !prev)} />
</Suspense>
<div className="left-pane-slot" id="left-pane-slot" />
</div>
<div
className={`left-pane-hover-bar${isLeftPaneCollapsed ? " active" : ""}`}
ref={hoverBarRef}
/>
<div className="main-content">
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</div>
<ServerProvider>
<IdentityProvider>
<SnippetProvider>
<ScriptProvider>
<SessionProvider>
<QuickActionProvider>
<div className="app-wrapper">
<TitleBar />
<ConnectionErrorBanner />
<div className="content-wrapper">
<div
className={`left-pane${isLeftPaneCollapsed ? " collapsed" : ""}${isLeftPaneVisible ? " open" : ""}`}
ref={leftPaneRef}
>
<Suspense fallback={<Loading />}>
<Sidebar onToggleCollapse={() => setIsLeftPaneCollapsed(prev => !prev)} />
</Suspense>
<div className="left-pane-slot" id="left-pane-slot" />
</div>
<div
className={`left-pane-hover-bar${isLeftPaneCollapsed ? " active" : ""}`}
ref={hoverBarRef}
/>
<div className="main-content">
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</div>
<MobileNav />
</div>
</QuickActionProvider>
</SessionProvider>
</ScriptProvider>
</SnippetProvider>
</IdentityProvider>
</ServerProvider>
</AIProvider>
<MobileNav />
</div>
</QuickActionProvider>
</SessionProvider>
</ScriptProvider>
</SnippetProvider>
</IdentityProvider>
</ServerProvider>
</KeymapProvider>
</StateStreamProvider>
</PreferencesWrapper>
+6
View File
@@ -1,5 +1,6 @@
let _isTauri = null;
let _tauriPromise = null;
const EXTERNAL_LINKS_ENABLED = String(import.meta.env.VITE_ENABLE_EXTERNAL_LINKS || "false").toLowerCase() === "true";
const checkTauri = () => typeof window !== "undefined" && !!(window.__TAURI_INTERNALS__ || window.__TAURI__);
@@ -46,6 +47,11 @@ export const setActiveServerUrl = (url) => {
};
export const openExternalUrl = async (url) => {
if (!EXTERNAL_LINKS_ENABLED) {
console.warn("External URL opening is disabled by VITE_ENABLE_EXTERNAL_LINKS.");
return;
}
if (!isTauri()) {
window.open(url, "_blank");
return;
+1 -3
View File
@@ -1,4 +1,4 @@
import { mdiServerOutline, mdiCodeBraces, mdiChartBoxOutline, mdiShieldCheckOutline, mdiAccountCircleOutline, mdiAccountGroup, mdiClockStarFourPointsOutline, mdiShieldAccountOutline, mdiDomain, mdiCreationOutline, mdiKeyVariant, mdiConsole, mdiKeyboardOutline, mdiCloudDownloadOutline, mdiChartLine, mdiHarddisk, mdiFolderOutline } from "@mdi/js";
import { mdiServerOutline, mdiCodeBraces, mdiChartBoxOutline, mdiShieldCheckOutline, mdiAccountCircleOutline, mdiAccountGroup, mdiClockStarFourPointsOutline, mdiShieldAccountOutline, mdiDomain, mdiKeyVariant, mdiConsole, mdiKeyboardOutline, mdiCloudDownloadOutline, mdiChartLine, mdiHarddisk, mdiFolderOutline } from "@mdi/js";
import Account from "@/pages/Settings/pages/Account";
import Terminal from "@/pages/Settings/pages/Terminal";
import FileManager from "@/pages/Settings/pages/FileManager";
@@ -11,7 +11,6 @@ import Authentication from "@/pages/Settings/pages/Authentication";
import Sources from "@/pages/Settings/pages/Sources";
import Monitoring from "@/pages/Settings/pages/Monitoring";
import Backup from "@/pages/Settings/pages/Backup";
import AI from "@/pages/Settings/pages/AI";
export const getSidebarNavigation = t => [
{ title: t('common.sidebar.servers'), key: "servers", path: "/servers", icon: mdiServerOutline, toggleEvent: "toggleServerList" },
@@ -36,7 +35,6 @@ export const getSettingsAdminPages = t => [
{ title: t("settings.pages.sources"), key: "sources", icon: mdiCloudDownloadOutline, content: <Sources /> },
{ title: t("settings.pages.monitoring"), key: "monitoring", icon: mdiChartLine, content: <Monitoring /> },
{ title: t("settings.pages.backup"), key: "backup", icon: mdiHarddisk, content: <Backup /> },
{ title: t("settings.pages.ai"), key: "ai", icon: mdiCreationOutline, content: <AI /> },
];
export const getAllSettingsPages = t => [...getSettingsUserPages(t), ...getSettingsAdminPages(t)];
@@ -1,13 +1,11 @@
import { useEffect, useRef, useState, useContext } from "react";
import { UserContext } from "@/common/contexts/UserContext.jsx";
import { IdentityContext } from "@/common/contexts/IdentityContext.jsx";
import { AIContext } from "@/common/contexts/AIContext.jsx";
import { useKeymaps, matchesKeybind } from "@/common/contexts/KeymapContext.jsx";
import { Terminal as Xterm } from "@xterm/xterm";
import { usePreferences } from "@/common/contexts/PreferencesContext.jsx";
import { FitAddon } from "@xterm/addon-fit";
import { ContextMenu, ContextMenuItem, ContextMenuSeparator, useContextMenu } from "@/common/components/ContextMenu";
import AICommandPopover from "./components/AICommandPopover";
import SnippetsMenu from "./components/SnippetsMenu";
import { createProgressParser } from "../utils/progressParser";
import { mdiContentCopy, mdiContentPaste, mdiCodeBrackets, mdiSelectAll, mdiDelete, mdiKeyboard, mdiKey } from "@mdi/js";
@@ -33,11 +31,8 @@ const XtermRenderer = ({ session, disconnectFromServer, registerTerminalRef, bro
const userContext = useContext(UserContext);
const sessionToken = userContext?.sessionToken;
const { theme, getCurrentTheme, selectedFont, fontSize, cursorStyle, cursorBlink, selectedTheme } = usePreferences();
const aiContext = useContext(AIContext);
const isAIAvailable = aiContext?.isAIAvailable || (() => false);
const { getParsedKeybind } = useKeymaps();
const { t } = useTranslation();
const [showAIPopover, setShowAIPopover] = useState(false);
const contextMenu = useContextMenu();
const { identities } = useContext(IdentityContext);
const [showSnippetsMenu, setShowSnippetsMenu] = useState(false);
@@ -71,19 +66,6 @@ const XtermRenderer = ({ session, disconnectFromServer, registerTerminalRef, bro
}
}, [session.id, updateProgress]);
const toggleAIPopover = () => {
if (showAIPopover) {
setTimeout(() => termRef.current?.focus(), 0);
}
setShowAIPopover(!showAIPopover);
};
const handleAICommandGenerated = (command) => {
if (termRef.current && wsRef.current) {
wsRef.current.send(command);
}
};
const handleContextMenu = (e) => {
e.preventDefault();
e.stopPropagation();
@@ -328,14 +310,6 @@ const XtermRenderer = ({ session, disconnectFromServer, registerTerminalRef, bro
}
}
const aiKeybind = getParsedKeybind("ai-menu");
if (aiKeybind && isAIAvailable() && matchesKeybind(event, aiKeybind)) {
event.preventDefault();
event.stopPropagation();
toggleAIPopover();
return false;
}
const snippetsKeybind = getParsedKeybind("snippets");
if (snippetsKeybind && matchesKeybind(event, snippetsKeybind)) {
event.preventDefault();
@@ -401,17 +375,6 @@ const XtermRenderer = ({ session, disconnectFromServer, registerTerminalRef, bro
<div className="xterm-container" onContextMenu={!isShared ? handleContextMenu : undefined}>
<ConnectionLoader onReady={(loader) => { connectionLoaderRef.current = loader; }} />
<div ref={ref} className="xterm-wrapper" />
{!isShared && isAIAvailable() && (
<AICommandPopover visible={showAIPopover} onClose={() => setShowAIPopover(false)}
onCommandGenerated={handleAICommandGenerated} focusTerminal={() => termRef.current?.focus()}
entryId={session.server?.id}
recentOutput={terminalBufferRef.current.join('')
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
.replace(/[\x00-\x1F\x7F]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(-1500)} />
)}
{!isShared && (
<ContextMenu
isOpen={contextMenu.isOpen}
@@ -1,85 +0,0 @@
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { mdiRobot, mdiSend, mdiContentCopy, mdiCheck } from "@mdi/js";
import Icon from "@mdi/react";
import { DialogProvider } from "@/common/components/Dialog";
import { postRequest } from "@/common/utils/RequestUtil.js";
import { useToast } from "@/common/contexts/ToastContext.jsx";
import "./styles.sass";
export const AICommandPopover = ({ visible, onClose, onCommandGenerated, focusTerminal, entryId, recentOutput }) => {
const { t } = useTranslation();
const { sendToast } = useToast();
const [prompt, setPrompt] = useState("");
const [loading, setLoading] = useState(false);
const [command, setCommand] = useState("");
const [copied, setCopied] = useState(false);
const inputRef = useRef(null);
const cmdRef = useRef(null);
useEffect(() => {
if (visible) {
setPrompt(""); setCommand(""); setCopied(false);
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [visible]);
const handleClose = () => { onClose(); focusTerminal?.(); };
const handleSubmit = async (e) => {
e.preventDefault();
if (!prompt.trim() || loading) return;
setLoading(true); setCommand("");
try {
const payload = { prompt: prompt.trim() };
if (entryId) payload.entryId = entryId;
if (recentOutput) payload.recentOutput = recentOutput;
const res = await postRequest("ai/generate", payload);
setCommand(res.command);
setTimeout(() => cmdRef.current?.focus(), 50);
} catch { sendToast("Error", t('servers.aiAssistant.error')); }
finally { setLoading(false); }
};
const handleUse = () => { if (command.trim()) { onCommandGenerated(command); handleClose(); } };
const handleCopy = () => {
navigator.clipboard.writeText(command).then(() => {
setCopied(true); setTimeout(() => setCopied(false), 1500);
});
};
return (
<DialogProvider open={visible} onClose={handleClose}>
<div className="ai-dialog">
<div className="ai-header"><Icon path={mdiRobot} /><h3>{t('servers.aiAssistant.title')}</h3></div>
<form onSubmit={handleSubmit} className="ai-form">
<input ref={inputRef} type="text" value={prompt} onChange={(e) => setPrompt(e.target.value)}
placeholder={t('servers.aiAssistant.placeholder')} disabled={loading || command} />
<button type="submit" disabled={!prompt.trim() || loading || command}>
{loading ? <span className="ai-spinner" /> : <Icon path={mdiSend} />}
</button>
</form>
{command && !loading && (
<div className="ai-result">
<div className="ai-result-header">
<span className="ai-label">{t('servers.aiAssistant.generatedCommand')}</span>
<button className="ai-copy" onClick={handleCopy} title={t('servers.aiAssistant.copy')}>
<Icon path={copied ? mdiCheck : mdiContentCopy} />
</button>
</div>
<textarea ref={cmdRef} value={command} onChange={(e) => setCommand(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); handleUse(); } }} rows={3} />
<div className="ai-actions">
<button className="secondary" onClick={() => { setCommand(""); inputRef.current?.focus(); }}>
{t('servers.aiAssistant.tryAgain')}
</button>
<button className="primary" onClick={handleUse}>{t('servers.aiAssistant.useCommand')}</button>
</div>
</div>
)}
<div className="ai-hint">{command ? t('servers.aiAssistant.hintUse') : t('servers.aiAssistant.hint')}</div>
</div>
</DialogProvider>
);
};
@@ -1 +0,0 @@
export { AICommandPopover as default } from "./AICommandPopover";
@@ -1,141 +0,0 @@
@use "@/common/styles/colors"
.ai-dialog
width: 380px
max-width: 90vw
.ai-header
display: flex
align-items: center
gap: 0.5rem
margin-bottom: 0.75rem
min-height: 2rem
svg
width: 1.25rem
height: 1.25rem
color: colors.$primary
h3
margin: 0
font-size: 0.95rem
font-weight: 600
.ai-form
display: flex
gap: 0.5rem
margin-bottom: 0.75rem
input
flex: 1
padding: 0.625rem 0.75rem
background: colors.$darker-gray
border: 1px solid colors.$gray
border-radius: 0.5rem
color: colors.$white
font-size: 0.9rem
outline: none
&:focus
border-color: colors.$primary
&::placeholder
color: colors.$subtext
&:disabled
opacity: 0.6
button
padding: 0.625rem
background: colors.$primary
border: none
border-radius: 0.5rem
color: colors.$white
cursor: pointer
display: flex
align-items: center
justify-content: center
svg
width: 1rem
height: 1rem
&:hover:not(:disabled)
filter: brightness(1.1)
&:disabled
opacity: 0.5
cursor: not-allowed
.ai-spinner
width: 1rem
height: 1rem
border: 2px solid colors.$white
border-top-color: transparent
border-radius: 50%
animation: spin 0.8s linear infinite
display: inline-block
@keyframes spin
to
transform: rotate(360deg)
.ai-result
margin-bottom: 0.5rem
.ai-result-header
display: flex
align-items: center
justify-content: space-between
margin-bottom: 0.375rem
.ai-label
font-size: 0.7rem
color: colors.$subtext
text-transform: uppercase
.ai-copy
background: none
border: none
padding: 0.25rem
cursor: pointer
color: colors.$subtext
border-radius: 0.25rem
display: flex
&:hover
color: colors.$white
background: colors.$gray
svg
width: 0.9rem
height: 0.9rem
textarea
width: 100%
box-sizing: border-box
padding: 0.625rem
background: colors.$darker-gray
border: 1px solid colors.$gray
border-radius: 0.5rem
color: colors.$white
font-family: monospace
font-size: 0.85rem
resize: vertical
min-height: 60px
max-height: 150px
outline: none
&:focus
border-color: colors.$primary
.ai-actions
display: flex
gap: 0.5rem
justify-content: flex-end
margin-top: 0.5rem
button
padding: 0.4rem 0.75rem
border-radius: 0.5rem
font-size: 0.85rem
cursor: pointer
&.primary
background: colors.$primary
border: none
color: colors.$white
&:hover
filter: brightness(1.1)
&.secondary
background: colors.$lighter-background
border: 1px solid colors.$gray
color: colors.$white
&:hover
background: colors.$gray
.ai-hint
font-size: 0.75rem
color: colors.$subtext
text-align: center
-280
View File
@@ -1,280 +0,0 @@
import "./styles.sass";
import { useEffect, useState, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { getRequest, patchRequest, postRequest } from "@/common/utils/RequestUtil.js";
import Button from "@/common/components/Button";
import ToggleSwitch from "@/common/components/ToggleSwitch";
import IconInput from "@/common/components/IconInput";
import SelectBox from "@/common/components/SelectBox";
import { useToast } from "@/common/contexts/ToastContext.jsx";
import { useAI } from "@/common/contexts/AIContext.jsx";
import { mdiRobot, mdiTestTube, mdiEye, mdiEyeOff } from "@mdi/js";
export const AI = () => {
const { t } = useTranslation();
const [settings, setSettings] = useState({
enabled: false,
provider: "",
model: "",
apiKey: "",
apiUrl: "http://localhost:11434",
hasApiKey: false,
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [loadingModels, setLoadingModels] = useState(false);
const [availableModels, setAvailableModels] = useState([]);
const [showApiKey, setShowApiKey] = useState(false);
const { sendToast } = useToast();
const { loadAISettings } = useAI();
const providerOptions = [
{ value: "", label: t("settings.ai.selectProvider") },
{ value: "ollama", label: "Ollama" },
{ value: "openai", label: "OpenAI" },
{ value: "openai_compatible", label: "OpenAI Compatible" },
];
const modelOptions = useMemo(() => {
if (!settings.provider) return [{ value: "", label: t("settings.ai.selectModel") }];
if (loadingModels) return [{ value: "", label: t("settings.ai.loadingModels") }];
if (availableModels.length > 0) {
return [{ value: "", label: t("settings.ai.selectModel") }, ...availableModels.map(model => ({
value: model,
label: model,
}))];
} else {
return [{ value: "", label: t("settings.ai.noModels") }];
}
}, [settings.provider, loadingModels, availableModels, t]);
const loadModels = useCallback(async () => {
if (!settings.provider) return;
try {
setLoadingModels(true);
const response = await getRequest("ai/models");
setAvailableModels(response.models || []);
} catch (error) {
setAvailableModels([]);
} finally {
setLoadingModels(false);
}
}, [settings.provider]);
const loadSettings = async () => {
try {
setLoading(true);
const response = await getRequest("ai");
setSettings(prev => ({ ...prev, ...response }));
} catch (error) {
sendToast(t("common.error"), t("settings.ai.errors.loadSettings"));
} finally {
setLoading(false);
}
};
const saveSettings = async () => {
try {
setSaving(true);
const updateData = {
enabled: settings.enabled,
provider: settings.provider || null,
model: settings.model || null,
apiUrl: settings.apiUrl || null,
};
if (updateData.provider === "") updateData.provider = null;
if (updateData.model === "") updateData.model = null;
if (updateData.apiUrl === "") updateData.apiUrl = null;
const apiKeyChanged = settings.apiKey && settings.apiKey !== "";
if (apiKeyChanged) updateData.apiKey = settings.apiKey;
const response = await patchRequest("ai", updateData);
if (settings.provider) loadModels();
setSettings(prev => ({ ...prev, ...response }));
sendToast(t("common.success"), t("settings.ai.saveSuccess"));
loadAISettings();
} catch (error) {
sendToast(t("common.error"), t("settings.ai.errors.saveSettings"));
} finally {
setSaving(false);
}
};
const testConnection = async () => {
try {
setTesting(true);
await postRequest("ai/test");
sendToast(t("common.success"), t("settings.ai.testSuccess"));
} catch (error) {
sendToast(t("common.error"), error.message || t("settings.ai.errors.testConnection"));
} finally {
setTesting(false);
}
};
const handleInputChange = useCallback((field, value) => {
setSettings(prev => ({ ...prev, [field]: value }));
}, []);
const handleProviderChange = useCallback((provider) => {
setSettings(prev => ({
...prev,
provider: provider,
model: "",
apiUrl: provider === "ollama" ? "http://localhost:11434" :
provider === "openai_compatible" ? "" : prev.apiUrl,
}));
}, []);
const isConfigurationValid = () => {
if (!settings.enabled) return false;
if (!settings.provider) return false;
if (!settings.model) return false;
if (settings.provider === "openai" && !settings.apiKey && !settings.hasApiKey) return false;
if (settings.provider === "openai_compatible") {
if (!settings.apiUrl) return false;
if (!settings.apiKey && !settings.hasApiKey) return false;
}
return true;
};
useEffect(() => {
loadSettings();
}, []);
useEffect(() => {
if (settings.provider) {
loadModels();
} else {
setAvailableModels([]);
}
}, [settings.provider, loadModels]);
if (loading) return <div className="ai-settings-loading">{t("settings.ai.loading")}</div>;
return (
<div className="ai-settings">
<div className="settings-section">
<h2>{t("settings.ai.title")}</h2>
<p>{t("settings.ai.description")}</p>
<div className="setting-item">
<div className="setting-label">
<h4>{t("settings.ai.enable.title")}</h4>
<p>{t("settings.ai.enable.description")}</p>
</div>
<ToggleSwitch onChange={(enabled) => handleInputChange("enabled", enabled)} id="ai-enabled"
checked={settings.enabled} />
</div>
{settings.enabled && (
<>
<div className="setting-item">
<div className="setting-label">
<h4>{t("settings.ai.provider.title")}</h4>
<p>{t("settings.ai.provider.description")}</p>
</div>
<div className="setting-input">
<SelectBox options={providerOptions} selected={settings.provider}
setSelected={handleProviderChange} />
</div>
</div>
{settings.provider && (
<>
<div className="setting-item">
<div className="setting-label">
<h4>{t("settings.ai.model.title")}</h4>
<p>{t("settings.ai.model.description")}</p>
</div>
<div className="setting-input">
<SelectBox setSelected={(model) => handleInputChange("model", model)}
disabled={loadingModels} options={modelOptions}
selected={settings.model}
searchable={availableModels.length > 5} />
</div>
</div>
{settings.provider === "openai" && (
<div className="setting-item">
<div className="setting-label">
<h4>{t("settings.ai.apiKey.title")}</h4>
<p>{t("settings.ai.apiKey.description")}</p>
</div>
<div className="setting-input api-key-input">
<IconInput
icon={showApiKey ? mdiEyeOff : mdiEye}
type={showApiKey ? "text" : "password"}
value={settings.apiKey}
setValue={(value) => handleInputChange("apiKey", value)}
placeholder={settings.hasApiKey ? t("settings.ai.apiKey.setPlaceholder") : t("settings.ai.apiKey.placeholder")}
onIconClick={() => setShowApiKey(!showApiKey)}
/>
</div>
</div>
)}
{settings.provider === "openai_compatible" && (
<div className="setting-item">
<div className="setting-label">
<h4>{t("settings.ai.apiKey.title")}</h4>
<p>{t("settings.ai.apiKey.description")}</p>
</div>
<div className="setting-input">
<IconInput icon={mdiRobot} value={settings.apiUrl}
setValue={(value) => handleInputChange("apiUrl", value)}
placeholder={`${t("settings.ai.ollamaUrl.placeholder")}/compatible-mode/v1`} />
</div>
<div className="setting-input api-key-input">
<IconInput
icon={showApiKey ? mdiEyeOff : mdiEye}
type={showApiKey ? "text" : "password"}
value={settings.apiKey}
setValue={(value) => handleInputChange("apiKey", value)}
placeholder={settings.hasApiKey ? t("settings.ai.apiKey.setPlaceholder") : t("settings.ai.apiKey.placeholder")}
onIconClick={() => setShowApiKey(!showApiKey)}
/>
</div>
</div>
)}
{settings.provider === "ollama" && (
<div className="setting-item">
<div className="setting-label">
<h4>{t("settings.ai.ollamaUrl.title")}</h4>
<p>{t("settings.ai.ollamaUrl.description")}</p>
</div>
<div className="setting-input">
<IconInput icon={mdiRobot} value={settings.apiUrl}
setValue={(value) => handleInputChange("apiUrl", value)}
placeholder={t("settings.ai.ollamaUrl.placeholder")} />
</div>
</div>
)}
</>
)}
</>
)}
</div>
<div className="settings-actions">
<Button text={t("settings.ai.saveSettings")} icon={mdiRobot} onClick={saveSettings} disabled={saving} type="primary" />
{isConfigurationValid() && (
<Button text={testing ? t("settings.ai.testing") : t("settings.ai.testConnection")} icon={mdiTestTube}
onClick={testConnection} disabled={testing} type="secondary" />
)}
</div>
</div>
);
};
@@ -1 +0,0 @@
export { AI as default } from "./AI.jsx";
@@ -1,173 +0,0 @@
@use "@/common/styles/colors"
$mobile: 768px
$tablet: 1024px
.ai-settings
display: flex
flex-direction: column
margin-top: 1rem
gap: 2rem
@media (max-width: $mobile)
gap: 1.5rem
.ai-settings-loading
padding: 2rem
text-align: center
color: colors.$subtext
.settings-section
h2
margin: 0 0 1rem 0
color: colors.$white
font-size: 1.5rem
@media (max-width: $mobile)
font-size: 1.25rem
p
margin: 0 0 1.5rem 0
color: colors.$subtext
font-size: 0.9rem
@media (max-width: $mobile)
margin-bottom: 1rem
font-size: 0.85rem
.setting-item
display: flex
align-items: flex-start
justify-content: space-between
padding: 1rem 0
border-bottom: 1px solid colors.$gray
gap: 1rem
@media (max-width: $mobile)
flex-direction: column
gap: 0.75rem
&:last-child
border-bottom: none
.setting-label
flex: 1
margin-right: 2rem
@media (max-width: $mobile)
margin-right: 0
h4
margin: 0 0 0.25rem 0
color: colors.$white
font-size: 1rem
font-weight: 600
@media (max-width: $mobile)
font-size: 0.9rem
p
margin: 0
color: colors.$subtext
font-size: 0.85rem
line-height: 1.4
@media (max-width: $mobile)
font-size: 0.8rem
.setting-input
min-width: 250px
@media (max-width: $mobile)
min-width: 100%
width: 100%
&.api-key-input
min-width: 300px
@media (max-width: $mobile)
min-width: 100%
&.number-input
min-width: 120px
@media (max-width: $mobile)
min-width: 100%
input
width: 100%
padding: 0.8rem
background: colors.$lighter-background
border: 1px solid colors.$gray
border-radius: 0.7rem
color: colors.$white
font-size: 14pt
outline: none
@media (max-width: $mobile)
padding: 0.625rem
font-size: 12pt
&:focus
border-color: colors.$primary
background-color: colors.$dark-gray
.advanced-settings
margin-top: 2rem
padding: 1.5rem
background: colors.$dark-gray
border-radius: 0.7rem
border: 1px solid colors.$gray
@media (max-width: $mobile)
margin-top: 1.5rem
padding: 1rem
h4
margin: 0 0 1.5rem 0
color: colors.$white
font-size: 1rem
font-weight: 600
@media (max-width: $mobile)
margin-bottom: 1rem
font-size: 0.9rem
.setting-row
display: grid
grid-template-columns: 1fr 1fr
gap: 2rem
@media (max-width: $tablet)
grid-template-columns: 1fr
gap: 1rem
.setting-item
padding: 0
border-bottom: none
.setting-label
margin-right: 1rem
@media (max-width: $mobile)
margin-right: 0
.settings-actions
display: flex
justify-content: flex-end
gap: 1rem
margin-top: 2rem
padding-top: 2rem
border-top: 1px solid colors.$gray
@media (max-width: $mobile)
flex-direction: column
margin-top: 1.5rem
padding-top: 1.5rem
gap: 0.75rem
button
min-width: 150px
@media (max-width: $mobile)
min-width: 100%
@@ -2,7 +2,7 @@ import "./styles.sass";
import { useKeymaps } from "@/common/contexts/KeymapContext.jsx";
import { useEffect, useState } from "react";
import Button from "@/common/components/Button";
import { mdiRestore, mdiMagnify, mdiRobotOutline, mdiCodeArray, mdiKeyboard, mdiBroadcast, mdiContentCopy, mdiFullscreen, mdiFlash } from "@mdi/js";
import { mdiRestore, mdiMagnify, mdiCodeArray, mdiKeyboard, mdiBroadcast, mdiContentCopy, mdiFullscreen, mdiFlash } from "@mdi/js";
import Icon from "@mdi/react";
import { useTranslation } from "react-i18next";
import { useToast } from "@/common/contexts/ToastContext.jsx";
@@ -10,7 +10,6 @@ import { useToast } from "@/common/contexts/ToastContext.jsx";
const KEYMAP_ICONS = {
"search": mdiMagnify,
"quick-action": mdiFlash,
"ai-menu": mdiRobotOutline,
"snippets": mdiCodeArray,
"keyboard-shortcuts": mdiKeyboard,
"broadcast": mdiBroadcast,
@@ -1,14 +1,13 @@
import "./styles.sass";
import { DialogProvider } from "@/common/components/Dialog";
import { useEffect, useState, useRef } from "react";
import { getRequest, patchRequest, putRequest, postRequest } from "@/common/utils/RequestUtil.js";
import { getRequest, patchRequest, putRequest } from "@/common/utils/RequestUtil.js";
import Button from "@/common/components/Button";
import { useToast } from "@/common/contexts/ToastContext.jsx";
import { useSnippets } from "@/common/contexts/SnippetContext.jsx";
import { useAI } from "@/common/contexts/AIContext.jsx";
import IconInput from "@/common/components/IconInput";
import SelectBox from "@/common/components/SelectBox";
import { mdiFormTextbox, mdiTextBox, mdiRobot, mdiCodeBrackets } from "@mdi/js";
import { mdiFormTextbox, mdiTextBox, mdiCodeBrackets } from "@mdi/js";
import Icon from "@mdi/react";
import { useTranslation } from "react-i18next";
import { OS_OPTIONS, parseOsFilter } from "@/common/utils/osUtils.js";
@@ -19,10 +18,8 @@ export const SnippetDialog = ({ open, onClose, editSnippetId, selectedOrganizati
const [command, setCommand] = useState("");
const [description, setDescription] = useState("");
const [osFilter, setOsFilter] = useState([]);
const [isGeneratingAI, setIsGeneratingAI] = useState(false);
const { sendToast } = useToast();
const { loadAllSnippets } = useSnippets();
const { isAIAvailable } = useAI();
const initialValues = useRef({ name: '', command: '', description: '', osFilter: [] });
@@ -103,21 +100,6 @@ export const SnippetDialog = ({ open, onClose, editSnippetId, selectedOrganizati
onClose();
};
const handleGenerateAICommand = async () => {
if (!name.trim() || !description.trim()) return;
setIsGeneratingAI(true);
try {
const response = await postRequest("ai/generate", { prompt: `${name}: ${description}` });
setCommand(response.command);
} catch (error) {
console.error("Error generating AI command:", error);
} finally {
setIsGeneratingAI(false);
}
};
const arraysEqual = (a, b) => {
if (a.length !== b.length) return false;
return a.every((val, i) => val === b[i]);
@@ -164,14 +146,7 @@ export const SnippetDialog = ({ open, onClose, editSnippetId, selectedOrganizati
</div>
<div className="form-group">
<div className="command-label-with-ai">
<label htmlFor="command">{t('snippets.dialog.fields.command')}</label>
{name.trim() && description.trim() && isAIAvailable() && (
<Button text={isGeneratingAI ? t('snippets.dialog.actions.generating') : t('snippets.dialog.actions.generateAI')}
icon={mdiRobot} onClick={handleGenerateAICommand} disabled={isGeneratingAI}
type="secondary" />
)}
</div>
<label htmlFor="command">{t('snippets.dialog.fields.command')}</label>
<div className="textarea-container">
<textarea id="command" value={command} onChange={(e) => setCommand(e.target.value)}
placeholder={t('snippets.dialog.placeholders.command')} rows={5} className="custom-textarea" />
@@ -60,12 +60,6 @@ $tablet: 1024px
@media (max-width: $mobile)
font-size: 0.8rem
.command-label-with-ai
display: flex
justify-content: space-between
align-items: center
margin-bottom: 0
.textarea-container
position: relative
width: 100%
+8
View File
@@ -106,3 +106,11 @@ make security-sbom
```
This only requires Docker or Podman on the host. The required Node/Yarn/pnpm tooling is executed inside ephemeral containers.
## Offline Runtime Controls
For production environments without internet access:
- Keep `ENABLE_SOURCE_SYNC=false` (default) to prevent outbound source synchronization.
- Keep `VITE_ENABLE_EXTERNAL_LINKS=false` in the client build (default) to block opening external links from the UI.
- Version checks are controlled by `ENABLE_VERSION_CHECK` (default: `true`) and can be disabled with `ENABLE_VERSION_CHECK=false`.
-330
View File
@@ -1,330 +0,0 @@
const AISettings = require("../models/AISettings");
const MonitoringSnapshot = require("../models/MonitoringSnapshot");
const logger = require("../utils/logger");
const OPENAI_BASE_URL = "https://api.openai.com/v1";
const DEFAULT_OLLAMA_URL = "http://localhost:11434";
const SYSTEM_PROMPT = process.env.AI_SYSTEM_PROMPT || `You are a Linux command generator assistant. Your job is to generate appropriate Linux/Unix shell commands based on user requests.
Rules:
1. Return ONLY the command(s), no explanations or markdown formatting
2. If multiple commands are needed, separate them with && or ;
3. Prefer safe, commonly available commands
4. If the request is unclear, provide the most likely intended command
5. For dangerous operations, use safer alternatives when possible
6. Always assume the user wants commands for a modern Linux system
Examples:
User: "list all files"
Response: ls -la
User: "find large files"
Response: find . -type f -size +100M -exec ls -lh {} + | sort -k5 -hr
User: "check memory usage"
Response: free -h && top -o %MEM -n 1`;
const normalizeUrl = (url) => url?.replace(/\/+$/, "") || "";
const authHeaders = (apiKey) => ({
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
});
const sanitizeSettingsResponse = (settings) => {
const response = settings.dataValues ? { ...settings.dataValues } : { ...settings };
response.enabled = Boolean(response.enabled);
if (response.apiKey) {
response.hasApiKey = true;
delete response.apiKey;
}
return response;
};
const getOrCreateSettings = async () => {
let settings = await AISettings.findOne();
if (!settings) settings = await AISettings.create({});
return settings;
};
const buildSystemPrompt = (osInfo, recentOutput) => {
let prompt = SYSTEM_PROMPT;
if (osInfo) {
const osParts = [];
if (osInfo.hostname) osParts.push(`hostname: ${osInfo.hostname}`);
if (osInfo.kernel) osParts.push(`kernel: ${osInfo.kernel}`);
if (osInfo.name) osParts.push(`distro: ${osInfo.name}`);
if (osInfo.version) osParts.push(`release: ${osInfo.version}`);
if (osParts.length) {
prompt += `\n\nServer info: ${osParts.join(', ')}`;
}
}
if (recentOutput) {
prompt += `\n\nRecent terminal output:\n${recentOutput}`;
}
return prompt;
};
const getProviderConfig = (settings) => {
const provider = settings.provider;
if (provider === "openai") {
return {
baseUrl: OPENAI_BASE_URL,
headers: authHeaders(settings.apiKey),
requiresApiKey: true,
requiresApiUrl: false,
};
}
if (provider === "openai_compatible") {
return {
baseUrl: normalizeUrl(settings.apiUrl),
headers: authHeaders(settings.apiKey),
requiresApiKey: true,
requiresApiUrl: true,
};
}
if (provider === "ollama") {
return {
baseUrl: normalizeUrl(settings.apiUrl) || DEFAULT_OLLAMA_URL,
headers: { "Content-Type": "application/json" },
requiresApiKey: false,
requiresApiUrl: false,
};
}
return null;
};
const validateProviderConfig = (settings, config) => {
if (!config) return { code: 400, message: "Unsupported provider" };
if (config.requiresApiKey && !settings.apiKey) {
const name = settings.provider === "openai" ? "OpenAI" : "OpenAI Compatible";
return { code: 400, message: `${name} API key not configured` };
}
if (config.requiresApiUrl && !settings.apiUrl) {
return { code: 400, message: "OpenAI Compatible API URL not configured" };
}
return null;
};
module.exports.getAISettings = async () => {
const settings = await getOrCreateSettings();
return sanitizeSettingsResponse(settings);
};
module.exports.updateAISettings = async (updateData) => {
const { enabled, provider, model, apiKey, apiUrl } = updateData;
const settings = await getOrCreateSettings();
const updatePayload = {};
if (enabled !== undefined) updatePayload.enabled = enabled;
if (provider !== undefined) updatePayload.provider = provider;
if (model !== undefined) updatePayload.model = model;
if (apiUrl !== undefined) updatePayload.apiUrl = apiUrl;
if (apiKey !== undefined) updatePayload.apiKey = apiKey === "" ? null : apiKey;
const settingsId = settings.dataValues ? settings.dataValues.id : settings.id;
await AISettings.update(updatePayload, { where: { id: settingsId } });
const updatedSettings = await AISettings.findOne();
return sanitizeSettingsResponse(updatedSettings);
};
module.exports.testAIConnection = async () => {
const settings = await AISettings.findOne();
if (!settings || !settings.enabled) return { code: 400, message: "AI is not enabled" };
if (!settings.provider) return { code: 400, message: "No AI provider configured" };
if (!settings.model) return { code: 400, message: "No AI model configured" };
const config = getProviderConfig(settings);
const validationError = validateProviderConfig(settings, config);
if (validationError) return validationError;
try {
const models = await fetchModelsForProvider(settings, config);
if (models.error) return models.error;
const modelExists = models.list.includes(settings.model);
if (!modelExists) {
const providerName = settings.provider === "ollama" ? "Ollama" :
settings.provider === "openai" ? "your OpenAI account" : "OpenAI Compatible API";
return { code: 400, message: `Configured model "${settings.model}" not found in ${providerName}` };
}
return { success: true, message: "Connection test successful" };
} catch (error) {
logger.error("AI connection test failed", { error: error.message, stack: error.stack });
return { code: 500, message: `Connection test failed: ${error.message}` };
}
};
const fetchModelsForProvider = async (settings, config) => {
const provider = settings.provider;
try {
if (provider === "ollama") {
const response = await fetch(`${config.baseUrl}/api/tags`, {
method: "GET",
headers: config.headers,
});
if (!response.ok) return { error: { code: 500, message: `Ollama API error: ${response.status}` } };
const data = await response.json();
return { list: data.models?.map(m => m.name).filter(Boolean) || [] };
}
const response = await fetch(`${config.baseUrl}/models`, {
headers: config.headers,
});
if (!response.ok) {
const providerName = provider === "openai" ? "OpenAI" : "OpenAI Compatible API";
return { error: { code: 500, message: `${providerName} error: ${response.status}` } };
}
const data = await response.json();
let models = data.data?.map(m => m.id).filter(Boolean) ||
data.models?.map(m => m.name || m.id).filter(Boolean) || [];
const excludePatterns = [
"whisper", "tts", "dall-e", "embedding", "embed",
"vision", "image", "audio", "speech", "moderation",
"instruct", "edit", "search", "similarity", "code-search",
"text-search", "realtime"
];
models = models.filter(id => {
const lowerId = id.toLowerCase();
if (provider === "openai" && !lowerId.includes("gpt")) return false;
return !excludePatterns.some(pattern => lowerId.includes(pattern));
}).sort();
return { list: models };
} catch (error) {
logger.error(`Error fetching models for ${provider}`, { error: error.message });
return { list: [] };
}
};
module.exports.getAvailableModels = async () => {
const settings = await AISettings.findOne();
if (!settings || !settings.provider) return { code: 400, message: "No AI provider configured" };
const config = getProviderConfig(settings);
const validationError = validateProviderConfig(settings, config);
if (validationError) return validationError;
const result = await fetchModelsForProvider(settings, config);
if (result.error) return result.error;
return { models: result.list };
};
module.exports.generateCommand = async (prompt, entryId, recentOutput) => {
const settings = await AISettings.findOne();
if (!settings || !settings.enabled) return { code: 400, message: "AI is not enabled" };
if (!settings.provider || !settings.model) return { code: 400, message: "AI not properly configured" };
let osInfo = null;
if (entryId) {
try {
const snapshot = await MonitoringSnapshot.findOne({ where: { entryId } });
if (snapshot?.osInfo) osInfo = snapshot.osInfo;
} catch (error) {
logger.error("Failed to fetch monitoring snapshot for AI", { entryId, error: error.message });
}
}
const systemPrompt = buildSystemPrompt(osInfo, recentOutput);
const config = getProviderConfig(settings);
if (!config) return { code: 400, message: "Unsupported AI provider" };
const command = settings.provider === "ollama"
? await generateOllamaCommand(prompt, settings, systemPrompt, config)
: await generateOpenAICommand(prompt, settings, systemPrompt, config);
return { command };
};
const generateOpenAICommand = async (prompt, settings, systemPrompt, config) => {
const response = await fetch(`${config.baseUrl}/chat/completions`, {
method: "POST",
headers: config.headers,
body: JSON.stringify({
model: settings.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
],
max_tokens: 150,
temperature: 0.3,
stop: ["\n\n"],
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(`API error: ${error.error?.message || response.status}`);
}
const data = await response.json();
return parseAIResponse(data.choices[0]?.message?.content?.trim());
};
const generateOllamaCommand = async (prompt, settings, systemPrompt, config) => {
const response = await fetch(`${config.baseUrl}/api/chat`, {
method: "POST",
headers: config.headers,
body: JSON.stringify({
model: settings.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
],
stream: false,
options: {
temperature: 0.3,
num_predict: 150,
stop: ["\n\n"],
},
}),
});
if (!response.ok) throw new Error(`Ollama API error: ${response.status}`);
const data = await response.json();
return parseAIResponse(data.message?.content?.trim());
};
const parseAIResponse = (response) => {
if (!response) return "echo 'No command generated'";
let clean = response.replace(/```(?:bash|sh|shell)?\n?/g, "").replace(/```/g, "");
const responseMatch = clean.match(/Response:\s*(.+?)(?:\n|$)/i);
if (responseMatch) return responseMatch[1].trim().replace(/[\r\n]+$/, "");
if (clean.toLowerCase().startsWith("user:")) return "echo 'Command not properly generated'";
const lines = clean.split("\n").map(l => l.trim()).filter(Boolean);
if (lines.length > 1) {
const cmd = lines.find(l => !l.toLowerCase().startsWith("user:") && !l.toLowerCase().startsWith("response:"));
if (cmd) return cmd.trim().replace(/[\r\n]+$/, "");
}
if (clean.toLowerCase().startsWith("response:")) return "echo 'Command not properly generated'";
return clean.trim().replace(/[\r\n]+$/, "") || "echo 'No command generated'";
};
-1
View File
@@ -3,7 +3,6 @@ const Keymap = require("../models/Keymap");
const DEFAULT_KEYMAPS = [
{ action: "search", key: "ctrl+s" },
{ action: "quick-action", key: "ctrl+p" },
{ action: "ai-menu", key: "ctrl+k" },
{ action: "snippets", key: "ctrl+shift+s" },
{ action: "keyboard-shortcuts", key: "ctrl+shift+k" },
{ action: "broadcast", key: "ctrl+b" },
+17
View File
@@ -3,8 +3,13 @@ const Snippet = require("../models/Snippet");
const Script = require("../models/Script");
const crypto = require("crypto");
const logger = require("../utils/logger");
const { isSourceSyncEnabled } = require("../utils/security");
module.exports.validateSourceUrl = async (url) => {
if (!isSourceSyncEnabled()) {
return { valid: false, error: "Source synchronization is disabled by configuration." };
}
try {
const baseUrl = url.replace(/\/$/, "");
const indexUrl = `${baseUrl}/NTINDEX`;
@@ -166,6 +171,10 @@ module.exports.deleteSource = async (sourceId) => {
};
module.exports.syncSource = async (sourceId) => {
if (!isSourceSyncEnabled()) {
return { success: false, error: "Source synchronization is disabled by configuration." };
}
const source = await Source.findByPk(sourceId);
if (!source) {
return { success: false, error: "Source not found" };
@@ -329,6 +338,10 @@ module.exports.syncSource = async (sourceId) => {
};
module.exports.syncAllSources = async () => {
if (!isSourceSyncEnabled()) {
return;
}
const sources = await Source.findAll({ where: { enabled: true } });
for (const source of sources) {
@@ -430,6 +443,10 @@ const parseScriptContent = (content, defaultName) => {
};
module.exports.ensureDefaultSource = async () => {
if (!isSourceSyncEnabled()) {
return;
}
const DEFAULT_SOURCE_URL = "https://github.com/swissmakers/infra-manager";
const DEFAULT_SOURCE_NAME = "Official";
-1
View File
@@ -56,7 +56,6 @@ app.use("/api/entries/sftp", require("./routes/sftp"));
app.use("/api/users", authenticate, isAdmin, require("./routes/users"));
app.use("/api/sources", authenticate, isAdmin, require("./routes/source"));
app.use("/api/ai", authenticate, require("./routes/ai"));
app.use("/api/sessions", authenticate, require("./routes/session"));
app.use("/api/connections", authenticate, require("./routes/serverSession"));
app.use("/api/folders", authenticate, require("./routes/folder"));
@@ -349,46 +349,6 @@ module.exports = {
logger.info("Created app_sources table");
}
if (!tableNames.includes("ai_settings")) {
await queryInterface.createTable("ai_settings", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
enabled: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
},
provider: {
type: DataTypes.ENUM("ollama", "openai"),
allowNull: true,
},
model: {
type: DataTypes.STRING,
allowNull: true,
},
apiKey: {
type: DataTypes.TEXT,
allowNull: true,
},
apiUrl: {
type: DataTypes.STRING,
allowNull: true,
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updatedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
});
logger.info("Created ai_settings table");
}
if (!tableNames.includes("server_monitoring")) {
await queryInterface.createTable("server_monitoring", {
id: {
-40
View File
@@ -1,40 +0,0 @@
const Sequelize = require("sequelize");
const db = require("../utils/database");
module.exports = db.define("ai_settings", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
enabled: {
type: Sequelize.BOOLEAN,
defaultValue: false,
allowNull: false,
},
provider: {
type: Sequelize.ENUM("ollama", "openai"),
allowNull: true,
},
model: {
type: Sequelize.STRING,
allowNull: true,
},
apiKey: {
type: Sequelize.TEXT,
allowNull: true,
},
apiUrl: {
type: Sequelize.STRING,
allowNull: true,
defaultValue: "http://localhost:11434",
},
createdAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
updatedAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
});
-136
View File
@@ -1,136 +0,0 @@
const { Router } = require("express");
const { isAdmin } = require("../middlewares/permission");
const { validateSchema } = require("../utils/schema");
const { updateAISettingsValidation, generateCommandValidation } = require("../validations/ai");
const {
getAISettings,
updateAISettings,
testAIConnection,
getAvailableModels,
generateCommand
} = require("../controllers/ai");
const { validateEntryAccess } = require("../controllers/entry");
const Entry = require("../models/Entry");
const app = Router();
/**
* GET /ai
* @summary Get AI Settings
* @description Retrieves the current AI configuration settings including API keys, models, and connection details. Admin access required.
* @tags AI
* @produces application/json
* @security BearerAuth
* @return {object} 200 - AI configuration settings
*/
app.get("/", async (req, res) => {
try {
const settings = await getAISettings();
res.json(settings);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
/**
* PATCH /ai
* @summary Update AI Settings
* @description Updates AI configuration settings such as API keys, model selection, and connection parameters. Admin access required.
* @tags AI
* @produces application/json
* @security BearerAuth
* @param {UpdateAISettings} request.body.required - Updated AI configuration settings
* @return {object} 200 - Updated AI settings
* @return {object} 403 - Admin access required
*/
app.patch("/", isAdmin, async (req, res) => {
try {
if (validateSchema(res, updateAISettingsValidation, req.body)) return;
const updatedSettings = await updateAISettings(req.body);
res.json(updatedSettings);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
/**
* POST /ai/test
* @summary Test AI Connection
* @description Tests the connection to the configured AI service to verify settings and connectivity. Admin access required.
* @tags AI
* @produces application/json
* @security BearerAuth
* @return {object} 200 - Connection test successful
* @return {object} 400 - Connection test failed
* @return {object} 403 - Admin access required
*/
app.post("/test", isAdmin, async (req, res) => {
try {
const result = await testAIConnection();
if (result.code) return res.status(result.code).json({ error: result.message });
res.json(result);
} catch (error) {
res.status(500).json({ error: "Connection test failed" });
}
});
/**
* GET /ai/models
* @summary Get Available AI Models
* @description Retrieves a list of available AI models that can be used for command generation and assistance.
* @tags AI
* @produces application/json
* @security BearerAuth
* @return {array} 200 - List of available AI models
* @return {object} 400 - Failed to retrieve models
*/
app.get("/models", async (req, res) => {
try {
const result = await getAvailableModels();
if (result.code) return res.status(result.code).json({ error: result.message });
res.json(result);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
});
/**
* POST /ai/generate
* @summary Generate AI Command
* @description Generates shell commands or scripts based on natural language prompts using AI assistance.
* @tags AI
* @produces application/json
* @security BearerAuth
* @param {GenerateCommand} request.body.required - Prompt text for command generation
* @return {object} 200 - Generated command or script
* @return {object} 400 - Failed to generate command
*/
app.post("/generate", async (req, res) => {
try {
if (validateSchema(res, generateCommandValidation, req.body)) return;
const { prompt, entryId, recentOutput } = req.body;
if (entryId) {
const entry = await Entry.findByPk(entryId);
const accessCheck = await validateEntryAccess(req.user.id, entry);
if (accessCheck.code) {
return res.status(accessCheck.code).json({ error: accessCheck.message });
}
}
const result = await generateCommand(prompt, entryId, recentOutput);
if (result.code) return res.status(result.code).json({ error: result.message });
res.json(result);
} catch (error) {
res.status(500).json({ error: "Failed to generate command" });
}
});
module.exports = app;
+1 -1
View File
@@ -31,7 +31,7 @@ app.get("/", authenticate, async (req, res) => {
* @tags Keymaps
* @produces application/json
* @security BearerAuth
* @param {string} action.path.required - The action identifier (e.g., 'search', 'ai-menu', 'snippets', 'keyboard-shortcuts')
* @param {string} action.path.required - The action identifier (e.g., 'search', 'snippets', 'keyboard-shortcuts')
* @param {object} request.body.required - Updates to apply (key and/or enabled)
* @return {object} 200 - Keymap successfully updated
* @return {object} 400 - Key combination already in use or invalid request
+76
View File
@@ -1,6 +1,7 @@
const express = require("express");
const { getFTSStatus } = require("../controllers/account");
const packageJson = require("../../package.json");
const { isVersionCheckEnabled } = require("../utils/security");
const app = express.Router();
@@ -30,4 +31,79 @@ app.get("/version", (req, res) => {
res.json({ version: packageJson.version });
});
const compareVersions = (a, b) => {
const aParts = String(a).replace(/^v/, "").split(".").map((p) => parseInt(p, 10) || 0);
const bParts = String(b).replace(/^v/, "").split(".").map((p) => parseInt(p, 10) || 0);
const maxLength = Math.max(aParts.length, bParts.length);
for (let i = 0; i < maxLength; i++) {
const left = aParts[i] || 0;
const right = bParts[i] || 0;
if (left > right) return 1;
if (left < right) return -1;
}
return 0;
};
/**
* GET /service/version/check
* @summary Check Latest Version
* @description Checks the latest published release tag from GitHub. Can be disabled with ENABLE_VERSION_CHECK=false.
* @tags Service
* @produces application/json
* @return {object} 200 - Version check result
*/
app.get("/version/check", async (req, res) => {
const currentVersion = packageJson.version;
if (!isVersionCheckEnabled()) {
return res.json({
enabled: false,
currentVersion,
latestVersion: currentVersion,
updateAvailable: false,
});
}
try {
const response = await fetch("https://api.github.com/repos/swissmakers/infra-manager/releases/latest", {
method: "GET",
headers: {
"User-Agent": "Infram-Version-Checker/1.0",
Accept: "application/vnd.github+json",
},
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
return res.status(502).json({
enabled: true,
currentVersion,
code: 502,
message: `Failed to fetch latest release (HTTP ${response.status})`,
});
}
const payload = await response.json();
const latestVersion = String(payload?.tag_name || "").replace(/^v/, "") || currentVersion;
const updateAvailable = compareVersions(currentVersion, latestVersion) < 0;
return res.json({
enabled: true,
currentVersion,
latestVersion,
updateAvailable,
releaseUrl: payload?.html_url || null,
});
} catch (error) {
return res.status(502).json({
enabled: true,
currentVersion,
code: 502,
message: error.message,
});
}
});
module.exports = app;
+2
View File
@@ -9,6 +9,7 @@ const parseBooleanEnv = (name, defaultValue) => {
const isStrictTlsEnabled = () => parseBooleanEnv("STRICT_TLS", true);
const isSourceSyncEnabled = () => parseBooleanEnv("ENABLE_SOURCE_SYNC", false);
const isVersionCheckEnabled = () => parseBooleanEnv("ENABLE_VERSION_CHECK", true);
const createHttpsAgent = () => new https.Agent({
rejectUnauthorized: isStrictTlsEnabled(),
@@ -22,6 +23,7 @@ module.exports = {
parseBooleanEnv,
isStrictTlsEnabled,
isSourceSyncEnabled,
isVersionCheckEnabled,
createHttpsAgent,
getLdapTlsOptions,
};
-15
View File
@@ -1,15 +0,0 @@
const Joi = require('joi');
module.exports.updateAISettingsValidation = Joi.object({
enabled: Joi.boolean(),
provider: Joi.string().valid('ollama', 'openai', 'openai_compatible').allow(null),
model: Joi.string().max(100).allow(null),
apiKey: Joi.string().allow('', null),
apiUrl: Joi.string().allow(null)
});
module.exports.generateCommandValidation = Joi.object({
prompt: Joi.string().required(),
entryId: Joi.number().integer().optional(),
recentOutput: Joi.string().max(5000).allow(null, '').optional()
});