Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions client/src/components/LLMHelper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,11 @@
<div class="mt-3 bg-slate-100 dark:bg-slate-700/30 rounded-lg p-3 border border-slate-200 dark:border-slate-700/50">
<textarea
v-model="manualPrompt"
placeholder="Ask the assistant for help or suggest a command..."
placeholder="Ask the assistant for help or suggest a command... (Press Enter to send)"
class="w-full p-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 dark:focus:border-indigo-500 transition-all duration-200"
:disabled="!helperEnabled || isProcessing || !isReady"
rows="3"
@keydown="handleKeydown"
></textarea>
<div class="flex justify-between mt-2">
<button
Expand Down Expand Up @@ -227,6 +228,15 @@ const sendPrompt = async () => {
}
}

const handleKeydown = (event) => {
// Send message on Enter key (without Shift)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendPrompt()
}
// Allow Shift+Enter for new lines
}

const clearHelperHistory = () => {
llmHelperStore.clearHistory()
}
Expand Down Expand Up @@ -272,10 +282,10 @@ const getUniqueHistory = () => {
const uniqueItems = [];
const uniqueContentMap = new Map();

// Get items in reverse order (newest first)
const reversedHistory = [...history.value].reverse();
// Keep items in chronological order (oldest first, newest last)
const chronologicalHistory = [...history.value];

reversedHistory.forEach(item => {
chronologicalHistory.forEach(item => {
// Create a unique identifier based on message content and timestamp
const contentKey = item.type + '-' +
(item.type === 'response' ? item.content.message :
Expand Down
10 changes: 0 additions & 10 deletions client/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">SSH Sessions</h1>
</div>
<div class="flex space-x-3">
<router-link
to="/debug"
class="btn-outline flex items-center"
title="SSH Debug Tool"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
Debug Tool
</router-link>
<button
@click="showCreateModal = true"
class="btn-primary"
Expand Down
159 changes: 154 additions & 5 deletions client/src/views/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,95 @@
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
>
<option value="openai">OpenAI</option>
<option value="gemini">Google Gemini</option>
<option value="ollama">Ollama</option>
<option value="custom">Custom API (OpenAI compatible)</option>
</select>
</div>

<!-- OpenAI API Key -->
<div v-else-if="setting.id === 'openai_api_key'">
<div class="relative">
<input
:id="setting.id"
v-model="formValues[setting.id]"
:type="showSensitive[setting.id] ? 'text' : 'password'"
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
placeholder="Enter your OpenAI API key"
/>
<button
type="button"
@click="toggleSensitive(setting.id)"
class="absolute right-2 top-2 text-slate-500 dark:text-slate-400"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="!showSensitive[setting.id]"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
v-if="!showSensitive[setting.id]"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
</button>
</div>
</div>

<!-- Gemini API Key -->
<div v-else-if="setting.id === 'gemini_api_key'">
<div class="relative">
<input
:id="setting.id"
v-model="formValues[setting.id]"
:type="showSensitive[setting.id] ? 'text' : 'password'"
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
placeholder="Enter your Gemini API key"
/>
<button
type="button"
@click="toggleSensitive(setting.id)"
class="absolute right-2 top-2 text-slate-500 dark:text-slate-400"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-if="!showSensitive[setting.id]"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
v-if="!showSensitive[setting.id]"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
</button>
</div>
</div>

<!-- Toggle for Registration Enabled -->
<div v-else-if="setting.id === 'registration_enabled' && isAdmin && !userSettings">
Expand Down Expand Up @@ -238,7 +323,6 @@
type="text"
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
placeholder="https://your-api-endpoint.com/v1"
:disabled="formValues['llm_provider'] !== 'custom'"
/>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
Enter the base URL for your OpenAI-compatible API (including /v1 if needed)
Expand All @@ -254,7 +338,6 @@
:type="showSensitive[setting.id] ? 'text' : 'password'"
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
placeholder="Enter your API key"
:disabled="formValues['llm_provider'] !== 'custom'"
/>
<button
type="button"
Expand Down Expand Up @@ -306,12 +389,29 @@
type="text"
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
placeholder="Enter model name (e.g., claude-3-opus, gemini-pro)"
:disabled="formValues['llm_provider'] !== 'custom'"
/>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
Enter the model name supported by your custom API service
</p>
</div>

<!-- Select for Gemini Model -->
<div v-else-if="setting.id === 'gemini_model'">
<select
:id="setting.id"
v-model="formValues[setting.id]"
class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white"
>
<option value="gemini-2.5-flash">Gemini 2.5 Flash (Recommended)</option>
<option value="gemini-2.5-pro">Gemini 2.5 Pro (Most Powerful)</option>
<option value="gemini-2.5-flash-lite">Gemini 2.5 Flash-Lite (Cost Efficient)</option>
<option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
<option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
</select>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
Choose the Gemini model to use for AI assistance. 2.5 Flash offers the best price-performance balance.
</p>
</div>
</div>

<div class="flex space-x-4 mt-8">
Expand Down Expand Up @@ -377,7 +477,56 @@ const visibleCategories = computed(() => {

// Computed property to get settings for the active category
const activeCategorySettings = computed(() => {
return settingsStore.getSettingsByCategory(activeCategory.value);
const allCategorySettings = settingsStore.getSettingsByCategory(activeCategory.value);

// If not in LLM category, return all settings
if (activeCategory.value !== 'llm') {
return allCategorySettings;
}

// Filter LLM settings based on selected provider
const selectedProvider = formValues.value['llm_provider'] || 'openai';

const filteredSettings = allCategorySettings.filter(setting => {
// Always show the provider selector
if (setting.id === 'llm_provider') {
return true;
}

// Show settings based on selected provider
switch (selectedProvider) {
case 'openai':
return ['openai_api_key', 'openai_model'].includes(setting.id);
case 'gemini':
return ['gemini_api_key', 'gemini_model'].includes(setting.id);
case 'ollama':
return ['ollama_url', 'ollama_model'].includes(setting.id);
case 'custom':
return ['custom_api_url', 'custom_api_key', 'custom_model'].includes(setting.id);
default:
return false;
}
});

// Sort settings to ensure proper order: provider first, then API key, then model/url, then model
const settingOrder = {
'llm_provider': 1,
'openai_api_key': 2,
'openai_model': 3,
'gemini_api_key': 2,
'gemini_model': 3,
'ollama_url': 2,
'ollama_model': 3,
'custom_api_url': 2,
'custom_api_key': 3,
'custom_model': 4
};

return filteredSettings.sort((a, b) => {
const orderA = settingOrder[a.id] || 999;
const orderB = settingOrder[b.id] || 999;
return orderA - orderB;
});
});

// Computed property to check if there are any changes
Expand Down Expand Up @@ -416,7 +565,7 @@ const getPlaceholder = (setting) => {

// Method to check if a setting has a special input type
const isSpecialInput = (id) => {
return ['llm_provider', 'openai_model', 'ollama_model', 'custom_api_url', 'custom_api_key', 'custom_model'].includes(id);
return ['llm_provider', 'openai_model', 'ollama_model', 'custom_api_url', 'custom_api_key', 'custom_model', 'gemini_api_key', 'gemini_model'].includes(id);
};

// Method to toggle visibility of sensitive values
Expand Down
12 changes: 1 addition & 11 deletions client/src/views/TerminalView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ const pasteFromClipboard = () => {
navigator.clipboard.readText()
.then(text => {
if (text && terminal.value) {
terminalStore.sendInput(text)
terminal.value.paste(text)
}
})
.catch(err => {
Expand Down Expand Up @@ -691,16 +691,6 @@ onMounted(async () => {
}
}

// Ctrl+V or Command+V to paste
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
navigator.clipboard.readText()
.then(text => {
if (text && terminal.value) {
terminalStore.sendInput(text)
}
})
e.preventDefault()
}
})
})

Expand Down
2 changes: 2 additions & 0 deletions server/src/db/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ async function insertDefaultSettings() {
{ id: 'custom_api_url', name: 'Custom API URL', value: '', category: 'llm', description: 'Base URL for custom OpenAI-compatible API', is_sensitive: 0 },
{ id: 'custom_api_key', name: 'Custom API Key', value: '', category: 'llm', description: 'API key for custom OpenAI-compatible API', is_sensitive: 1 },
{ id: 'custom_model', name: 'Custom Model', value: 'gpt-3.5-turbo', category: 'llm', description: 'Model name for custom API', is_sensitive: 0 },
{ id: 'gemini_api_key', name: 'Gemini API Key', value: '', category: 'llm', description: 'API key for Google Gemini', is_sensitive: 1 },
{ id: 'gemini_model', name: 'Gemini Model', value: 'gemini-2.5-flash', category: 'llm', description: 'Model name for Gemini', is_sensitive: 0 },

// Encryption settings
{ id: 'encryption_key', name: 'Encryption Key', value: '736f4149702aae82ab6e45e64d977e3c6c1e9f7b29b368f61cafab1b9c2cc3b2', category: 'security', description: 'Encryption key for sensitive data', is_sensitive: 1 },
Expand Down
Loading