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
2 changes: 2 additions & 0 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { ProgressAnnotation } from '~/types/context';
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
import { useStore } from '@nanostores/react';
import { PromptQueuePanel } from './PromptQueuePanel';
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
import { ChatBox } from './ChatBox';
import type { DesignScheme } from '~/types/design-scheme';
Expand Down Expand Up @@ -469,6 +470,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setSelectedElement={setSelectedElement}
onWebSearchResult={onWebSearchResult}
/>
{chatStarted && <PromptQueuePanel isStreaming={isStreaming} />}
</div>
</StickToBottom>
<div className="flex flex-col justify-center">
Expand Down
44 changes: 44 additions & 0 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { createSampler } from '~/utils/sampler';
import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
import { logStore } from '~/lib/stores/logs';
import { streamingState } from '~/lib/stores/streaming';
import { promptQueueStore, advanceQueue, clearPendingPrompt, stopQueue } from '~/lib/stores/promptQueue';
import { filesToArtifacts } from '~/utils/fileUtils';
import { supabaseConnection } from '~/lib/stores/supabase';
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
Expand Down Expand Up @@ -154,6 +155,7 @@ export const ChatImpl = memo(
onError: (e) => {
setFakeLoading(false);
handleError(e, 'chat');
stopQueue();
},
onFinish: (message, response) => {
const usage = response.usage;
Expand All @@ -172,6 +174,16 @@ export const ChatImpl = memo(
}

logger.debug('Finished streaming');

/* Advance the prompt queue if one is running */
const nextPrompt = advanceQueue();

if (nextPrompt) {
/* Small delay so the UI can settle before the next message fires */
setTimeout(() => {
promptQueueStore.setKey('pendingPrompt', nextPrompt);
}, 800);
}
},
initialMessages,
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
Expand Down Expand Up @@ -200,6 +212,38 @@ export const ChatImpl = memo(
chatStore.setKey('started', initialMessages.length > 0);
}, []);

/*
* Pre-fill the textarea with the follow-up prompt set by ImportZipButton before the
* full-page navigation. Using setInput instead of append so the user confirms with
* one Enter keystroke — avoids model-state race conditions on chat initialisation.
*/
useEffect(() => {
if (initialMessages.length === 0) {
return;
}

const autorun = localStorage.getItem('bolt_zip_autorun');

if (autorun) {
localStorage.removeItem('bolt_zip_autorun');
setInput(autorun);
}
}, []);

/* Fire the next queued prompt whenever the store signals one is ready */
useEffect(() => {
const unsubscribe = promptQueueStore.subscribe((state) => {
if (state.pendingPrompt) {
clearPendingPrompt();

const messageText = `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${state.pendingPrompt}`;
append({ role: 'user', content: messageText });
}
});

return unsubscribe;
}, [append, model, provider]);

useEffect(() => {
processSampledMessages({
messages,
Expand Down
109 changes: 109 additions & 0 deletions app/components/chat/ImportZipButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useRef, useState } from 'react';
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { createChatFromZip } from '~/utils/zipImport';
import { logStore } from '~/lib/stores/logs';
import { Button } from '~/components/ui/Button';
import { classNames } from '~/utils/classNames';

interface ImportZipButtonProps {
className?: string;
importChat?: (description: string, messages: Message[]) => Promise<void>;
}

export const ImportZipButton: React.FC<ImportZipButtonProps> = ({ className, importChat }) => {
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];

if (!file) {
return;
}

setIsLoading(true);

const loadingToast = toast.loading(`Importing ${file.name}…`);

try {
const result = await createChatFromZip(file);

if (result.skippedBinary > 0) {
logStore.logWarning('Skipping binary files during ZIP import', {
zipName: file.name,
binaryCount: result.skippedBinary,
});
toast.info(`Skipping ${result.skippedBinary} binary file${result.skippedBinary === 1 ? '' : 's'}`);
}

/*
* Set flag before navigation so the new chat picks it up on mount.
* importChat does a full window.location.href redirect, so append()
* would be gone by the time it resolves.
*/
if (result.hasExpoConfig) {
/*
* Expo/React Native — WebContainer can't run native code.
* Ask bolt to review the code instead of trying to boot the project.
*/
localStorage.setItem(
'bolt_zip_autorun',
'This is an Expo/React Native project. Review the code structure and give me a summary of what the app does and how it is organized. Do not run any install or dev server commands — I will run this locally with Expo CLI.',
);
} else if (result.hasPackageJson) {
localStorage.setItem('bolt_zip_autorun', 'Install the dependencies and start the development server.');
}

if (importChat) {
await importChat(file.name.replace(/\.zip$/i, ''), result.messages);
}

logStore.logSystem('ZIP imported successfully', {
zipName: file.name,
textFileCount: result.totalFiles - result.skippedBinary - result.skippedIgnored,
binaryFileCount: result.skippedBinary,
ignoredFileCount: result.skippedIgnored,
});

toast.success('ZIP imported successfully');
} catch (error) {
logStore.logError('Failed to import ZIP', error, { zipName: file.name });
console.error('Failed to import ZIP:', error);
toast.error(error instanceof Error ? error.message : 'Failed to import ZIP');
} finally {
setIsLoading(false);
toast.dismiss(loadingToast);

// Reset so the same file can be re-selected
if (inputRef.current) {
inputRef.current.value = '';
}
}
};

return (
<>
<input ref={inputRef} type="file" className="hidden" accept=".zip" onChange={handleFileChange} />
<Button
onClick={() => inputRef.current?.click()}
title="Import ZIP"
variant="default"
size="lg"
className={classNames(
'gap-2 bg-bolt-elements-background-depth-1',
'text-bolt-elements-textPrimary',
'hover:bg-bolt-elements-background-depth-2',
'border border-bolt-elements-borderColor',
'h-10 px-4 py-2 min-w-[120px] justify-center',
'transition-all duration-200 ease-in-out',
className,
)}
disabled={isLoading}
>
<span className="i-ph:file-zip w-4 h-4" />
{isLoading ? 'Importing…' : 'Import ZIP'}
</Button>
</>
);
};
Loading
Loading