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
21 changes: 20 additions & 1 deletion app/components/@settings/tabs/features/FeaturesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,14 @@ export default function FeaturesTab() {
isLatestBranch,
contextOptimizationEnabled,
eventLogs,
autoPromptOptimization,
setAutoSelectTemplate,
enableLatestBranch,
enableContextOptimization,
setEventLogs,
setPromptId,
promptId,
setAutoPromptOptimization,
} = useSettings();

// Enable features by default on first load
Expand All @@ -141,6 +143,10 @@ export default function FeaturesTab() {
if (eventLogs === undefined) {
setEventLogs(true); // Default: ON - Enable event logging
}

if (autoPromptOptimization === undefined) {
setAutoPromptOptimization(true); // Default: ON - Optimize prompts for smaller models
}
}, []); // Only run once on component mount

const handleToggleFeature = useCallback(
Expand Down Expand Up @@ -169,12 +175,17 @@ export default function FeaturesTab() {
toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
break;
}
case 'autoPromptOptimization': {
setAutoPromptOptimization(enabled);
toast.success(`Auto prompt optimization ${enabled ? 'enabled' : 'disabled'}`);
break;
}

default:
break;
}
},
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs, setAutoPromptOptimization],
);

const features = {
Expand Down Expand Up @@ -211,6 +222,14 @@ export default function FeaturesTab() {
enabled: eventLogs,
tooltip: 'Enabled by default to record detailed logs of system events and user actions',
},
{
id: 'autoPromptOptimization',
title: 'Auto Prompt Optimization',
description: 'Use optimized prompts for smaller models automatically',
icon: 'i-ph:magic-wand',
enabled: autoPromptOptimization,
tooltip: 'Enabled by default to keep prompts concise for smaller context windows',
},
],
beta: [],
};
Expand Down
101 changes: 78 additions & 23 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ import type { ProgressAnnotation } from '~/types/context';
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
import { toast } from 'react-toastify';
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
import { ChatBox } from './ChatBox';
import type { DesignScheme } from '~/types/design-scheme';
import type { ElementInfo } from '~/components/workbench/Inspector';
import LlmErrorAlert from './LLMApiAlert';
import {
ACCEPTED_ATTACHMENT_TYPES,
MAX_ATTACHMENT_BYTES,
isImageFile,
isSupportedAttachment,
readFileAsDataUrl,
} from './uploadUtils';

const TEXTAREA_MIN_HEIGHT = 76;

Expand All @@ -49,6 +57,10 @@ interface BaseChatProps {
enhancingPrompt?: boolean;
promptEnhanced?: boolean;
input?: string;
projectMemory?: string;
setProjectMemory?: (value: string) => void;
planMode?: boolean;
setPlanMode?: (enabled: boolean) => void;
model?: string;
setModel?: (model: string) => void;
provider?: ProviderInfo;
Expand Down Expand Up @@ -97,6 +109,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setProvider,
providerList,
input = '',
projectMemory,
setProjectMemory,
planMode,
setPlanMode,
enhancingPrompt,
handleInputChange,

Expand Down Expand Up @@ -287,24 +303,51 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}
};

const appendUploads = (items: Array<{ file: File; imageData?: string }>) => {
if (!setUploadedFiles || !setImageDataList || items.length === 0) {
return;
}

setUploadedFiles([...uploadedFiles, ...items.map((item) => item.file)]);
setImageDataList([...imageDataList, ...items.map((item) => item.imageData || '')]);
};

const handleFileUpload = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.accept = ACCEPTED_ATTACHMENT_TYPES;
input.multiple = true;

input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
const files = Array.from((e.target as HTMLInputElement).files || []);

if (file) {
const reader = new FileReader();
if (!files.length) {
return;
}

const uploads: Array<{ file: File; imageData?: string }> = [];

for (const file of files) {
if (file.size > MAX_ATTACHMENT_BYTES) {
toast.error(`"${file.name}" exceeds the ${Math.round(MAX_ATTACHMENT_BYTES / 1024 / 1024)}MB limit`);
continue;
}

if (!isSupportedAttachment(file)) {
toast.error(`"${file.name}" is not a supported attachment type`);
continue;
}

reader.onload = (e) => {
const base64Image = e.target?.result as string;
setUploadedFiles?.([...uploadedFiles, file]);
setImageDataList?.([...imageDataList, base64Image]);
};
reader.readAsDataURL(file);
try {
const dataUrl = await readFileAsDataUrl(file);
uploads.push({ file, imageData: isImageFile(file) ? dataUrl : '' });
} catch (error) {
console.error('Failed to read file', error);
toast.error(`Failed to read "${file.name}"`);
}
}

appendUploads(uploads);
};

input.click();
Expand All @@ -318,24 +361,32 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}

for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
if (!item.type.startsWith('image/')) {
continue;
}

const file = item.getAsFile();
e.preventDefault();

if (file) {
const reader = new FileReader();
const file = item.getAsFile();

reader.onload = (e) => {
const base64Image = e.target?.result as string;
setUploadedFiles?.([...uploadedFiles, file]);
setImageDataList?.([...imageDataList, base64Image]);
};
reader.readAsDataURL(file);
}
if (!file) {
continue;
}

break;
if (file.size > MAX_ATTACHMENT_BYTES) {
toast.error(`"${file.name}" exceeds the ${Math.round(MAX_ATTACHMENT_BYTES / 1024 / 1024)}MB limit`);
continue;
}

try {
const base64Image = await readFileAsDataUrl(file);
appendUploads([{ file, imageData: base64Image }]);
} catch (error) {
console.error('Failed to read pasted image', error);
toast.error(`Failed to read "${file.name}"`);
}

break;
}
};

Expand Down Expand Up @@ -442,6 +493,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setImageDataList={setImageDataList}
textareaRef={textareaRef}
input={input}
projectMemory={projectMemory}
setProjectMemory={setProjectMemory}
planMode={planMode}
setPlanMode={setPlanMode}
handleInputChange={handleInputChange}
handlePaste={handlePaste}
TEXTAREA_MIN_HEIGHT={TEXTAREA_MIN_HEIGHT}
Expand Down
38 changes: 36 additions & 2 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useShortcuts } from '~/lib/hooks';
import { description, useChatHistory } from '~/lib/persistence';
import { chatId } from '~/lib/persistence/useChatHistory';
import { getProjectSettings, setProjectSettings } from '~/lib/persistence/projectSettings';
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';
Expand Down Expand Up @@ -100,7 +102,11 @@ 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, autoPromptOptimization } =
useSettings();
const currentChatId = useStore(chatId);
const [projectMemory, setProjectMemory] = useState('');
const [planMode, setPlanMode] = useState(false);
const [llmErrorAlert, setLlmErrorAlert] = useState<LlmErrorAlertType | undefined>(undefined);
const [model, setModel] = useState(() => {
const savedModel = Cookies.get('selectedModel');
Expand All @@ -117,6 +123,27 @@ export const ChatImpl = memo(
const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(null);
const mcpSettings = useMCPStore((state) => state.settings);

useEffect(() => {
if (!currentChatId) {
setProjectMemory('');
setPlanMode(false);

return;
}

const settings = getProjectSettings(currentChatId);
setProjectMemory(settings.memory);
setPlanMode(settings.planMode);
}, [currentChatId]);

useEffect(() => {
if (!currentChatId) {
return;
}

setProjectSettings(currentChatId, { memory: projectMemory, planMode });
}, [currentChatId, projectMemory, planMode]);

const {
messages,
isLoading,
Expand All @@ -140,6 +167,9 @@ export const ChatImpl = memo(
contextOptimization: contextOptimizationEnabled,
chatMode,
designScheme,
projectMemory,
planMode,
autoPromptOptimization,
supabase: {
isConnected: supabaseConn.isConnected,
hasSelectedProject: !!selectedProject,
Expand Down Expand Up @@ -344,7 +374,7 @@ export const ChatImpl = memo(
];

// Add image parts if any
images.forEach((imageData) => {
images.filter(Boolean).forEach((imageData) => {
// Extract correct MIME type from the data URL
const mimeType = imageData.split(';')[0].split(':')[1] || 'image/jpeg';

Expand Down Expand Up @@ -643,6 +673,10 @@ export const ChatImpl = memo(
apiKeys,
);
}}
projectMemory={projectMemory}
setProjectMemory={setProjectMemory}
planMode={planMode}
setPlanMode={setPlanMode}
uploadedFiles={uploadedFiles}
setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList}
Expand Down
64 changes: 56 additions & 8 deletions app/components/chat/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { ColorSchemeDialog } from '~/components/ui/ColorSchemeDialog';
import type { DesignScheme } from '~/types/design-scheme';
import type { ElementInfo } from '~/components/workbench/Inspector';
import { McpTools } from './MCPTools';
import { ProjectMemoryDialog } from './ProjectMemoryDialog';
import { MAX_ATTACHMENT_BYTES, isSupportedAttachment, isImageFile, readFileAsDataUrl } from './uploadUtils';

interface ChatBoxProps {
isModelSettingsCollapsed: boolean;
Expand Down Expand Up @@ -55,6 +57,10 @@ interface ChatBoxProps {
handleStop?: (() => void) | undefined;
enhancingPrompt?: boolean | undefined;
enhancePrompt?: (() => void) | undefined;
projectMemory?: string;
setProjectMemory?: ((value: string) => void) | undefined;
planMode?: boolean;
setPlanMode?: ((enabled: boolean) => void) | undefined;
chatMode?: 'discuss' | 'build';
setChatMode?: (mode: 'discuss' | 'build') => void;
designScheme?: DesignScheme;
Expand Down Expand Up @@ -193,18 +199,40 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';

const files = Array.from(e.dataTransfer.files);
const uploads: Array<{ file: File; imageData?: string }> = [];

files.forEach((file) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
if (file.size > MAX_ATTACHMENT_BYTES) {
toast.error(`\"${file.name}\" exceeds the ${Math.round(MAX_ATTACHMENT_BYTES / 1024 / 1024)}MB limit`);
return;
}

reader.onload = (e) => {
const base64Image = e.target?.result as string;
props.setUploadedFiles?.([...props.uploadedFiles, file]);
props.setImageDataList?.([...props.imageDataList, base64Image]);
};
reader.readAsDataURL(file);
if (!isSupportedAttachment(file)) {
toast.error(`\"${file.name}\" is not a supported attachment type`);
return;
}

uploads.push({ file });
});

if (!uploads.length) {
return;
}

Promise.all(
uploads.map(async (item) => ({
...item,
imageData: isImageFile(item.file) ? await readFileAsDataUrl(item.file) : '',
})),
)
.then((resolved) => {
props.setUploadedFiles?.([...props.uploadedFiles, ...resolved.map((item) => item.file)]);
props.setImageDataList?.([...props.imageDataList, ...resolved.map((item) => item.imageData || '')]);
})
.catch((error) => {
console.error('Failed to read dropped files', error);
toast.error('Failed to read one or more dropped files');
});
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
Expand Down Expand Up @@ -262,6 +290,26 @@ export const ChatBox: React.FC<ChatBoxProps> = (props) => {
<div className="flex gap-1 items-center">
<ColorSchemeDialog designScheme={props.designScheme} setDesignScheme={props.setDesignScheme} />
<McpTools />
<ProjectMemoryDialog
memory={props.projectMemory}
onSave={(value) => {
props.setProjectMemory?.(value);
toast.success(value ? 'Project memory saved' : 'Project memory cleared');
}}
/>
<IconButton
title="Plan mode"
className={classNames(
'transition-all flex items-center gap-1 px-1.5',
props.planMode
? 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent'
: 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault',
)}
onClick={() => props.setPlanMode?.(!props.planMode)}
>
<div className="i-ph:list-checks text-xl" />
{props.planMode ? <span>Plan</span> : <span />}
</IconButton>
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
<div className="i-ph:paperclip text-xl"></div>
</IconButton>
Expand Down
Loading
Loading