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
91 changes: 86 additions & 5 deletions app/components/@settings/tabs/features/FeaturesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Remove unused imports
import React, { memo, useCallback } from 'react';
import React, { memo, useCallback, useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Switch } from '~/components/ui/Switch';
import { useSettings } from '~/lib/hooks/useSettings';
Expand Down Expand Up @@ -119,6 +119,29 @@ export default function FeaturesTab() {
promptId,
} = useSettings();

// State for custom prompt text
const [customPromptText, setCustomPromptText] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

// Load custom prompt from localStorage on component mount
useEffect(() => {
setCustomPromptText(PromptLibrary.getCustomPrompt());
setHasUnsavedChanges(false);
}, []);

// Handle custom prompt change
const handleCustomPromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setCustomPromptText(e.target.value);
setHasUnsavedChanges(true);
};

// Save custom prompt
const handleSaveCustomPrompt = () => {
PromptLibrary.saveCustomPrompt(customPromptText);
toast.success('Custom prompt saved');
setHasUnsavedChanges(false);
};

// Enable features by default on first load
React.useEffect(() => {
// Only set defaults if values are undefined
Expand All @@ -143,6 +166,13 @@ export default function FeaturesTab() {
}
}, []); // Only run once on component mount

// Add this effect to check if the user has selected the custom prompt option but hasn't saved a custom prompt yet
useEffect(() => {
if (promptId === 'custom' && !PromptLibrary.hasCustomPrompt() && !customPromptText) {
toast.warning('Please create and save a custom prompt to use this option');
}
}, [promptId, customPromptText]);

const handleToggleFeature = useCallback(
(id: string, enabled: boolean) => {
switch (id) {
Expand Down Expand Up @@ -177,6 +207,18 @@ export default function FeaturesTab() {
[enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
);

// Update the select onChange handler to warn if selecting custom without a saved prompt
const handlePromptIdChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newPromptId = e.target.value;

if (newPromptId === 'custom' && !PromptLibrary.hasCustomPrompt()) {
toast.info('Please create and save a custom prompt');
}

setPromptId(newPromptId);
toast.success('Prompt template updated');
};

const features = {
stable: [
{
Expand Down Expand Up @@ -269,10 +311,7 @@ export default function FeaturesTab() {
</div>
<select
value={promptId}
onChange={(e) => {
setPromptId(e.target.value);
toast.success('Prompt template updated');
}}
onChange={handlePromptIdChange}
className={classNames(
'p-2 rounded-lg text-sm min-w-[200px]',
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
Expand All @@ -289,6 +328,48 @@ export default function FeaturesTab() {
))}
</select>
</div>

{/* Custom Prompt Text Area */}
{promptId === 'custom' && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4"
>
<textarea
value={customPromptText}
onChange={handleCustomPromptChange}
placeholder="Enter your custom system prompt here..."
className={classNames(
'w-full p-3 rounded-lg text-sm min-h-[200px]',
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
'resize-y',
)}
/>
<div className="flex justify-between items-center mt-2">
<p className="text-xs text-bolt-elements-textSecondary">
{hasUnsavedChanges ? 'You have unsaved changes' : 'Your custom prompt is saved'}
</p>
<button
onClick={handleSaveCustomPrompt}
disabled={!hasUnsavedChanges}
className={classNames(
'px-4 py-2 rounded-lg text-sm font-medium',
'transition-all duration-200',
hasUnsavedChanges
? 'bg-purple-500 text-white hover:bg-purple-600'
: 'bg-bolt-elements-background-depth-4 text-bolt-elements-textTertiary cursor-not-allowed',
)}
>
Save Prompt
</button>
</div>
</motion.div>
)}
</motion.div>
</div>
);
Expand Down
32 changes: 32 additions & 0 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTempla
import { logStore } from '~/lib/stores/logs';
import { streamingState } from '~/lib/stores/streaming';
import { filesToArtifacts } from '~/utils/fileUtils';
import { CUSTOM_PROMPT_STORAGE_KEY } from '~/lib/common/prompt-library';

const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
Expand Down Expand Up @@ -140,6 +141,36 @@ export const ChatImpl = memo(

const [apiKeys, setApiKeys] = useState<Record<string, string>>({});

const [customPrompt, setCustomPrompt] = useState<string | undefined>(() => {
if (promptId === 'custom') {
return localStorage.getItem(CUSTOM_PROMPT_STORAGE_KEY) || undefined;
}

return undefined;
});

useEffect(() => {
if (promptId === 'custom') {
setCustomPrompt(localStorage.getItem(CUSTOM_PROMPT_STORAGE_KEY) || undefined);
} else {
setCustomPrompt(undefined);
}
}, [promptId]);

useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === CUSTOM_PROMPT_STORAGE_KEY && promptId === 'custom') {
setCustomPrompt(event.newValue || undefined);
}
};

window.addEventListener('storage', handleStorageChange);

return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [promptId]);

const {
messages,
isLoading,
Expand All @@ -160,6 +191,7 @@ export const ChatImpl = memo(
files,
promptId,
contextOptimization: contextOptimizationEnabled,
customPrompt,
},
sendExtraMessageFields: true,
onError: (e) => {
Expand Down
31 changes: 25 additions & 6 deletions app/lib/.server/llm/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function streamText(props: {
contextFiles?: FileMap;
summary?: string;
messageSliceId?: number;
customPrompt?: string;
}) {
const {
messages,
Expand All @@ -40,6 +41,7 @@ export async function streamText(props: {
contextOptimization,
contextFiles,
summary,
customPrompt,
} = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
Expand Down Expand Up @@ -92,12 +94,29 @@ export async function streamText(props: {

const dynamicMaxTokens = modelDetails && modelDetails.maxTokenAllowed ? modelDetails.maxTokenAllowed : MAX_TOKENS;

let systemPrompt =
PromptLibrary.getPropmtFromLibrary(promptId || 'default', {
cwd: WORK_DIR,
allowedHtmlElements: allowedHTMLElements,
modificationTagName: MODIFICATIONS_TAG_NAME,
}) ?? getSystemPrompt();
let systemPrompt;

if (promptId === 'custom') {
if (customPrompt && customPrompt.trim()) {
systemPrompt = customPrompt;
} else {
systemPrompt =
PromptLibrary.getPropmtFromLibrary('default', {
cwd: WORK_DIR,
allowedHtmlElements: allowedHTMLElements,
modificationTagName: MODIFICATIONS_TAG_NAME,
}) ?? getSystemPrompt();

logger.warn('Custom prompt selected but no custom prompt provided, falling back to default prompt');
}
} else {
systemPrompt =
PromptLibrary.getPropmtFromLibrary(promptId || 'default', {
cwd: WORK_DIR,
allowedHtmlElements: allowedHTMLElements,
modificationTagName: MODIFICATIONS_TAG_NAME,
}) ?? getSystemPrompt();
}

if (files && contextFiles && contextOptimization) {
const codeContext = createFilesContext(contextFiles, true);
Expand Down
54 changes: 53 additions & 1 deletion app/lib/common/prompt-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface PromptOptions {
modificationTagName: string;
}

// Define a constant for the localStorage key
export const CUSTOM_PROMPT_STORAGE_KEY = 'custom_prompt';

export class PromptLibrary {
static library: Record<
string,
Expand All @@ -26,6 +29,21 @@ export class PromptLibrary {
description: 'an Experimental version of the prompt for lower token usage',
get: (options) => optimized(options),
},
custom: {
label: 'Custom Prompt',
description: 'Your own custom system prompt',
get: (_options) => {
// Get the custom prompt from localStorage if available
if (typeof window !== 'undefined') {
const customPrompt = localStorage.getItem(CUSTOM_PROMPT_STORAGE_KEY);
return customPrompt && customPrompt.trim()
? customPrompt
: 'No custom prompt defined. Please set a custom prompt in the settings.';
}

return 'Custom prompt not available';
},
},
};
static getList() {
return Object.entries(this.library).map(([key, value]) => {
Expand All @@ -41,9 +59,43 @@ export class PromptLibrary {
const prompt = this.library[promptId];

if (!prompt) {
throw 'Prompt Now Found';
throw 'Prompt Not Found';
}

return this.library[promptId]?.get(options);
}

// Helper method to save a custom prompt to localStorage
static saveCustomPrompt(promptText: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem(CUSTOM_PROMPT_STORAGE_KEY, promptText);

// Dispatch a storage event to notify other tabs/windows
window.dispatchEvent(
new StorageEvent('storage', {
key: CUSTOM_PROMPT_STORAGE_KEY,
newValue: promptText,
}),
);
}
}

// Helper method to get the current custom prompt from localStorage
static getCustomPrompt(): string {
if (typeof window !== 'undefined') {
return localStorage.getItem(CUSTOM_PROMPT_STORAGE_KEY) || '';
}

return '';
}

// Helper method to check if a custom prompt exists
static hasCustomPrompt(): boolean {
if (typeof window !== 'undefined') {
const prompt = localStorage.getItem(CUSTOM_PROMPT_STORAGE_KEY);
return !!prompt && prompt.trim().length > 0;
}

return false;
}
}
11 changes: 7 additions & 4 deletions app/routes/api.chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ function parseCookies(cookieHeader: string): Record<string, string> {
}

async function chatAction({ context, request }: ActionFunctionArgs) {
const { messages, files, promptId, contextOptimization } = await request.json<{
const { messages, files, promptId, contextOptimization, customPrompt } = await request.json<{
messages: Messages;
files: any;
promptId?: string;
contextOptimization: boolean;
customPrompt?: string;
}>();

const cookieHeader = request.headers.get('Cookie');
Expand Down Expand Up @@ -242,6 +243,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextFiles: filteredFiles,
summary,
messageSliceId,
customPrompt,
});

result.mergeIntoDataStream(dataStream);
Expand Down Expand Up @@ -269,7 +271,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
message: 'Generating Response',
} satisfies ProgressAnnotation);

const result = await streamText({
const textStream = await streamText({
messages,
env: context.cloudflare?.env,
options,
Expand All @@ -281,10 +283,11 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
contextFiles: filteredFiles,
summary,
messageSliceId,
customPrompt,
});

(async () => {
for await (const part of result.fullStream) {
for await (const part of textStream.fullStream) {
if (part.type === 'error') {
const error: any = part.error;
logger.error(`${error}`);
Expand All @@ -293,7 +296,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}
}
})();
result.mergeIntoDataStream(dataStream);
textStream.mergeIntoDataStream(dataStream);
},
onError: (error: any) => `Custom error: ${error.message}`,
}).pipeThrough(
Expand Down
Loading