diff --git a/locales/de.json b/locales/de.json index fc400c3..2d2bf7c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -153,14 +153,14 @@ "modal": { "title": "Gib deine API-Zugangsdaten ein", "apikey": "API-Schlüssel", - "apikeyplaceholder": "Um deinen Schlüssel nicht mehr zu verwenden: /ai use_custom_api false", + "apikeyplaceholder": "Um deinen Schlüssel nicht mehr zu verwenden: /ai custom_setup:false", "apiurl": "API-URL", "apiurlplaceholder": "Deine API-URL", "model": "Modell", "modelplaceholder": "Modellname (z.B. gpt-4)" }, "nopendingrequest": "Keine ausstehende Anfrage gefunden. Bitte versuche den Befehl erneut.", - "apicredssaved": "API-Zugangsdaten gespeichert. Du kannst jetzt den Befehl `/ai` verwenden, ohne deine Zugangsdaten erneut einzugeben. Um deinen Schlüssel nicht mehr zu verwenden, nutze `/ai use_custom_api false`", + "apicredssaved": "API-Zugangsdaten gespeichert. Du kannst jetzt den Befehl `/ai` verwenden, ohne deine Zugangsdaten erneut einzugeben. Um deinen Schlüssel nicht mehr zu verwenden, nutze `/ai custom_setup:false`", "process": { "dailylimit": "Du hast dein tägliches Limit an KI-Anfragen erreicht", "noapikey": "Bitte richte zuerst deinen API-Schlüssel ein" diff --git a/locales/en-US.json b/locales/en-US.json index 7e6f993..db89cdc 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -162,14 +162,14 @@ "modal": { "title": "Enter your API Credentials", "apikey": "API Key", - "apikeyplaceholder": "To stop using your key: /ai use_custom_api false", + "apikeyplaceholder": "To stop using your key: /ai custom_setup:false", "apiurl": "API Url", "apiurlplaceholder": "Your API Url", "model": "Model", "modelplaceholder": "Model name (eg. gpt-4)" }, "nopendingrequest": "No pending request found. Please try the command again.", - "apicredssaved": "API credentials saved. You can now use the `/ai` command without re-entering your credentials. To stop using your key, do `/ai use_custom_api false`", + "apicredssaved": "API credentials saved. You can now use the `/ai` command without re-entering your credentials. To stop using your key, do `/ai custom_setup:false`", "testing": "Testing API key...", "testfailed": "❌ API key test failed: {error}. Please check your credentials and try again.", "testsuccess": "✅ API key test successful! Credentials saved.", diff --git a/locales/es-419.json b/locales/es-419.json index 096fa82..b2d8deb 100644 --- a/locales/es-419.json +++ b/locales/es-419.json @@ -153,14 +153,14 @@ "modal": { "title": "Ingresa tus credenciales de API", "apikey": "Clave API", - "apikeyplaceholder": "Para dejar de usar tu clave: /ai use_custom_api false", + "apikeyplaceholder": "Para dejar de usar tu clave: /ai custom_setup:false", "apiurl": "URL de la API", "apiurlplaceholder": "Tu URL de API", "model": "Modelo", "modelplaceholder": "Nombre del modelo (ej. gpt-4)" }, "nopendingrequest": "No se encontró ninguna solicitud pendiente. Por favor, intenta el comando de nuevo.", - "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai use_custom_api false`", + "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai custom_setup:false`", "process": { "dailylimit": "Has alcanzado tu límite diario de solicitudes de IA", "noapikey": "Por favor, configura primero tu clave API" diff --git a/locales/es-ES.json b/locales/es-ES.json index 63f4b33..3ba05ff 100644 --- a/locales/es-ES.json +++ b/locales/es-ES.json @@ -153,14 +153,14 @@ "modal": { "title": "Introduce tus credenciales de API", "apikey": "Clave API", - "apikeyplaceholder": "Para dejar de usar tu clave: /ai use_custom_api false", + "apikeyplaceholder": "Para dejar de usar tu clave: /ai custom_setup:false", "apiurl": "URL de la API", "apiurlplaceholder": "Tu URL de API", "model": "Modelo", "modelplaceholder": "Nombre del modelo (ej. gpt-4)" }, "nopendingrequest": "No se encontró ninguna solicitud pendiente. Por favor, intenta el comando de nuevo.", - "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai use_custom_api false`", + "apicredssaved": "Credenciales de API guardadas. Ahora puedes usar el comando `/ai` sin volver a ingresar tus credenciales. Para dejar de usar tu clave, usa `/ai custom_setup:false`", "process": { "dailylimit": "Has alcanzado tu límite diario de solicitudes de IA", "noapikey": "Por favor, configura primero tu clave API" diff --git a/locales/fr.json b/locales/fr.json index e472a40..a3dc05c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -153,14 +153,14 @@ "modal": { "title": "Entrez vos identifiants API", "apikey": "Clé API", - "apikeyplaceholder": "Pour ne plus utiliser votre clé : /ai use_custom_api false", + "apikeyplaceholder": "Pour ne plus utiliser votre clé : /ai custom_setup:false", "apiurl": "URL de l'API", "apiurlplaceholder": "Votre URL d'API", "model": "Modèle", "modelplaceholder": "Nom du modèle (ex. gpt-4)" }, "nopendingrequest": "Aucune demande en attente trouvée. Veuillez réessayer la commande.", - "apicredssaved": "Identifiants API enregistrés. Vous pouvez maintenant utiliser la commande `/ai` sans ressaisir vos identifiants. Pour ne plus utiliser votre clé, faites `/ai use_custom_api false`", + "apicredssaved": "Identifiants API enregistrés. Vous pouvez maintenant utiliser la commande `/ai` sans ressaisir vos identifiants. Pour ne plus utiliser votre clé, faites `/ai custom_setup:false`", "process": { "dailylimit": "Vous avez atteint votre limite quotidienne de requêtes IA", "noapikey": "Veuillez d'abord configurer votre clé API" diff --git a/locales/ja.json b/locales/ja.json index 14c6bc5..0efa342 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -153,14 +153,14 @@ "modal": { "title": "API認証情報を入力してください", "apikey": "APIキー", - "apikeyplaceholder": "キーの使用をやめるには: /ai use_custom_api false", - "apiurl": "APIのURL", + "apikeyplaceholder": "キーの使用をやめるには: /ai custom_setup:false", "apiurlplaceholder": "あなたのAPIのURL", "model": "モデル", - "modelplaceholder": "モデル名(例: gpt-4)" + "modelplaceholder": "モデル名(例: gpt-4)", + "customsetup": "カスタム設定" }, "nopendingrequest": "保留中のリクエストが見つかりません。もう一度コマンドをお試しください。", - "apicredssaved": "API認証情報が保存されました。これで再入力せずに`/ai`コマンドを使えます。キーの使用をやめるには `/ai use_custom_api false` を使ってください", + "apicredssaved": "API認証情報が保存されました。これで認証情報を再入力することなく`/ai`コマンドを使用できます。キーの使用をやめるには、`/ai custom_setup:false`を実行してください", "process": { "dailylimit": "AIリクエストの1日上限に達しました", "noapikey": "まずAPIキーを設定してください" diff --git a/locales/pt-BR.json b/locales/pt-BR.json index aeb4af8..78108ff 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -153,14 +153,14 @@ "modal": { "title": "Insira suas credenciais de API", "apikey": "Chave de API", - "apikeyplaceholder": "Para parar de usar sua chave: /ai use_custom_api false", + "apikeyplaceholder": "Para parar de usar sua chave: /ai custom_setup:false", "apiurl": "URL da API", "apiurlplaceholder": "Sua URL de API", "model": "Modelo", "modelplaceholder": "Nome do modelo (ex. gpt-4)" }, "nopendingrequest": "Nenhuma solicitação pendente encontrada. Por favor, tente o comando novamente.", - "apicredssaved": "Credenciais de API salvas. Agora você pode usar o comando `/ai` sem reinserir suas credenciais. Para parar de usar sua chave, use `/ai use_custom_api false`", + "apicredssaved": "Credenciais de API salvas. Agora você pode usar o comando `/ai` sem reinserir suas credenciais. Para parar de usar sua chave, use `/ai custom_setup:false`", "process": { "dailylimit": "Você atingiu seu limite diário de solicitações de IA", "noapikey": "Por favor, configure sua chave de API primeiro" diff --git a/locales/tr.json b/locales/tr.json index 3b21497..3fce837 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -153,14 +153,14 @@ "modal": { "title": "API Kimlik Bilgilerinizi Girin", "apikey": "API Anahtarı", - "apikeyplaceholder": "Kendi anahtarınızı kullanmayı bırakmak için: /ai use_custom_api false", + "apikeyplaceholder": "Kendi anahtarınızı kullanmayı bırakmak için: /ai custom_setup:false", "apiurl": "API Adresi", "apiurlplaceholder": "API Adresiniz", "model": "Model", "modelplaceholder": "Model adı (örn. gpt-4)" }, "nopendingrequest": "Bekleyen bir istek bulunamadı. Lütfen komutu tekrar deneyin.", - "apicredssaved": "API kimlik bilgileri kaydedildi. Artık `/ai` komutunu anahtarınızı tekrar girmeden kullanabilirsiniz. Kendi anahtarınızı kullanmayı bırakmak için /ai use_custom_api false yazın", + "apicredssaved": "API kimlik bilgileri kaydedildi. Artık `/ai` komutunu anahtarınızı tekrar girmeden kullanabilirsiniz. Kendi anahtarınızı kullanmayı bırakmak için /ai custom_setup:false yazın", "process": { "dailylimit": "Günlük AI istek limitinize ulaştınız", "noapikey": "Lütfen önce API anahtarınızı ayarlayın" diff --git a/src/commands/utilities/ai.ts b/src/commands/utilities/ai.ts index d0663c2..70d8607 100644 --- a/src/commands/utilities/ai.ts +++ b/src/commands/utilities/ai.ts @@ -1,25 +1,13 @@ import type { ToolCall } from '@/utils/commandExecutor'; import { extractToolCalls, executeToolCall } from '@/utils/commandExecutor'; import BotClient from '@/services/Client'; -import { - buildProviderModal, - PROVIDER_TO_URL, - parseV2ModalSubmission, - type V2ModalPayload, -} from '@/types/componentsV2'; + import { SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, EmbedBuilder, - ModalBuilder, - TextInputBuilder, - ActionRowBuilder, - TextInputStyle, - StringSelectMenuInteraction, - ModalSubmitInteraction, ChatInputCommandInteraction, InteractionResponse, - Message, InteractionContextType, ApplicationIntegrationType, MessageFlags, @@ -34,25 +22,13 @@ import { createCommandLogger } from '@/utils/commandLogger'; import { createErrorHandler } from '@/utils/errorHandler'; import { createMemoryManager } from '@/utils/memoryManager'; -function getInvokerId( - interaction: ChatInputCommandInteraction | ModalSubmitInteraction | StringSelectMenuInteraction, -): string { - if ('guildId' in interaction && interaction.guildId) { +function getInvokerId(interaction: ChatInputCommandInteraction): string { + if (interaction.guildId) { return `${interaction.guildId}-${interaction.user.id}`; } return interaction.user.id; } -const ALLOWED_API_HOSTS = [ - 'api.openai.com', - 'openrouter.ai', - 'generativelanguage.googleapis.com', - 'api.anthropic.com', - 'api.deepseek.com', - 'api.moonshot.ai', - 'api.perplexity.ai', -]; - interface ConversationMessage { role: 'system' | 'user' | 'assistant'; content: @@ -87,39 +63,12 @@ interface PendingRequest { status?: 'awaiting' | 'processing'; } -type ModalRawEntry = { data?: { components?: unknown[] }; components?: unknown[] }; -type ClientWithModalState = BotClient & { lastModalRawByUser?: Map }; - interface UserCredentials { apiKey?: string | null; model?: string | null; apiUrl?: string | null; } -async function showV2Modal( - interaction: ChatInputCommandInteraction | StringSelectMenuInteraction, - v2Modal: V2ModalPayload, -): Promise { - const url = `https://discord.com/api/v10/interactions/${interaction.id}/${interaction.token}/callback?with_response=false`; - const body = { - type: 9, - data: { - custom_id: v2Modal.custom_id, - title: v2Modal.title, - components: v2Modal.components, - }, - }; - const resp = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - if (!resp.ok) { - const text = await resp.text(); - throw new Error(`Discord API error showing modal: ${resp.status} ${text}`); - } -} - interface User { user_id: string; api_key_encrypted?: string; @@ -490,92 +439,6 @@ async function incrementAndCheckServerDailyLimit(serverId: string, limit = 20): } } -async function testApiKey( - apiKey: string, - model: string, - apiUrl: string, -): Promise<{ success: boolean; error?: string }> { - try { - const url = new URL(apiUrl); - const host = url.hostname; - - if (host === 'generativelanguage.googleapis.com') { - const base = apiUrl.replace(/\/$/, ''); - const modelName = model.replace(/^models\//, ''); - const endpoint = `${base}/v1beta/models/${modelName}:generateContent?key=${encodeURIComponent(apiKey)}`; - - const resp = await fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - contents: [ - { - parts: [ - { - text: 'Hello! This is a test message. Please respond with "API key test successful!"', - }, - ], - }, - ], - }), - }); - - if (!resp.ok) { - const text = await resp.text(); - throw new Error(`${resp.status} ${text || resp.statusText}`); - } - } else if (host === 'api.perplexity.ai') { - const resp = await fetch(`${url.origin}/chat/completions`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model, - messages: [ - { - role: 'user', - content: - 'Hello! This is a test message. Please respond with "API key test successful!"', - }, - ], - max_tokens: 50, - temperature: 0.1, - }), - }); - if (!resp.ok) { - const text = await resp.text(); - throw new Error(`${resp.status} ${text || resp.statusText}`); - } - } else { - const client = getOpenAIClient(apiKey, apiUrl); - await client.chat.completions.create({ - model, - messages: [ - { - role: 'user', - content: - 'Hello! This is a test message. Please respond with "API key test successful!"', - }, - ], - max_tokens: 50, - temperature: 0.1, - }); - } - - logger.info('API key test successful'); - return { success: true }; - } catch (error: unknown) { - logger.error('Error testing API key:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - return { - success: false, - error: errorMessage, - }; - } -} - async function makeAIRequest( config: ReturnType, conversation: ConversationMessage[], @@ -709,7 +572,6 @@ async function makeAIRequest( } } - // Last resort: try to find any text in the response const seen = new Set(); const queue: unknown[] = [obj]; while (queue.length) { @@ -913,7 +775,7 @@ async function makeAIRequest( async function processAIRequest( client: BotClient, - interaction: ChatInputCommandInteraction | ModalSubmitInteraction, + interaction: ChatInputCommandInteraction, promptOverride?: string, ): Promise { try { @@ -934,24 +796,17 @@ async function processAIRequest( await interaction.editReply('❌ Missing prompt. Please try again.'); return; } - if ('commandType' in interaction) { - commandLogger.logFromInteraction( - interaction as ChatInputCommandInteraction, - `AI command executed - prompt content hidden for privacy`, - ); - } else { - commandLogger.logAction({ - isGuild: interaction.inGuild(), - isDM: !interaction.inGuild(), - additionalInfo: `AI command executed - prompt content hidden for privacy`, - }); - } + commandLogger.logFromInteraction( + interaction, + `AI command executed - prompt content hidden for privacy`, + ); const { apiKey, model, apiUrl } = await getUserCredentials(interaction.user.id); const config = getApiConfiguration(apiKey ?? null, model ?? null, apiUrl ?? null); - const exemptUserId = process.env.AI_EXEMPT_USER_ID; + const exemptUserId = process.env.AI_EXEMPT_USER_ID?.trim(); + const userId = interaction.user.id; - if (invokerId !== exemptUserId && config.usingDefaultKey) { - const allowed = await incrementAndCheckDailyLimit(interaction.user.id, 10); + if (userId !== exemptUserId && config.usingDefaultKey) { + const allowed = await incrementAndCheckDailyLimit(userId, 50); // Increased global limit for slash command if (!allowed) { await interaction.editReply( '❌ ' + @@ -959,16 +814,6 @@ async function processAIRequest( ); return; } - - if (interaction.inGuild()) { - const serverAllowed = await incrementAndCheckServerDailyLimit(interaction.guildId!, 50); - if (!serverAllowed) { - await interaction.editReply( - '❌ This server has reached its daily AI usage limit. Please try again tomorrow.', - ); - return; - } - } } if (!config.finalApiKey && config.usingDefaultKey) { @@ -978,7 +823,7 @@ async function processAIRequest( return; } - const existingConversation = userConversations.get(invokerId) || []; + const existingConversation = userConversations.get(userId) || []; const _conversationArray = Array.isArray(existingConversation) ? existingConversation : []; const chatInputInteraction = 'commandType' in interaction ? (interaction as ChatInputCommandInteraction) : undefined; @@ -1021,19 +866,16 @@ async function processAIRequest( await sendAIResponse(interaction, aiResponse, client); } catch (error) { const err = error as Error; - if ( - 'commandType' in interaction || - 'componentType' in (interaction as unknown as Record) - ) { + if (errorHandler) { await errorHandler({ - interaction: interaction as unknown as ChatInputCommandInteraction, + interaction: interaction as ChatInputCommandInteraction, client, error: err, userId: getInvokerId(interaction), username: interaction.user.tag, }); } else { - const msg = await client.getLocaleText('failedrequest', interaction.locale); + const msg = await client.getLocaleText('failedrequest', interaction.locale || 'en-US'); try { if (interaction.deferred || interaction.replied) { await interaction.editReply(msg); @@ -1051,7 +893,7 @@ async function processAIRequest( } async function sendAIResponse( - interaction: ChatInputCommandInteraction | ModalSubmitInteraction, + interaction: ChatInputCommandInteraction, aiResponse: AIResponse, _client: BotClient, ): Promise { @@ -1272,11 +1114,6 @@ interface AICommand { client: BotClient, interaction: ChatInputCommandInteraction, ) => Promise>; - handleModal: ( - client: BotClient, - interaction: ModalSubmitInteraction, - ) => Promise>; - handleSelect: (client: BotClient, interaction: StringSelectMenuInteraction) => Promise; } const aiCommand: AICommand = { @@ -1311,7 +1148,7 @@ const aiCommand: AICommand = { .setRequired(true), ) .addBooleanOption((option) => - option.setName('use_custom_api').setDescription('Use your own API key?').setRequired(false), + option.setName('custom_setup').setDescription('Use your own API key?').setRequired(false), ) .addBooleanOption((option) => option.setName('reset').setDescription('Reset your AI chat history').setRequired(false), @@ -1334,7 +1171,7 @@ const aiCommand: AICommand = { } try { - const useCustomApi = interaction.options.getBoolean('use_custom_api'); + const customSetup = interaction.options.getBoolean('custom_setup'); const prompt = interaction.options.getString('prompt')!; const reset = interaction.options.getBoolean('reset'); @@ -1355,7 +1192,30 @@ const aiCommand: AICommand = { return; } - if (useCustomApi === false) { + if (customSetup !== null) { + const { apiKey } = await getUserCredentials(interaction.user.id); + + if (customSetup) { + if (!apiKey) { + const setupUrl = process.env.FRONTEND_URL + ? `${process.env.FRONTEND_URL}/api-keys` + : 'the API keys page'; + + return interaction.reply({ + content: `🔑 Please set up your API key first by visiting: ${setupUrl}\n\nAfter setting up your API key, you can use the AI command with your custom key.`, + flags: MessageFlags.Ephemeral, + }); + } + const setupUrl = process.env.FRONTEND_URL + ? `${process.env.FRONTEND_URL}/api-keys` + : 'the API keys page'; + + await interaction.reply({ + content: `✅ You're already using a custom API key. To change your API key settings, please visit: ${setupUrl}`, + flags: MessageFlags.Ephemeral, + }); + return; + } await setUserApiKey(interaction.user.id, null, null, null); userConversations.delete(userId); await interaction.reply({ @@ -1366,13 +1226,6 @@ const aiCommand: AICommand = { return; } - const { apiKey } = await getUserCredentials(interaction.user.id); - if (useCustomApi && !apiKey) { - const title = await client.getLocaleText('commands.ai.modal.title', interaction.locale); - const v2Modal = buildProviderModal('apiCredentials', title); - await showV2Modal(interaction, v2Modal); - return; - } await processAIRequest(client, interaction); } catch (error) { console.error('Error in AI command:', error); @@ -1395,183 +1248,6 @@ const aiCommand: AICommand = { pendingRequests.delete(userId); } }, - - async handleModal(client: BotClient, interaction: ModalSubmitInteraction) { - try { - if (interaction.customId.startsWith('apiConfig')) { - console.log('Handling API configuration modal submission'); - try { - await interaction.reply({ - content: '✅ Provider noted. Please submit credentials next.', - flags: MessageFlags.Ephemeral, - }); - } catch (replyError) { - console.error('Failed to send apiConfig acknowledgement:', replyError); - } - } else if (interaction.customId.startsWith('apiCredentials')) { - console.log('Handling API credentials modal submission'); - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - const userId = getInvokerId(interaction); - const pendingRequest = pendingRequests.get(userId); - - if (!pendingRequest) { - console.error('No pending request found for user:', userId); - return interaction - .editReply({ - content: '❌ No pending AI request found. Please try your request again.', - }) - .catch(console.error); - } - - let apiKey = ''; - let model = ''; - let providerValue: string | undefined; - - const parts = interaction.customId.split(':'); - if (parts.length > 1) { - providerValue = parts[1]; - } - - try { - try { - apiKey = interaction.fields.getTextInputValue('apiKey')?.trim() || ''; - model = interaction.fields.getTextInputValue('model')?.trim() || ''; - } catch (_error) { - console.log('Could not get values from text inputs, trying V2 components'); - } - } catch (error) { - console.error('Error processing provider value:', error); - } - if (!providerValue) { - const submitted = parseV2ModalSubmission( - interaction as unknown as Record, - ); - providerValue = Array.isArray(submitted.provider) - ? submitted.provider[0] - : (submitted.provider as string | undefined); - } - if (!providerValue) { - const m = model.toLowerCase(); - if (m.startsWith('models/gemini') || m.startsWith('gemini')) providerValue = 'gemini'; - else if (m.startsWith('pplx') || m.includes('perplexity')) providerValue = 'perplexity'; - else if (m.startsWith('deepseek')) providerValue = 'deepseek'; - else if (m.startsWith('moonshot') || m.includes('kimi')) providerValue = 'moonshot'; - else if (m.startsWith('anthropic') || m.includes('anthropic')) { - providerValue = 'anthropic'; - } else if (m.includes('/')) providerValue = 'openrouter'; - else providerValue = 'openai'; - } - if (!apiKey || !model) { - await interaction.editReply( - 'Missing API key or model from modal submission. Please try again.', - ); - return; - } - const apiUrl = providerValue - ? PROVIDER_TO_URL[providerValue] - : 'https://openrouter.ai/api/v1'; - - let parsedUrl; - try { - parsedUrl = new URL(apiUrl); - } catch { - await interaction.editReply('API provider URL is invalid.'); - return; - } - - if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) { - await interaction.editReply('Selected provider is not allowed.'); - return; - } - - await interaction.editReply( - await client.getLocaleText('commands.ai.testing', interaction.locale), - ); - const testResult = await testApiKey(apiKey, model, apiUrl); - - if (!testResult.success) { - const errorMessage = await client.getLocaleText( - 'commands.ai.testfailed', - interaction.locale, - ); - await interaction.editReply( - errorMessage.replace('{error}', testResult.error || 'Unknown error'), - ); - return; - } - - await setUserApiKey(interaction.user.id, apiKey, model, apiUrl); - await interaction.editReply( - await client.getLocaleText('commands.ai.testsuccess', interaction.locale), - ); - - await processAIRequest(client, interaction, pendingRequest.prompt); - } - } catch (error) { - try { - await interaction.editReply({ - content: await client.getLocaleText('failedrequest', interaction.locale), - }); - } catch (editError) { - console.error('Failed to send error reply in handleModal:', editError || error); - } - } finally { - pendingRequests.delete(getInvokerId(interaction)); - try { - const clientInstance = BotClient.getInstance(); - const typedClient = clientInstance as ClientWithModalState; - if (typedClient.lastModalRawByUser) { - typedClient.lastModalRawByUser.delete(interaction.user.id); - } - } catch (error) { - logger.warn('Error cleaning up modal state:', error); - } - } - }, - handleSelect: async (client: BotClient, interaction: StringSelectMenuInteraction) => { - try { - if (interaction.customId !== 'ai_provider_select') return; - - const provider = interaction.values[0]; - const modalTitle = await client.getLocaleText('commands.ai.modal.title', interaction.locale); - - const modal = new ModalBuilder() - .setCustomId(`apiCredentials:${provider}`) - .setTitle(modalTitle); - - const apiKeyInput = new TextInputBuilder() - .setCustomId('apiKey') - .setLabel('API Key') - .setStyle(TextInputStyle.Short) - .setRequired(true) - .setPlaceholder('Paste your provider API key'); - - const modelInput = new TextInputBuilder() - .setCustomId('model') - .setLabel('Model') - .setStyle(TextInputStyle.Short) - .setRequired(true) - .setPlaceholder('e.g., gpt-4o-mini, gemini-1.5-pro'); - - modal.addComponents( - new ActionRowBuilder().addComponents(apiKeyInput), - new ActionRowBuilder().addComponents(modelInput), - ); - - await interaction.showModal(modal); - } catch (error) { - console.error('Error in handleSelect:', error); - try { - await interaction.reply({ - content: '❌ Failed to show API configuration modal. Please try again.', - flags: MessageFlags.Ephemeral, - }); - } catch (replyError) { - console.error('Failed to send error message:', replyError); - } - } - }, }; export default aiCommand as unknown as SlashCommandProps; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 4d27b95..9faf7be 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -269,7 +269,7 @@ export default class MessageCreateEvent { ); if (config.usingDefaultKey) { - const exemptUserId = process.env.AI_EXEMPT_USER_ID; + const exemptUserId = process.env.AI_EXEMPT_USER_ID?.trim(); const actorId = message.author.id; if (actorId !== exemptUserId) { diff --git a/src/events/ready.ts b/src/events/ready.ts index f97512a..716176e 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -3,7 +3,10 @@ import logger from '@/utils/logger'; import { loadActiveReminders } from '@/commands/utilities/remind'; export default class ReadyEvent { - constructor(c: BotClient) { + private startTime: number; + + constructor(c: BotClient, startTime: number = Date.now()) { + this.startTime = startTime; c.once('clientReady', () => this.readyEvent(c)); } @@ -11,8 +14,10 @@ export default class ReadyEvent { try { logger.info(`Logged in as ${client.user?.username}`); await client.application?.commands.fetch({ withLocalizations: true }); - await loadActiveReminders(client); + + const { sendDeploymentNotification } = await import('@/utils/sendDeploymentNotification.js'); + await sendDeploymentNotification(this.startTime); } catch (error) { logger.error('Error during ready event:', error); } diff --git a/src/index.ts b/src/index.ts index dfe699f..617e0a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,6 @@ import helmet from 'helmet'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; - -import './utils/djsModalPatch.js'; import BotClient from './services/Client'; import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; import rateLimit from 'express-rate-limit'; diff --git a/src/routes/apiKeys.ts b/src/routes/apiKeys.ts index 0fa963a..b5cb210 100644 --- a/src/routes/apiKeys.ts +++ b/src/routes/apiKeys.ts @@ -1,4 +1,5 @@ import { Router } from 'express'; +import axios from 'axios'; import pool from '../utils/pgClient'; import logger from '../utils/logger'; import { authenticateToken } from '../middlewares/auth'; @@ -11,9 +12,15 @@ const ALLOWED_API_HOSTS = [ 'openrouter.ai', 'generativelanguage.googleapis.com', 'api.anthropic.com', + 'api.mistral.ai', 'api.deepseek.com', - 'api.moonshot.ai', + 'api.together.xyz', 'api.perplexity.ai', + 'api.groq.com', + 'api.lepton.ai', + 'api.deepinfra.com', + 'api.moonshot.ai', + 'api.x.ai', ]; function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI { @@ -253,37 +260,121 @@ router.post( }); } - const testModel = model || 'openai/gpt-4o-mini'; - const client = getOpenAIClient(apiKey, fullApiUrl); + const isGemini = fullApiUrl.includes('generativelanguage.googleapis.com'); - try { - const response = await client.chat.completions.create({ - model: testModel, - messages: [ - { - role: 'user', - content: - 'Hello! This is a test message. Please respond with "API key test successful!"', - }, - ], - max_tokens: 50, - temperature: 0.1, - }); + if (isGemini) { + const listModelsUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`; - const testMessage = response.choices?.[0]?.message?.content || 'Test completed'; + try { + const listResponse = await axios.get(listModelsUrl, { + headers: { 'Content-Type': 'application/json' }, + timeout: 10000, + }); - logger.info(`API key test successful for user ${userId}`); - res.json({ - success: true, - message: 'API key is valid and working!', - testResponse: testMessage, - }); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); - return res.status(400).json({ - error: `API key test failed: ${errorMessage}`, - }); + interface ModelInfo { + name: string; + supportedGenerationMethods?: string[]; + [key: string]: unknown; + } + + const availableModels: ModelInfo[] = listResponse.data?.models || []; + const workingModel = availableModels.find((m) => + m.supportedGenerationMethods?.includes('generateContent'), + ); + + if (!workingModel) { + throw new Error('No models found that support generateContent'); + } + + const testPrompt = + 'Hello! This is a test message. Please respond with "API key test successful!"'; + const generateUrl = `https://generativelanguage.googleapis.com/v1beta/${workingModel.name}:generateContent?key=${apiKey}`; + + const response = await axios.post( + generateUrl, + { + contents: [ + { + role: 'user', + parts: [ + { + text: testPrompt, + }, + ], + }, + ], + }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 10000, + }, + ); + + const testMessage = + response.data?.candidates?.[0]?.content?.parts?.[0]?.text || 'Test completed'; + + logger.info( + `Gemini API key test successful for user ${userId} using model ${workingModel.name}`, + ); + return res.json({ + success: true, + message: 'Gemini API key is valid and working!', + testResponse: testMessage, + model: workingModel.name.split('/').pop(), + }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + const response = + error && typeof error === 'object' && 'response' in error + ? (error as { response?: { status?: number; data?: unknown } }).response + : undefined; + + logger.warn(`Gemini API key test failed for user ${userId}:`, { + error: errorMessage, + status: response?.status, + data: response?.data, + }); + return res.status(400).json({ + error: `Gemini API key test failed: ${errorMessage}`, + details: + response?.data && typeof response.data === 'object' && response.data !== null + ? (response.data as { error?: { details?: unknown } }).error?.details + : undefined, + }); + } + } else { + const testModel = model || 'gpt-5-nano'; + const client = getOpenAIClient(apiKey, fullApiUrl); + + try { + const response = await client.chat.completions.create({ + model: testModel, + messages: [ + { + role: 'user', + content: + 'Hello! This is a test message. Please respond with "API key test successful!"', + }, + ], + max_tokens: 50, + temperature: 0.1, + }); + + const testMessage = response.choices?.[0]?.message?.content || 'Test completed'; + + logger.info(`API key test successful for user ${userId}`); + return res.json({ + success: true, + message: 'API key is valid and working!', + testResponse: testMessage, + }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); + return res.status(400).json({ + error: `API key test failed: ${errorMessage}`, + }); + } } } catch (error) { logger.error('Error testing API key:', error); diff --git a/src/utils/djsModalPatch.ts b/src/utils/djsModalPatch.ts deleted file mode 100644 index 3b69570..0000000 --- a/src/utils/djsModalPatch.ts +++ /dev/null @@ -1,77 +0,0 @@ -import Module from 'module'; -import path from 'path'; - -export class SafeModalSubmitFields { - public components: unknown[] = []; - - public fields: { - getField: (customId: string) => { value: string }; - }; - - private fieldsMap: Record = {}; - - constructor(components: unknown) { - this.components = Array.isArray(components) ? components : []; - - try { - const rows = this.components as Array>; - for (const row of rows) { - const labeled = row?.component ? [row.component] : undefined; - const innerComponents = Array.isArray(row?.components) ? row.components : labeled || []; - - for (const comp of innerComponents) { - const leaf = comp?.component || comp; - const isTextInput = leaf?.type === 4 || typeof leaf?.style === 'number'; - if (!isTextInput) continue; - const id = leaf?.custom_id || leaf?.customId || leaf?.id; - if (!id) continue; - const value = typeof leaf?.value === 'string' ? leaf.value : ''; - this.fieldsMap[id] = value; - } - } - } catch { - // ignore - } - - this.fields = { - getField: (customId: string) => { - if (!(customId in this.fieldsMap)) { - throw new Error(`Text input with custom id '${customId}' not found`); - } - return { value: this.fieldsMap[customId] }; - }, - }; - } - - getTextInputValue(customId: string): string { - return this.fields.getField(customId).value; - } -} - -try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const anyModule = Module as any; - const originalLoad = anyModule._load; - anyModule._load = function ( - this: unknown, - request: string, - parent: unknown, - _isMain: boolean, - ...args: unknown[] - ) { - try { - const resolved = anyModule._resolveFilename(request, parent); - if (typeof resolved === 'string') { - const normalized = path.posix.normalize(resolved.replace(/\\/g, '/')); - if (normalized.includes('/structures/ModalSubmitFields.js')) { - return SafeModalSubmitFields; - } - } - } catch { - // fallthrough - } - return originalLoad.apply(this, [request, parent, _isMain, ...args] as const); - }; -} catch { - // ignore -} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 49af2a2..0c2146e 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -61,6 +61,33 @@ export const apiKeysAPI = { deleteApiKey: () => api.delete('/user/api-keys'), testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) => api.post('/user/api-keys/test', data), + getModels: async ({ apiKey, apiUrl }: { apiKey: string; apiUrl?: string }) => { + try { + let baseUrl = apiUrl || 'https://api.openai.com/v1'; + if (!baseUrl.endsWith('/v1')) { + baseUrl = baseUrl.endsWith('/') ? `${baseUrl}v1` : `${baseUrl}/v1`; + } + + const response = await axios.get(`${baseUrl}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + timeout: 10000, + }); + + interface ModelResponse { + id: string; + [key: string]: unknown; + } + + const models = (response.data?.data as ModelResponse[])?.map((model) => model.id) || []; + return { data: { models } }; + } catch (error) { + console.error('Failed to fetch models from provider API:', error); + return { data: { models: [] } }; + } + }, }; export const remindersAPI = { diff --git a/web/src/pages/ApiKeysPage.tsx b/web/src/pages/ApiKeysPage.tsx index ee26bb5..615b7e6 100644 --- a/web/src/pages/ApiKeysPage.tsx +++ b/web/src/pages/ApiKeysPage.tsx @@ -1,105 +1,148 @@ -import { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Key, Eye, EyeOff, TestTube, Save, Trash2, AlertCircle, CheckCircle } from 'lucide-react'; +import { + Key, + Eye, + EyeOff, + TestTube, + Save, + Trash2, + AlertCircle, + CheckCircle, + Loader2, +} from 'lucide-react'; import { toast } from 'sonner'; import { apiKeysAPI } from '../lib/api'; +interface ApiKeyInfo { + apiKey?: string; + model?: string; + apiUrl?: string; + hasApiKey: boolean; +} + +interface FormData { + apiKey: string; + model: string; + apiUrl: string; +} + +interface TestResult { + success: boolean; + message: string; +} + const ApiKeysPage = () => { const [showApiKey, setShowApiKey] = useState(false); - const [formData, setFormData] = useState({ + const [isLoadingModels, setIsLoadingModels] = useState(false); + const [availableModels, setAvailableModels] = useState([]); + const [showModelDropdown, setShowModelDropdown] = useState(false); + const [modelSearch, setModelSearch] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const [hasPassedTest, setHasPassedTest] = useState(false); + const [testResult, setTestResult] = useState(null); + const [formData, setFormData] = useState({ apiKey: '', model: '', - apiUrl: '', + apiUrl: 'https://api.openai.com/v1', }); - const [isEditing, setIsEditing] = useState(false); - const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); - const [hasPassedTest, setHasPassedTest] = useState(false); - const queryClient = useQueryClient(); - const { data: apiKeyInfo, isLoading } = useQuery({ - queryKey: ['api-keys'], - queryFn: () => apiKeysAPI.getApiKeys().then((res) => res.data), - }); + const filteredModels = React.useMemo(() => { + if (!modelSearch) return availableModels; + const searchTerm = modelSearch.toLowerCase(); + return availableModels.filter((model) => model.toLowerCase().includes(searchTerm)); + }, [availableModels, modelSearch]); - const updateApiKeyMutation = useMutation({ - mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => - apiKeysAPI.updateApiKey(data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['api-keys'] }); - setIsEditing(false); - setFormData({ apiKey: '', model: '', apiUrl: '' }); - toast.success('API key updated successfully!'); - }, - onError: () => { - toast.error('Failed to update API key'); - }, - }); + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; - const deleteApiKeyMutation = useMutation({ - mutationFn: () => apiKeysAPI.deleteApiKey(), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['api-keys'] }); - setFormData({ apiKey: '', model: '', apiUrl: '' }); - setIsEditing(false); - toast.success('API key deleted successfully!'); - }, - onError: () => { - toast.error('Failed to delete API key'); - }, - }); + const handleModelSelect = (model: string) => { + setFormData((prev) => ({ + ...prev, + model, + })); + setModelSearch(''); + setShowModelDropdown(false); + }; - const testApiKeyMutation = useMutation({ - mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => - apiKeysAPI.testApiKey(data), - onSuccess: () => { - setTestResult({ success: true, message: 'API key is valid and working!' }); - setHasPassedTest(true); - toast.success('API key test successful!'); - }, - onError: (error: { response?: { data?: { error?: string } } }) => { - const message = error.response?.data?.error || 'API key test failed'; - setTestResult({ success: false, message }); - setHasPassedTest(false); - toast.error(message); - }, - }); + const handleModelInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setFormData((prev) => ({ ...prev, model: value })); + setModelSearch(value); + setHasPassedTest(false); + setTestResult(null); + setShowModelDropdown(true); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!formData.apiKey.trim()) { - toast.error('API key is required'); - return; + if (availableModels.length === 0 && formData.apiKey) { + fetchModels(); } - if (!hasPassedTest) { - toast.error('Please test the API key before saving'); - return; + }; + + const fetchModels = async () => { + if (!formData.apiKey) return; + + setIsLoadingModels(true); + try { + const response = await apiKeysAPI.getModels({ + apiKey: formData.apiKey, + apiUrl: formData.apiUrl, + }); + + if (response.data?.models?.length > 0) { + setAvailableModels(response.data.models); + } else { + setAvailableModels([]); + } + } catch (error) { + console.error('Error fetching models:', error); + setAvailableModels([]); + } finally { + setIsLoadingModels(false); } - updateApiKeyMutation.mutate({ - apiKey: formData.apiKey, - model: formData.model || undefined, - apiUrl: formData.apiUrl || undefined, - }); }; - const handleTest = () => { - if (!formData.apiKey.trim()) { - toast.error('API key is required for testing'); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (hasPassedTest) { + updateApiKeyMutation.mutate({ + apiKey: formData.apiKey, + model: formData.model, + apiUrl: formData.apiUrl, + }); return; } - setTestResult(null); - testApiKeyMutation.mutate({ - apiKey: formData.apiKey, - model: formData.model || undefined, - apiUrl: formData.apiUrl || undefined, - }); + + try { + const result = await testApiKeyMutation.mutateAsync({ + apiKey: formData.apiKey, + model: formData.model, + apiUrl: formData.apiUrl, + }); + + if (result.data?.success) { + updateApiKeyMutation.mutate({ + apiKey: formData.apiKey, + model: formData.model, + apiUrl: formData.apiUrl, + }); + } + } catch (_error) { + // ignore + } }; const handleEdit = () => { setIsEditing(true); setFormData({ - apiKey: '', + apiKey: apiKeyInfo?.apiKey || '', model: apiKeyInfo?.model || '', - apiUrl: apiKeyInfo?.apiUrl || '', + apiUrl: apiKeyInfo?.apiUrl || 'https://api.openai.com/v1', }); setTestResult(null); setHasPassedTest(false); @@ -107,11 +150,118 @@ const ApiKeysPage = () => { const handleCancel = () => { setIsEditing(false); - setFormData({ apiKey: '', model: '', apiUrl: '' }); + setFormData({ + apiKey: apiKeyInfo?.apiKey || '', + model: apiKeyInfo?.model || '', + apiUrl: apiKeyInfo?.apiUrl || 'https://api.openai.com/v1', + }); setTestResult(null); setHasPassedTest(false); }; + const queryClient = useQueryClient(); + + const { data: apiKeyInfo, isLoading } = useQuery({ + queryKey: ['api-keys'], + queryFn: () => apiKeysAPI.getApiKeys().then((res) => res.data as ApiKeyInfo), + }); + + useEffect(() => { + if (apiKeyInfo) { + setFormData({ + apiKey: apiKeyInfo.apiKey || '', + model: apiKeyInfo.model || '', + apiUrl: apiKeyInfo.apiUrl || 'https://api.openai.com/v1', + }); + } + }, [apiKeyInfo]); + + const updateApiKeyMutation = useMutation({ + mutationFn: (data: { apiKey?: string; model?: string; apiUrl?: string }) => + apiKeysAPI.updateApiKey(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['api-keys'] }); + toast.success('API key updated successfully'); + setIsEditing(false); + }, + onError: (error: unknown) => { + const errorMessage = + error && typeof error === 'object' && 'response' in error + ? (error as { response?: { data?: { error?: string } } })?.response?.data?.error + : 'Failed to update API key'; + toast.error(errorMessage); + }, + }); + const testApiKeyMutation = useMutation({ + mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => + apiKeysAPI.testApiKey(data), + onSuccess: async (_, variables) => { + setHasPassedTest(true); + setTestResult({ + success: true, + message: 'API key test successful!', + }); + toast.success('API key test successful!'); + + if (variables.apiKey) { + try { + setIsLoadingModels(true); + const modelsResponse = await apiKeysAPI.getModels({ + apiKey: variables.apiKey, + apiUrl: variables.apiUrl, + }); + if (modelsResponse.data?.models?.length > 0) { + setAvailableModels(modelsResponse.data.models); + } else { + setAvailableModels([]); + } + } catch (error) { + console.error('Error fetching models:', error); + setAvailableModels([]); + } finally { + setIsLoadingModels(false); + } + } + }, + onError: (error: unknown) => { + const errorMessage = + error && typeof error === 'object' && 'response' in error + ? (error as { response?: { data?: { error?: string | { message: string } } } })?.response + ?.data?.error + : undefined; + const message = + typeof errorMessage === 'object' + ? errorMessage?.message + : errorMessage || 'API key test failed'; + setHasPassedTest(false); + setTestResult({ success: false, message }); + toast.error(message); + }, + }); + + const deleteApiKeyMutation = useMutation({ + mutationFn: () => apiKeysAPI.deleteApiKey(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['api-keys'] }); + toast.success('API key deleted successfully'); + setIsEditing(false); + setHasPassedTest(false); + setTestResult(null); + setFormData({ + apiKey: '', + model: '', + apiUrl: 'https://api.openai.com/v1', + }); + }, + onError: (error: unknown) => { + const errorMessage = + error && typeof error === 'object' && 'response' in error + ? (error as { response?: { data?: { error?: string } } })?.response?.data?.error + : 'Failed to delete API key'; + toast.error(errorMessage); + }, + }); + if (isLoading) { return (
@@ -216,75 +366,136 @@ const ApiKeysPage = () => { * You must test the API key before saving to ensure it works correctly.

- -
- + +
-
-
- - { - setFormData({ ...formData, model: e.target.value }); - setHasPassedTest(false); - setTestResult(null); - }} - placeholder="e.g., openai/gpt-4o-mini, anthropic/claude-4-sonnet" - className="input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400" - /> -

- Leave empty to use the default model -

+
+ +
+ { + handleInputChange(e); + setHasPassedTest(false); + setTestResult(null); + }} + placeholder="Enter your API key" + className="input pr-10 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400 w-full" + required + /> + +
+
-
+
- { - setFormData({ ...formData, apiUrl: e.target.value }); - setHasPassedTest(false); - setTestResult(null); - }} - placeholder="https://openrouter.ai/api/v1" - className="input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400" - /> -

- Enter the base API URL (e.g., https://openrouter.ai/api/v1, - https://api.openai.com/v1) -

+
+ setShowModelDropdown(true)} + onChange={handleModelInputChange} + onBlur={() => { + setTimeout(() => setShowModelDropdown(false), 200); + }} + placeholder="Select or type a model name" + className="model-input input dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:placeholder-gray-400 w-full pr-8" + /> + {isLoadingModels && ( +
+ +
+ )} + {showModelDropdown && (availableModels.length > 0 || modelSearch) && ( +
+ {filteredModels.length > 0 ? ( + filteredModels.map((model: string) => ( +
{ + e.preventDefault(); + handleModelSelect(model); + }} + > + {model} +
+ )) + ) : ( +
+ No matching models found +
+ )} +
+ )} +

+ {availableModels.length > 0 + ? `Found ${availableModels.length} models` + : 'Type to search or leave empty for default'} +

+
{testResult && ( @@ -304,43 +515,48 @@ const ApiKeysPage = () => {
)} -
+
+ - -
- - -
diff --git a/web/src/pages/PrivacyPage.tsx b/web/src/pages/PrivacyPage.tsx index 385d042..b3010c9 100644 --- a/web/src/pages/PrivacyPage.tsx +++ b/web/src/pages/PrivacyPage.tsx @@ -178,7 +178,7 @@ export default function PrivacyPolicy() {

We implement the following security measures to protect your information:

-
    +
    • API Key Encryption: AES-256-GCM encryption for all stored API keys
    • diff --git a/web/src/pages/TermsPage.tsx b/web/src/pages/TermsPage.tsx index 3eadf1e..d800555 100644 --- a/web/src/pages/TermsPage.tsx +++ b/web/src/pages/TermsPage.tsx @@ -4,7 +4,7 @@ export default function TermsOfService() { return (
      @@ -63,9 +63,10 @@ export default function TermsOfService() {
      • You are responsible for the security of your API keys
      • - We do not store your API keys permanently - they are only kept in memory during your - active session + Your API keys are stored securely using industry-standard encryption and are only + accessible to you
      • +
      • You can delete your API keys at any time through the API keys management page
      • You must comply with the terms of service of any third-party APIs you use with the Bot