mirror of
https://github.com/swissmakers/infram.git
synced 2026-05-08 22:49:00 +02:00
Remove all AI integrations and OpenAI libraries/functions and so on..
This commit is contained in:
+2
-1
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
VITE_ENABLE_EXTERNAL_LINKS=false
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Нет доступных сниппетов",
|
||||
|
||||
@@ -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": "无可用代码片段",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
-85
@@ -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
@@ -1 +0,0 @@
|
||||
export { AICommandPopover as default } from "./AICommandPopover";
|
||||
-141
@@ -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
|
||||
@@ -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%
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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'";
|
||||
};
|
||||
@@ -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" },
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
Reference in New Issue
Block a user