diff --git a/.env.example b/.env.example index b724838845..5696d7032a 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,12 @@ FIREWORKS_API_KEY=your_fireworks_api_key_here # Get your API key from: https://platform.openai.com/api-keys OPENAI_API_KEY=your_openai_api_key_here +# Azure OpenAI +# Azure resource endpoint example: https://your-resource.openai.azure.com +AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here +AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com +AZURE_OPENAI_API_VERSION=2024-02-15-preview + # GitHub Models (OpenAI models hosted by GitHub) # Get your Personal Access Token from: https://github.com/settings/tokens # - Select "Fine-grained tokens" @@ -43,6 +49,12 @@ DEEPSEEK_API_KEY=your_deepseek_api_key_here # Get your API key from: https://makersuite.google.com/app/apikey GOOGLE_GENERATIVE_AI_API_KEY=your_google_gemini_api_key_here +# Vertex AI (Gemini via Vertex) +# Base URL format: https://REGION-aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/REGION/publishers/google +# Access token example: gcloud auth print-access-token +VERTEX_AI_ACCESS_TOKEN=your_vertex_access_token_here +VERTEX_AI_BASE_URL=your_vertex_ai_base_url_here + # Cohere # Get your API key from: https://dashboard.cohere.ai/api-keys COHERE_API_KEY=your_cohere_api_key_here diff --git a/app/components/@settings/tabs/features/FeaturesTab.tsx b/app/components/@settings/tabs/features/FeaturesTab.tsx index 3b14a7565d..7c06bdbcf5 100644 --- a/app/components/@settings/tabs/features/FeaturesTab.tsx +++ b/app/components/@settings/tabs/features/FeaturesTab.tsx @@ -6,6 +6,8 @@ import { useSettings } from '~/lib/hooks/useSettings'; import { classNames } from '~/utils/classNames'; import { toast } from 'react-toastify'; import { PromptLibrary } from '~/lib/common/prompt-library'; +import { isMac } from '~/utils/os'; +import type { Shortcut, Shortcuts } from '~/lib/stores/settings'; interface FeatureToggle { id: string; @@ -111,14 +113,69 @@ export default function FeaturesTab() { isLatestBranch, contextOptimizationEnabled, eventLogs, + autoPromptEnhancement, + confirmFileWrites, + performanceMode, + agentMode, + frameworkLock, + shortcuts, setAutoSelectTemplate, enableLatestBranch, enableContextOptimization, setEventLogs, + setAutoPromptEnhancement, + setConfirmFileWrites, + setPerformanceMode, + setAgentMode, + setFrameworkLock, + updateShortcutBinding, + resetShortcuts, setPromptId, promptId, } = useSettings(); + const [recordingShortcut, setRecordingShortcut] = React.useState(null); + + const formatShortcutKey = (key: string) => { + if (key === ' ') { + return 'Space'; + } + + if (key === 'Escape') { + return 'Esc'; + } + + return key.length === 1 ? key.toUpperCase() : key; + }; + + const formatShortcut = (shortcut: Shortcut) => { + const parts: string[] = []; + + if (shortcut.ctrlOrMetaKey) { + parts.push(isMac ? 'Cmd' : 'Ctrl'); + } else { + if (shortcut.ctrlKey) { + parts.push('Ctrl'); + } + + if (shortcut.metaKey) { + parts.push('Cmd'); + } + } + + if (shortcut.altKey) { + parts.push(isMac ? 'Option' : 'Alt'); + } + + if (shortcut.shiftKey) { + parts.push('Shift'); + } + + parts.push(formatShortcutKey(shortcut.key)); + + return parts.join(' + '); + }; + // Enable features by default on first load React.useEffect(() => { // Only set defaults if values are undefined @@ -169,12 +226,47 @@ export default function FeaturesTab() { toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`); break; } + case 'autoPromptEnhancement': { + setAutoPromptEnhancement(enabled); + toast.success(`Auto prompt enhancement ${enabled ? 'enabled' : 'disabled'}`); + break; + } + case 'confirmFileWrites': { + setConfirmFileWrites(enabled); + toast.success(`Confirm file writes ${enabled ? 'enabled' : 'disabled'}`); + break; + } + case 'performanceMode': { + setPerformanceMode(enabled); + toast.success(`Performance mode ${enabled ? 'enabled' : 'disabled'}`); + break; + } + case 'agentMode': { + setAgentMode(enabled); + toast.success(`Agent mode ${enabled ? 'enabled' : 'disabled'}`); + break; + } + case 'frameworkLock': { + setFrameworkLock(enabled); + toast.success(`Framework lock ${enabled ? 'enabled' : 'disabled'}`); + break; + } default: break; } }, - [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs], + [ + enableLatestBranch, + setAutoSelectTemplate, + enableContextOptimization, + setEventLogs, + setAutoPromptEnhancement, + setConfirmFileWrites, + setPerformanceMode, + setAgentMode, + setFrameworkLock, + ], ); const features = { @@ -211,10 +303,94 @@ export default function FeaturesTab() { enabled: eventLogs, tooltip: 'Enabled by default to record detailed logs of system events and user actions', }, + { + id: 'autoPromptEnhancement', + title: 'Auto Prompt Enhancement', + description: 'Automatically enhance prompts before sending', + icon: 'i-bolt:stars', + enabled: autoPromptEnhancement, + beta: true, + tooltip: 'Adds an extra LLM step to refine prompts for clarity and completeness', + }, + { + id: 'confirmFileWrites', + title: 'Confirm File Changes', + description: 'Require approval before applying file edits', + icon: 'i-ph:git-diff', + enabled: confirmFileWrites, + beta: true, + tooltip: 'Stages AI file changes for review, similar to git confirmations', + }, + { + id: 'performanceMode', + title: 'Performance Mode', + description: 'Reduce visual effects for better performance', + icon: 'i-ph:speedometer', + enabled: performanceMode, + tooltip: 'Disables heavy visuals and blur effects to reduce CPU/GPU usage', + }, + { + id: 'agentMode', + title: 'Agent Mode', + description: 'Run a planning step before responses', + icon: 'i-ph:robot', + enabled: agentMode, + experimental: true, + tooltip: 'Adds a backend planning step to improve multi-step task execution', + }, + { + id: 'frameworkLock', + title: 'Framework Lock', + description: 'Keep the AI aligned with the detected project framework', + icon: 'i-ph:lock', + enabled: frameworkLock, + tooltip: 'Injects a framework hint so the assistant does not drift to other stacks', + }, ], beta: [], }; + const shortcutEntries: Array<{ id: keyof Shortcuts; title: string; description: string }> = [ + { + id: 'toggleTheme', + title: 'Toggle Theme', + description: 'Switch between light and dark themes', + }, + { + id: 'toggleTerminal', + title: 'Toggle Terminal', + description: 'Show or hide the terminal panel', + }, + ]; + + React.useEffect(() => { + if (!recordingShortcut) { + return undefined; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (['Shift', 'Control', 'Alt', 'Meta'].includes(event.key)) { + return; + } + + event.preventDefault(); + updateShortcutBinding(recordingShortcut, { + key: event.key, + ctrlKey: event.ctrlKey || undefined, + metaKey: event.metaKey || undefined, + altKey: event.altKey || undefined, + shiftKey: event.shiftKey || undefined, + ctrlOrMetaKey: undefined, + }); + setRecordingShortcut(null); + toast.success('Shortcut updated'); + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => window.removeEventListener('keydown', handleKeyDown); + }, [recordingShortcut, updateShortcutBinding]); + return (
+ + +
+
+
+
+
+

+ Keyboard Shortcuts +

+

+ Record custom shortcuts for common actions +

+
+ +
+ +
+ {shortcutEntries.map((entry) => { + const shortcut = shortcuts[entry.id]; + const isRecording = recordingShortcut === entry.id; + + return ( +
+
+
+
{entry.title}
+
{entry.description}
+
+ +
+
+ Current: {formatShortcut(shortcut)} +
+
+ ); + })} +
+
); } diff --git a/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx b/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx index 7311851426..19dfa2d1e2 100644 --- a/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx +++ b/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx @@ -19,6 +19,7 @@ import type { IconType } from 'react-icons'; type ProviderName = | 'AmazonBedrock' | 'Anthropic' + | 'AzureOpenAI' | 'Cohere' | 'Deepseek' | 'Github' @@ -31,12 +32,14 @@ type ProviderName = | 'OpenRouter' | 'Perplexity' | 'Together' + | 'VertexAI' | 'XAI'; // Update the PROVIDER_ICONS type to use the ProviderName type const PROVIDER_ICONS: Record = { AmazonBedrock: SiAmazon, Anthropic: FaBrain, + AzureOpenAI: FaCloud, Cohere: BiChip, Deepseek: BiCodeBlock, Github: SiGithub, @@ -49,6 +52,7 @@ const PROVIDER_ICONS: Record = { OpenRouter: FaCloud, Perplexity: SiPerplexity, Together: BsCloud, + VertexAI: SiGoogle, XAI: BsRobot, }; @@ -57,6 +61,8 @@ const PROVIDER_DESCRIPTIONS: Partial> = { Anthropic: 'Access Claude and other Anthropic models', Github: 'Use OpenAI models hosted through GitHub infrastructure', OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models', + AzureOpenAI: 'Use Azure OpenAI deployments from your Azure resource', + VertexAI: 'Use Gemini models via Google Vertex AI', }; const CloudProvidersTab = () => { diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 934a3d5545..035aab5574 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -33,6 +33,7 @@ import { ChatBox } from './ChatBox'; import type { DesignScheme } from '~/types/design-scheme'; import type { ElementInfo } from '~/components/workbench/Inspector'; import LlmErrorAlert from './LLMApiAlert'; +import { PendingFileActions } from './PendingFileActions'; const TEXTAREA_MIN_HEIGHT = 76; @@ -49,6 +50,15 @@ interface BaseChatProps { enhancingPrompt?: boolean; promptEnhanced?: boolean; input?: string; + autoPromptEnhancement?: boolean; + setAutoPromptEnhancement?: (enabled: boolean) => void; + agentMode?: boolean; + setAgentMode?: (enabled: boolean) => void; + performanceMode?: boolean; + setPerformanceMode?: (enabled: boolean) => void; + isAutoEnhancing?: boolean; + confirmFileWrites?: boolean; + setConfirmFileWrites?: (enabled: boolean) => void; model?: string; setModel?: (model: string) => void; provider?: ProviderInfo; @@ -98,6 +108,15 @@ export const BaseChat = React.forwardRef( setProvider, providerList, input = '', + autoPromptEnhancement, + setAutoPromptEnhancement, + agentMode, + setAgentMode, + performanceMode, + setPerformanceMode, + isAutoEnhancing, + confirmFileWrites, + setConfirmFileWrites, enhancingPrompt, handleInputChange, @@ -372,7 +391,7 @@ export const BaseChat = React.forwardRef( {() => { return chatStarted ? ( (
{deployAlert && ( @@ -426,6 +448,7 @@ export const BaseChat = React.forwardRef( {llmErrorAlert && clearLlmErrorAlert?.()} />}
{progressAnnotations && } + ( handleStop={handleStop} handleSendMessage={handleSendMessage} enhancingPrompt={enhancingPrompt} + autoPromptEnhancement={autoPromptEnhancement} + setAutoPromptEnhancement={setAutoPromptEnhancement} + agentMode={agentMode} + setAgentMode={setAgentMode} + performanceMode={performanceMode} + setPerformanceMode={setPerformanceMode} + isAutoEnhancing={isAutoEnhancing} + confirmFileWrites={confirmFileWrites} + setConfirmFileWrites={setConfirmFileWrites} enhancePrompt={enhancePrompt} isListening={isListening} startListening={startListening} diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index ccddaf51d6..bfb463823d 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import type { Message } from 'ai'; import { useChat } from '@ai-sdk/react'; import { useAnimate } from 'framer-motion'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { useMessageParser, usePromptEnhancer, useShortcuts } from '~/lib/hooks'; import { description, useChatHistory } from '~/lib/persistence'; @@ -10,6 +10,7 @@ import { chatStore } from '~/lib/stores/chat'; import { workbenchStore } from '~/lib/stores/workbench'; import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants'; import { cubicEasingFn } from '~/utils/easings'; +import { detectFrameworkFromFiles } from '~/utils/framework'; import { createScopedLogger, renderLogger } from '~/utils/logger'; import { BaseChat } from './BaseChat'; import Cookies from 'js-cookie'; @@ -100,8 +101,36 @@ export const ChatImpl = memo( (project) => project.id === supabaseConn.selectedProjectId, ); const supabaseAlert = useStore(workbenchStore.supabaseAlert); - const { activeProviders, promptId, autoSelectTemplate, contextOptimizationEnabled } = useSettings(); + const { + activeProviders, + promptId, + autoSelectTemplate, + contextOptimizationEnabled, + autoPromptEnhancement, + agentMode, + performanceMode, + confirmFileWrites, + frameworkLock, + setAutoPromptEnhancement, + setAgentMode, + setPerformanceMode, + setConfirmFileWrites, + } = useSettings(); + const frameworkHint = useMemo(() => { + if (!frameworkLock) { + return undefined; + } + + const detected = detectFrameworkFromFiles(files); + + if (!detected) { + return undefined; + } + + return `Detected framework: ${detected}. Stay within this stack unless the user explicitly asks to change frameworks.`; + }, [files, frameworkLock]); const [llmErrorAlert, setLlmErrorAlert] = useState(undefined); + const [isAutoEnhancing, setIsAutoEnhancing] = useState(false); const [model, setModel] = useState(() => { const savedModel = Cookies.get('selectedModel'); return savedModel || DEFAULT_MODEL; @@ -140,6 +169,8 @@ export const ChatImpl = memo( contextOptimization: contextOptimizationEnabled, chatMode, designScheme, + agentMode, + frameworkHint, supabase: { isConnected: supabaseConn.isConnected, hasSelectedProject: !!selectedProject, @@ -386,6 +417,61 @@ export const ChatImpl = memo( return attachments; }; + const enhancePromptForSend = async (rawPrompt: string): Promise => { + if (!rawPrompt?.trim()) { + return rawPrompt; + } + + setIsAutoEnhancing(true); + + try { + const response = await fetch('/api/enhancer', { + method: 'POST', + body: JSON.stringify({ + message: rawPrompt, + model, + provider, + apiKeys, + }), + }); + + if (!response.ok || !response.body) { + throw new Error(`Enhancer failed: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let enhanced = ''; + + while (true) { + const { value, done } = await reader.read(); + + if (done) { + break; + } + + enhanced += decoder.decode(value); + } + + const trimmed = enhanced.trim(); + + if (trimmed && trimmed !== rawPrompt.trim()) { + toast.success('Prompt enhanced'); + + return trimmed; + } + + return rawPrompt; + } catch (error) { + console.error('Auto prompt enhancement failed:', error); + toast.error('Auto prompt enhancement failed'); + + return rawPrompt; + } finally { + setIsAutoEnhancing(false); + } + }; + const sendMessage = async (_event: React.UIEvent, messageInput?: string) => { const messageContent = messageInput || input; @@ -400,11 +486,15 @@ export const ChatImpl = memo( let finalMessageContent = messageContent; + if (autoPromptEnhancement && !messageInput) { + finalMessageContent = await enhancePromptForSend(finalMessageContent); + } + if (selectedElement) { console.log('Selected Element:', selectedElement); const elementInfo = `
${JSON.stringify(`${selectedElement.displayText}`)}
`; - finalMessageContent = messageContent + elementInfo; + finalMessageContent = finalMessageContent + elementInfo; } runAnimation(); @@ -657,6 +747,15 @@ export const ChatImpl = memo( apiKeys, ); }} + autoPromptEnhancement={autoPromptEnhancement} + setAutoPromptEnhancement={setAutoPromptEnhancement} + agentMode={agentMode} + setAgentMode={setAgentMode} + performanceMode={performanceMode} + setPerformanceMode={setPerformanceMode} + isAutoEnhancing={isAutoEnhancing} + confirmFileWrites={confirmFileWrites} + setConfirmFileWrites={setConfirmFileWrites} uploadedFiles={uploadedFiles} setUploadedFiles={setUploadedFiles} imageDataList={imageDataList} diff --git a/app/components/chat/ChatBox.tsx b/app/components/chat/ChatBox.tsx index 209a24c949..7610419126 100644 --- a/app/components/chat/ChatBox.tsx +++ b/app/components/chat/ChatBox.tsx @@ -2,6 +2,9 @@ import React from 'react'; import { ClientOnly } from 'remix-utils/client-only'; import { classNames } from '~/utils/classNames'; import { PROVIDER_LIST } from '~/utils/constants'; +import { useStore } from '@nanostores/react'; +import { chatStore } from '~/lib/stores/chat'; +import { workbenchStore } from '~/lib/stores/workbench'; import { ModelSelector } from '~/components/chat/ModelSelector'; import { APIKeyManager } from './APIKeyManager'; import { LOCAL_PROVIDERS } from '~/lib/stores/settings'; @@ -56,7 +59,19 @@ interface ChatBoxProps { handleStop?: (() => void) | undefined; enhancingPrompt?: boolean | undefined; enhancePrompt?: (() => void) | undefined; + + autoPromptEnhancement?: boolean; + setAutoPromptEnhancement?: ((enabled: boolean) => void) | undefined; + agentMode?: boolean; + setAgentMode?: ((enabled: boolean) => void) | undefined; + performanceMode?: boolean; + setPerformanceMode?: ((enabled: boolean) => void) | undefined; + isAutoEnhancing?: boolean; + confirmFileWrites?: boolean; + setConfirmFileWrites?: ((enabled: boolean) => void) | undefined; + onWebSearchResult?: (result: string) => void; + chatMode?: 'discuss' | 'build'; setChatMode?: (mode: 'discuss' | 'build') => void; designScheme?: DesignScheme; @@ -66,10 +81,22 @@ interface ChatBoxProps { } export const ChatBox: React.FC = (props) => { + const showEffects = !props.performanceMode; + const showWorkbench = useStore(workbenchStore.showWorkbench); + + const toggleMobileView = () => { + const nextShowWorkbench = !showWorkbench; + workbenchStore.showWorkbench.set(nextShowWorkbench); + chatStore.setKey('showChat', !nextShowWorkbench); + }; + return (
= (props) => { */ )} > - - - - - - - - - - - - - - - - - - + {showEffects && ( + + + + + + + + + + + + + + + + + + + )}
{() => ( @@ -169,7 +198,9 @@ export const ChatBox: React.FC = (props) => {
)}