Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add prompt caching to improve response times #134

Closed
wants to merge 2 commits into from
Closed
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
13 changes: 11 additions & 2 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ interface BaseChatProps {
messages?: Message[];
enhancingPrompt?: boolean;
promptEnhanced?: boolean;
fromCache?: boolean;
input?: string;
model: string;
setModel: (model: string) => void;
Expand All @@ -96,6 +97,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
isStreaming = false,
enhancingPrompt = false,
promptEnhanced = false,
fromCache = false,
messages,
input = '',
model,
Expand Down Expand Up @@ -224,8 +226,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</>
) : (
<>
<div className="i-bolt:stars text-xl"></div>
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
<div className={classNames("text-xl", {
"i-bolt:stars": !fromCache,
"i-heroicons:clock": fromCache && promptEnhanced
})}></div>
{promptEnhanced && (
<div className="ml-1.5">
{fromCache ? "From cache" : "Prompt enhanced"}
</div>
)}
</>
)}
</IconButton>
Expand Down
3 changes: 2 additions & 1 deletion app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
initialMessages,
});

const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
const { enhancingPrompt, promptEnhanced, fromCache, enhancePrompt, resetEnhancer } = usePromptEnhancer();
const { parsedMessages, parseMessages } = useMessageParser();

const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
Expand Down Expand Up @@ -212,6 +212,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
isStreaming={isLoading}
enhancingPrompt={enhancingPrompt}
promptEnhanced={promptEnhanced}
fromCache={fromCache}
sendMessage={sendMessage}
model={model}
setModel={setModel}
Expand Down
69 changes: 69 additions & 0 deletions app/components/editor/VersionHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useStore } from '@nanostores/react';
import { useState } from 'react';
import { versionHistoryStore } from '~/lib/stores/version-history';

interface VersionHistoryProps {
filePath: string;
}

export function VersionHistory({ filePath }: VersionHistoryProps) {
const [isReverting, setIsReverting] = useState(false);
const versions = versionHistoryStore.getVersions(filePath);
const currentVersion = versionHistoryStore.getCurrentVersion(filePath);

if (!versions.length) {
return null;
}

const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
};

const handleRevert = async (versionIndex: number) => {
try {
setIsReverting(true);
await versionHistoryStore.revertToVersion(filePath, versionIndex);
} catch (error) {
console.error('Failed to revert file:', error);
} finally {
setIsReverting(false);
}
};

return (
<div className="version-history p-4 bg-bolt-elements-background-depth-1">
<h3 className="text-lg font-semibold mb-4">Version History</h3>
<div className="version-list space-y-3 max-h-[300px] overflow-y-auto">
{versions.map((version, index) => (
<div
key={version.timestamp}
className={`version-item p-3 rounded-lg ${
currentVersion && currentVersion.timestamp === version.timestamp
? 'bg-bolt-elements-background-depth-3 border-l-2 border-bolt-elements-borderColor-active'
: 'bg-bolt-elements-background-depth-2'
}`}
>
<div className="version-info space-y-1">
<div className="flex justify-between items-center">
<span className="font-medium">Version {versions.length - index}</span>
<span className="text-sm text-bolt-elements-textSecondary">
{formatDate(version.timestamp)}
</span>
</div>
<p className="text-sm text-bolt-elements-textSecondary">{version.description}</p>
</div>
{currentVersion && currentVersion.timestamp !== version.timestamp && (
<button
onClick={() => handleRevert(index)}
className="mt-2 w-full px-3 py-1.5 text-sm bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary rounded hover:bg-bolt-elements-background-depth-4 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
disabled={isReverting}
>
{isReverting ? 'Reverting...' : 'Revert to this version'}
</button>
)}
</div>
))}
</div>
</div>
);
}
36 changes: 20 additions & 16 deletions app/components/workbench/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type OnSaveCallback as OnEditorSave,
type OnScrollCallback as OnEditorScroll,
} from '~/components/editor/codemirror/CodeMirrorEditor';
import { VersionHistory } from '~/components/editor/VersionHistory';
import { IconButton } from '~/components/ui/IconButton';
import { PanelHeader } from '~/components/ui/PanelHeader';
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
Expand Down Expand Up @@ -76,10 +77,6 @@ export const EditorPanel = memo(
return editorDocument.filePath.split('/');
}, [editorDocument]);

const activeFileUnsaved = useMemo(() => {
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
}, [editorDocument, unsavedFiles]);

useEffect(() => {
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
terminalToggledByShortcut.current = true;
Expand Down Expand Up @@ -149,7 +146,7 @@ export const EditorPanel = memo(
{activeFileSegments?.length && (
<div className="flex items-center flex-1 text-sm">
<FileBreadcrumb pathSegments={activeFileSegments} files={files} onFileSelect={onFileSelect} />
{activeFileUnsaved && (
{editorDocument && (
<div className="flex gap-1 ml-auto -mr-1.5">
<PanelHeaderButton onClick={onFileSave}>
<div className="i-ph:floppy-disk-duotone" />
Expand All @@ -164,17 +161,24 @@ export const EditorPanel = memo(
</div>
)}
</PanelHeader>
<div className="h-full flex-1 overflow-hidden">
<CodeMirrorEditor
theme={theme}
editable={!isStreaming && editorDocument !== undefined}
settings={editorSettings}
doc={editorDocument}
autoFocusOnDocumentChange={!isMobile()}
onScroll={onEditorScroll}
onChange={onEditorChange}
onSave={onFileSave}
/>
<div className="flex flex-col h-full flex-1 overflow-hidden">
<div className="flex-1">
<CodeMirrorEditor
theme={theme}
editable={!isStreaming && editorDocument !== undefined}
settings={editorSettings}
doc={editorDocument}
autoFocusOnDocumentChange={!isMobile()}
onScroll={onEditorScroll}
onChange={onEditorChange}
onSave={onFileSave}
/>
</div>
{editorDocument && (
<div className="border-t border-bolt-elements-borderColor">
<VersionHistory filePath={editorDocument.filePath} />
</div>
)}
</div>
</Panel>
</PanelGroup>
Expand Down
18 changes: 18 additions & 0 deletions app/components/workbench/Workbench.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
workbenchStore.setDocuments(files);
}, [files]);

// Force workbench to show when a file is selected or modified
useEffect(() => {
if (selectedFile || (currentDocument && unsavedFiles.has(currentDocument.filePath))) {
workbenchStore.showWorkbench.set(true);
workbenchStore.currentView.set('code');
}
}, [selectedFile, currentDocument, unsavedFiles]);

// Show version history for files modified through chat
useEffect(() => {
const currentFile = currentDocument?.filePath;
if (currentFile && files[currentFile]) {
workbenchStore.setShowWorkbench(true);
workbenchStore.currentView.set('code');
}
}, [files, currentDocument]);

const onEditorChange = useCallback<OnEditorChange>((update) => {
workbenchStore.setCurrentDocumentContent(update.content);
}, []);
Expand Down Expand Up @@ -230,6 +247,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
)
);
});

interface ViewProps extends HTMLMotionProps<'div'> {
children: JSX.Element;
}
Expand Down
15 changes: 14 additions & 1 deletion app/lib/hooks/usePromptEnhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ const logger = createScopedLogger('usePromptEnhancement');
export function usePromptEnhancer() {
const [enhancingPrompt, setEnhancingPrompt] = useState(false);
const [promptEnhanced, setPromptEnhanced] = useState(false);
const [fromCache, setFromCache] = useState(false);

const resetEnhancer = () => {
setEnhancingPrompt(false);
setPromptEnhanced(false);
setFromCache(false);
};

const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
setEnhancingPrompt(true);
setPromptEnhanced(false);
setFromCache(false);

const response = await fetch('/api/enhancer', {
method: 'POST',
Expand All @@ -23,6 +26,10 @@ export function usePromptEnhancer() {
}),
});

// Check if response was from cache
const isCached = response.headers.get('x-from-cache') === 'true';
setFromCache(isCached);

const reader = response.body?.getReader();

const originalInput = input;
Expand Down Expand Up @@ -67,5 +74,11 @@ export function usePromptEnhancer() {
}
};

return { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer };
return {
enhancingPrompt,
promptEnhanced,
fromCache,
enhancePrompt,
resetEnhancer
};
}
27 changes: 19 additions & 8 deletions app/lib/runtime/action-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type { BoltAction } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser';
import { versionHistoryStore } from '../stores/version-history';
import { workbenchStore } from '../stores/workbench';
import type { FilesStore } from '../stores/files';

const logger = createScopedLogger('ActionRunner');

Expand Down Expand Up @@ -35,12 +38,14 @@ type ActionsMap = MapStore<Record<string, ActionState>>;

export class ActionRunner {
#webcontainer: Promise<WebContainer>;
#filesStore: FilesStore;
#currentExecutionPromise: Promise<void> = Promise.resolve();

actions: ActionsMap = map({});

constructor(webcontainerPromise: Promise<WebContainer>) {
constructor(webcontainerPromise: Promise<WebContainer>, filesStore: FilesStore) {
this.#webcontainer = webcontainerPromise;
this.#filesStore = filesStore;
}

addAction(data: ActionCallbackData) {
Expand All @@ -50,7 +55,6 @@ export class ActionRunner {
const action = actions[actionId];

if (action) {
// action already added
return;
}

Expand Down Expand Up @@ -115,8 +119,6 @@ export class ActionRunner {
this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
} catch (error) {
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });

// re-throw the error to be caught in the promise chain
throw error;
}
}
Expand Down Expand Up @@ -145,7 +147,6 @@ export class ActionRunner {
);

const exitCode = await process.exit;

logger.debug(`Process terminated with code ${exitCode}`);
}

Expand All @@ -157,8 +158,6 @@ export class ActionRunner {
const webcontainer = await this.#webcontainer;

let folder = nodePath.dirname(action.filePath);

// remove trailing slashes
folder = folder.replace(/\/+$/g, '');

if (folder !== '.') {
Expand All @@ -172,6 +171,19 @@ export class ActionRunner {

try {
await webcontainer.fs.writeFile(action.filePath, action.content);

// Check if this is a modification of an existing file
if (this.#filesStore.isExistingFile(action.filePath)) {
// Only mark as modified if file existed before
const newUnsavedFiles = new Set(workbenchStore.unsavedFiles.get());
newUnsavedFiles.add(action.filePath);
workbenchStore.unsavedFiles.set(newUnsavedFiles);
versionHistoryStore.addVersion(action.filePath, action.content, 'Modified through chat');
} else {
// This is a new file
versionHistoryStore.addVersion(action.filePath, action.content, 'Initial version');
}

logger.debug(`File written ${action.filePath}`);
} catch (error) {
logger.error('Failed to write file\n\n', error);
Expand All @@ -180,7 +192,6 @@ export class ActionRunner {

#updateAction(id: string, newState: ActionStateUpdate) {
const actions = this.actions.get();

this.actions.setKey(id, { ...actions[id], ...newState });
}
}
Loading