Merge branch 'main' into feat/improve-prompt-enhancement

This commit is contained in:
Dustin Loring
2024-12-03 07:44:53 -05:00
committed by GitHub
49 changed files with 30962 additions and 3429 deletions

View File

@@ -17,3 +17,107 @@
.Chat {
opacity: 1;
}
.RayContainer {
--gradient-opacity: 0.85;
--ray-gradient: radial-gradient(rgba(83, 196, 255, var(--gradient-opacity)) 0%, rgba(43, 166, 255, 0) 100%);
transition: opacity 0.25s linear;
position: fixed;
inset: 0;
pointer-events: none;
user-select: none;
}
.LightRayOne {
width: 480px;
height: 680px;
transform: rotate(80deg);
top: -540px;
left: 250px;
filter: blur(110px);
position: absolute;
border-radius: 100%;
background: var(--ray-gradient);
}
.LightRayTwo {
width: 110px;
height: 400px;
transform: rotate(-20deg);
top: -280px;
left: 350px;
mix-blend-mode: overlay;
opacity: 0.6;
filter: blur(60px);
position: absolute;
border-radius: 100%;
background: var(--ray-gradient);
}
.LightRayThree {
width: 400px;
height: 370px;
top: -350px;
left: 200px;
mix-blend-mode: overlay;
opacity: 0.6;
filter: blur(21px);
position: absolute;
border-radius: 100%;
background: var(--ray-gradient);
}
.LightRayFour {
position: absolute;
width: 330px;
height: 370px;
top: -330px;
left: 50px;
mix-blend-mode: overlay;
opacity: 0.5;
filter: blur(21px);
border-radius: 100%;
background: var(--ray-gradient);
}
.LightRayFive {
position: absolute;
width: 110px;
height: 400px;
transform: rotate(-40deg);
top: -280px;
left: -10px;
mix-blend-mode: overlay;
opacity: 0.8;
filter: blur(60px);
border-radius: 100%;
background: var(--ray-gradient);
}
.PromptEffectContainer {
--prompt-container-offset: 50px;
--prompt-line-stroke-width: 1px;
position: absolute;
pointer-events: none;
inset: calc(var(--prompt-container-offset) / -2);
width: calc(100% + var(--prompt-container-offset));
height: calc(100% + var(--prompt-container-offset));
}
.PromptEffectLine {
width: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
height: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
x: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
y: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
rx: calc(8px - var(--prompt-line-stroke-width));
fill: transparent;
stroke-width: var(--prompt-line-stroke-width);
stroke: url(#line-gradient);
stroke-dasharray: 35px 65px;
stroke-dashoffset: 10;
}
.PromptShine {
fill: url(#shine-gradient);
mix-blend-mode: overlay;
}

View File

@@ -19,7 +19,7 @@ import * as Tooltip from '@radix-ui/react-tooltip';
import styles from './BaseChat.module.scss';
import type { ProviderInfo } from '~/utils/types';
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
import { ImportButton } from '~/components/chat/chatExportAndImport/ImportButton';
import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
// @ts-ignore TODO: Introduce proper types
@@ -47,7 +47,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
key={provider?.name}
value={model}
onChange={(e) => setModel(e.target.value)}
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%] "
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%]"
>
{[...modelList]
.filter((e) => e.provider == provider?.name && e.name)
@@ -116,6 +116,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const [modelList, setModelList] = useState(MODEL_LIST);
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
useEffect(() => {
// Load API keys from cookies on component mount
@@ -167,6 +168,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
)}
data-chat-visible={showChat}
>
<div className={classNames(styles.RayContainer)}>
<div className={classNames(styles.LightRayOne)}></div>
<div className={classNames(styles.LightRayTwo)}></div>
<div className={classNames(styles.LightRayThree)}></div>
<div className={classNames(styles.LightRayFour)}></div>
<div className={classNames(styles.LightRayFive)}></div>
</div>
<ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
@@ -199,39 +207,85 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</ClientOnly>
<div
className={classNames(
' bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
{
'sticky bottom-2': chatStarted,
},
)}
>
<ModelSelector
key={provider?.name + ':' + modelList.length}
model={model}
setModel={setModel}
modelList={modelList}
provider={provider}
setProvider={setProvider}
providerList={PROVIDER_LIST}
apiKeys={apiKeys}
/>
<svg className={classNames(styles.PromptEffectContainer)}>
<defs>
<linearGradient
id="line-gradient"
x1="20%"
y1="0%"
x2="-14%"
y2="10%"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(-45)"
>
<stop offset="0%" stopColor="#1488fc" stopOpacity="0%"></stop>
<stop offset="40%" stopColor="#1488fc" stopOpacity="80%"></stop>
<stop offset="50%" stopColor="#1488fc" stopOpacity="80%"></stop>
<stop offset="100%" stopColor="#1488fc" stopOpacity="0%"></stop>
</linearGradient>
<linearGradient id="shine-gradient">
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
<stop offset="40%" stopColor="#8adaff" stopOpacity="80%"></stop>
<stop offset="50%" stopColor="#8adaff" stopOpacity="80%"></stop>
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
</linearGradient>
</defs>
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
</svg>
<div>
<div className="flex justify-between items-center mb-2">
<button
onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
className={classNames('flex items-center gap-2 p-2 rounded-lg transition-all', {
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
isModelSettingsCollapsed,
'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
!isModelSettingsCollapsed,
})}
>
<div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
<span>Model Settings</span>
</button>
</div>
{provider && (
<APIKeyManager
provider={provider}
apiKey={apiKeys[provider.name] || ''}
setApiKey={(key) => updateApiKey(provider.name, key)}
/>
)}
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
<ModelSelector
key={provider?.name + ':' + modelList.length}
model={model}
setModel={setModel}
modelList={modelList}
provider={provider}
setProvider={setProvider}
providerList={PROVIDER_LIST}
apiKeys={apiKeys}
/>
{provider && (
<APIKeyManager
provider={provider}
apiKey={apiKeys[provider.name] || ''}
setApiKey={(key) => updateApiKey(provider.name, key)}
/>
)}
</div>
</div>
<div
className={classNames(
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
)}
>
<textarea
ref={textareaRef}
className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-0 focus:border-none focus:shadow-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
className={
'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm'
}
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.shiftKey) {
@@ -299,15 +353,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary">
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for
a new line
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a
new line
</div>
) : null}
</div>
</div>
</div>
</div>
{!chatStarted && ImportButton(importChat)}
{!chatStarted && ImportButtons(importChat)}
{!chatStarted && ExamplePrompts(sendMessage)}
</div>
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>

View File

@@ -6,19 +6,20 @@ import { useStore } from '@nanostores/react';
import type { Message } from 'ai';
import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
import { description, useChatHistory } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { fileModificationsToHTML } from '~/utils/diff';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
import Cookies from 'js-cookie';
import type { ProviderInfo } from '~/utils/types';
import { debounce } from '~/utils/debounce';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -120,6 +121,7 @@ export const ChatImpl = memo(
logger.debug('Finished streaming');
},
initialMessages,
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
});
const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
@@ -225,12 +227,33 @@ export const ChatImpl = memo(
}
setInput('');
Cookies.remove(PROMPT_COOKIE_KEY);
resetEnhancer();
textareaRef.current?.blur();
};
/**
* Handles the change event for the textarea and updates the input state.
* @param event - The change event from the textarea.
*/
const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
handleInputChange(event);
};
/**
* Debounced function to cache the prompt in cookies.
* Caches the trimmed value of the textarea input after a delay to optimize performance.
*/
const debouncedCachePrompt = useCallback(
debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => {
const trimmedValue = event.target.value.trim();
Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 });
}, 1000),
[],
);
const [messageRef, scrollRef] = useSnapScroll();
useEffect(() => {
@@ -268,7 +291,10 @@ export const ChatImpl = memo(
setProvider={handleProviderChange}
messageRef={messageRef}
scrollRef={scrollRef}
handleInputChange={handleInputChange}
handleInputChange={(e) => {
onTextareaChange(e);
debouncedCachePrompt(e);
}}
handleStop={abort}
description={description}
importChat={importChat}

View File

@@ -5,13 +5,18 @@ const EXAMPLE_PROMPTS = [
{ text: 'Build a simple blog using Astro' },
{ text: 'Create a cookie consent form using Material UI' },
{ text: 'Make a space invaders game' },
{ text: 'How do I center a div?' },
{ text: 'Make a Tic Tac Toe game in html, css and js only' },
];
export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInput?: string): void | undefined }) {
return (
<div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
<div id="examples" className="relative flex flex-col gap-9 w-full max-w-3xl mx-auto flex justify-center mt-6">
<div
className="flex flex-wrap justify-center gap-2"
style={{
animation: '.25s ease-out 0s 1 _fade-and-move-in_g2ptj_1 forwards',
}}
>
{EXAMPLE_PROMPTS.map((examplePrompt, index: number) => {
return (
<button
@@ -19,10 +24,9 @@ export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInp
onClick={(event) => {
sendMessage?.(event, examplePrompt.text);
}}
className="group flex items-center w-full gap-2 justify-center bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-theme"
className="border border-bolt-elements-borderColor rounded-full bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary px-3 py-1 text-xs transition-theme"
>
{examplePrompt.text}
<div className="i-ph:arrow-bend-down-left" />
</button>
);
})}

View File

@@ -0,0 +1,164 @@
import React from 'react';
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import ignore from 'ignore';
interface ImportFolderButtonProps {
className?: string;
importChat?: (description: string, messages: Message[]) => Promise<void>;
}
// Common patterns to ignore, similar to .gitignore
const IGNORE_PATTERNS = [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'.next/**',
'coverage/**',
'.cache/**',
'.vscode/**',
'.idea/**',
'**/*.log',
'**/.DS_Store',
'**/npm-debug.log*',
'**/yarn-debug.log*',
'**/yarn-error.log*',
];
const ig = ignore().add(IGNORE_PATTERNS);
const generateId = () => Math.random().toString(36).substring(2, 15);
const isBinaryFile = async (file: File): Promise<boolean> => {
const chunkSize = 1024; // Read the first 1 KB of the file
const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer());
for (let i = 0; i < buffer.length; i++) {
const byte = buffer[i];
if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
return true; // Found a binary character
}
}
return false;
};
export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
const shouldIncludeFile = (path: string): boolean => {
return !ig.ignores(path);
};
const createChatFromFolder = async (files: File[], binaryFiles: string[]) => {
const fileArtifacts = await Promise.all(
files.map(async (file) => {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const content = reader.result as string;
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
resolve(
`<boltAction type="file" filePath="${relativePath}">
${content}
</boltAction>`,
);
};
reader.onerror = reject;
reader.readAsText(file);
});
}),
);
const binaryFilesMessage =
binaryFiles.length > 0
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
: '';
const message: Message = {
role: 'assistant',
content: `I'll help you set up these files.${binaryFilesMessage}
<boltArtifact id="imported-files" title="Imported Files">
${fileArtifacts.join('\n\n')}
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
};
const userMessage: Message = {
role: 'user',
id: generateId(),
content: 'Import my files',
createdAt: new Date(),
};
const description = `Folder Import: ${files[0].webkitRelativePath.split('/')[0]}`;
if (importChat) {
await importChat(description, [userMessage, message]);
}
};
return (
<>
<input
type="file"
id="folder-import"
className="hidden"
webkitdirectory=""
directory=""
onChange={async (e) => {
const allFiles = Array.from(e.target.files || []);
const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
if (filteredFiles.length === 0) {
toast.error('No files found in the selected folder');
return;
}
try {
const fileChecks = await Promise.all(
filteredFiles.map(async (file) => ({
file,
isBinary: await isBinaryFile(file),
})),
);
const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file);
const binaryFilePaths = fileChecks
.filter((f) => f.isBinary)
.map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
if (textFiles.length === 0) {
toast.error('No text files found in the selected folder');
return;
}
if (binaryFilePaths.length > 0) {
toast.info(`Skipping ${binaryFilePaths.length} binary files`);
}
await createChatFromFolder(textFiles, binaryFilePaths);
} catch (error) {
console.error('Failed to import folder:', error);
toast.error('Failed to import folder');
}
e.target.value = ''; // Reset file input
}}
{...({} as any)} // if removed webkitdirectory will throw errors as unknow attribute
/>
<button
onClick={() => {
const input = document.getElementById('folder-import');
input?.click();
}}
className={className}
>
<div className="i-ph:upload-simple" />
Import Folder
</button>
</>
);
};

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { stripCodeFenceFromArtifact } from './Markdown';
describe('stripCodeFenceFromArtifact', () => {
it('should remove code fences around artifact element', () => {
const input = "```xml\n<div class='__boltArtifact__'></div>\n```";
const expected = "\n<div class='__boltArtifact__'></div>\n";
expect(stripCodeFenceFromArtifact(input)).toBe(expected);
});
it('should handle code fence with language specification', () => {
const input = "```typescript\n<div class='__boltArtifact__'></div>\n```";
const expected = "\n<div class='__boltArtifact__'></div>\n";
expect(stripCodeFenceFromArtifact(input)).toBe(expected);
});
it('should not modify content without artifacts', () => {
const input = '```\nregular code block\n```';
expect(stripCodeFenceFromArtifact(input)).toBe(input);
});
it('should handle empty input', () => {
expect(stripCodeFenceFromArtifact('')).toBe('');
});
it('should handle artifact without code fences', () => {
const input = "<div class='__boltArtifact__'></div>";
expect(stripCodeFenceFromArtifact(input)).toBe(input);
});
it('should handle multiple artifacts but only remove fences around them', () => {
const input = [
'Some text',
'```typescript',
"<div class='__boltArtifact__'></div>",
'```',
'```',
'regular code',
'```',
].join('\n');
const expected = ['Some text', '', "<div class='__boltArtifact__'></div>", '', '```', 'regular code', '```'].join(
'\n',
);
expect(stripCodeFenceFromArtifact(input)).toBe(expected);
});
});

View File

@@ -68,7 +68,51 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
remarkPlugins={remarkPlugins(limitedMarkdown)}
rehypePlugins={rehypePlugins(html)}
>
{children}
{stripCodeFenceFromArtifact(children)}
</ReactMarkdown>
);
});
/**
* Removes code fence markers (```) surrounding an artifact element while preserving the artifact content.
* This is necessary because artifacts should not be wrapped in code blocks when rendered for rendering action list.
*
* @param content - The markdown content to process
* @returns The processed content with code fence markers removed around artifacts
*
* @example
* // Removes code fences around artifact
* const input = "```xml\n<div class='__boltArtifact__'></div>\n```";
* stripCodeFenceFromArtifact(input);
* // Returns: "\n<div class='__boltArtifact__'></div>\n"
*
* @remarks
* - Only removes code fences that directly wrap an artifact (marked with __boltArtifact__ class)
* - Handles code fences with optional language specifications (e.g. ```xml, ```typescript)
* - Preserves original content if no artifact is found
* - Safely handles edge cases like empty input or artifacts at start/end of content
*/
export const stripCodeFenceFromArtifact = (content: string) => {
if (!content || !content.includes('__boltArtifact__')) {
return content;
}
const lines = content.split('\n');
const artifactLineIndex = lines.findIndex((line) => line.includes('__boltArtifact__'));
// Return original content if artifact line not found
if (artifactLineIndex === -1) {
return content;
}
// Check previous line for code fence
if (artifactLineIndex > 0 && lines[artifactLineIndex - 1]?.trim().match(/^```\w*$/)) {
lines[artifactLineIndex - 1] = '';
}
if (artifactLineIndex < lines.length - 1 && lines[artifactLineIndex + 1]?.trim().match(/^```$/)) {
lines[artifactLineIndex + 1] = '';
}
return lines.join('\n');
};

View File

@@ -1,8 +1,9 @@
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import React from 'react';
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
export function ImportButton(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
return (
<div className="flex flex-col items-center justify-center flex-1 p-4">
<input
@@ -59,6 +60,10 @@ export function ImportButton(importChat: ((description: string, messages: Messag
<div className="i-ph:upload-simple" />
Import Chat
</button>
<ImportFolderButton
importChat={importChat}
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
/>
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import { cubicEasingFn } from '~/utils/easings';
import { logger } from '~/utils/logger';
import { HistoryItem } from './HistoryItem';
import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
const menuVariants = {
closed: {
@@ -39,6 +40,11 @@ export function Menu() {
const [open, setOpen] = useState(false);
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
items: list,
searchFields: ['description'],
});
const loadEntries = useCallback(() => {
if (db) {
getAll(db)
@@ -115,11 +121,11 @@ export function Menu() {
initial="closed"
animate={open ? 'open' : 'closed'}
variants={menuVariants}
className="flex flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
>
<div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
<div className="p-4">
<div className="p-4 select-none">
<a
href="/"
className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
@@ -128,11 +134,26 @@ export function Menu() {
Start new chat
</a>
</div>
<div className="pl-4 pr-4 my-2">
<div className="relative w-full">
<input
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
type="search"
placeholder="Search"
onChange={handleSearchChange}
aria-label="Search chats"
/>
</div>
</div>
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
<div className="flex-1 overflow-auto pl-4 pr-5 pb-5">
{list.length === 0 && <div className="pl-2 text-bolt-elements-textTertiary">No previous conversations</div>}
{filteredList.length === 0 && (
<div className="pl-2 text-bolt-elements-textTertiary">
{list.length === 0 ? 'No previous conversations' : 'No matches found'}
</div>
)}
<DialogRoot open={dialogContent !== null}>
{binDates(list).map(({ category, items }) => (
{binDates(filteredList).map(({ category, items }) => (
<div key={category} className="mt-4 first:mt-0 space-y-1">
<div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
{category}

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { memo, useEffect, useMemo, useRef, useState } from 'react';
import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels';
import { memo, useMemo } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import {
CodeMirrorEditor,
type EditorDocument,
@@ -9,21 +9,17 @@ import {
type OnSaveCallback as OnEditorSave,
type OnScrollCallback as OnEditorScroll,
} from '~/components/editor/codemirror/CodeMirrorEditor';
import { IconButton } from '~/components/ui/IconButton';
import { PanelHeader } from '~/components/ui/PanelHeader';
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
import { shortcutEventEmitter } from '~/lib/hooks';
import type { FileMap } from '~/lib/stores/files';
import { themeStore } from '~/lib/stores/theme';
import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
import { WORK_DIR } from '~/utils/constants';
import { logger, renderLogger } from '~/utils/logger';
import { renderLogger } from '~/utils/logger';
import { isMobile } from '~/utils/mobile';
import { FileBreadcrumb } from './FileBreadcrumb';
import { FileTree } from './FileTree';
import { Terminal, type TerminalRef } from './terminal/Terminal';
import React from 'react';
import { DEFAULT_TERMINAL_SIZE, TerminalTabs } from './terminal/TerminalTabs';
import { workbenchStore } from '~/lib/stores/workbench';
interface EditorPanelProps {
files?: FileMap;
@@ -38,8 +34,6 @@ interface EditorPanelProps {
onFileReset?: () => void;
}
const MAX_TERMINALS = 3;
const DEFAULT_TERMINAL_SIZE = 25;
const DEFAULT_EDITOR_SIZE = 100 - DEFAULT_TERMINAL_SIZE;
const editorSettings: EditorSettings = { tabSize: 2 };
@@ -62,13 +56,6 @@ export const EditorPanel = memo(
const theme = useStore(themeStore);
const showTerminal = useStore(workbenchStore.showTerminal);
const terminalRefs = useRef<Array<TerminalRef | null>>([]);
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
const terminalToggledByShortcut = useRef(false);
const [activeTerminal, setActiveTerminal] = useState(0);
const [terminalCount, setTerminalCount] = useState(1);
const activeFileSegments = useMemo(() => {
if (!editorDocument) {
return undefined;
@@ -81,48 +68,6 @@ export const EditorPanel = memo(
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
}, [editorDocument, unsavedFiles]);
useEffect(() => {
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
terminalToggledByShortcut.current = true;
});
const unsubscribeFromThemeStore = themeStore.subscribe(() => {
for (const ref of Object.values(terminalRefs.current)) {
ref?.reloadStyles();
}
});
return () => {
unsubscribeFromEventEmitter();
unsubscribeFromThemeStore();
};
}, []);
useEffect(() => {
const { current: terminal } = terminalPanelRef;
if (!terminal) {
return;
}
const isCollapsed = terminal.isCollapsed();
if (!showTerminal && !isCollapsed) {
terminal.collapse();
} else if (showTerminal && isCollapsed) {
terminal.resize(DEFAULT_TERMINAL_SIZE);
}
terminalToggledByShortcut.current = false;
}, [showTerminal]);
const addTerminal = () => {
if (terminalCount < MAX_TERMINALS) {
setTerminalCount(terminalCount + 1);
setActiveTerminal(terminalCount);
}
};
return (
<PanelGroup direction="vertical">
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
@@ -181,118 +126,7 @@ export const EditorPanel = memo(
</PanelGroup>
</Panel>
<PanelResizeHandle />
<Panel
ref={terminalPanelRef}
defaultSize={showTerminal ? DEFAULT_TERMINAL_SIZE : 0}
minSize={10}
collapsible
onExpand={() => {
if (!terminalToggledByShortcut.current) {
workbenchStore.toggleTerminal(true);
}
}}
onCollapse={() => {
if (!terminalToggledByShortcut.current) {
workbenchStore.toggleTerminal(false);
}
}}
>
<div className="h-full">
<div className="bg-bolt-elements-terminals-background h-full flex flex-col">
<div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
{Array.from({ length: terminalCount + 1 }, (_, index) => {
const isActive = activeTerminal === index;
return (
<React.Fragment key={index}>
{index == 0 ? (
<button
key={index}
className={classNames(
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
{
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
isActive,
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
!isActive,
},
)}
onClick={() => setActiveTerminal(index)}
>
<div className="i-ph:terminal-window-duotone text-lg" />
Bolt Terminal
</button>
) : (
<React.Fragment>
<button
key={index}
className={classNames(
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
{
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
!isActive,
},
)}
onClick={() => setActiveTerminal(index)}
>
<div className="i-ph:terminal-window-duotone text-lg" />
Terminal {terminalCount > 1 && index}
</button>
</React.Fragment>
)}
</React.Fragment>
);
})}
{terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
<IconButton
className="ml-auto"
icon="i-ph:caret-down"
title="Close"
size="md"
onClick={() => workbenchStore.toggleTerminal(false)}
/>
</div>
{Array.from({ length: terminalCount + 1 }, (_, index) => {
const isActive = activeTerminal === index;
if (index == 0) {
logger.info('Starting bolt terminal');
return (
<Terminal
key={index}
className={classNames('h-full overflow-hidden', {
hidden: !isActive,
})}
ref={(ref) => {
terminalRefs.current.push(ref);
}}
onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
theme={theme}
/>
);
}
return (
<Terminal
key={index}
className={classNames('h-full overflow-hidden', {
hidden: !isActive,
})}
ref={(ref) => {
terminalRefs.current.push(ref);
}}
onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
theme={theme}
/>
);
})}
</div>
</div>
</Panel>
<TerminalTabs />
</PanelGroup>
);
},

View File

@@ -16,71 +16,74 @@ export interface TerminalProps {
className?: string;
theme: Theme;
readonly?: boolean;
id: string;
onTerminalReady?: (terminal: XTerm) => void;
onTerminalResize?: (cols: number, rows: number) => void;
}
export const Terminal = memo(
forwardRef<TerminalRef, TerminalProps>(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => {
const terminalElementRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<XTerm>();
forwardRef<TerminalRef, TerminalProps>(
({ className, theme, readonly, id, onTerminalReady, onTerminalResize }, ref) => {
const terminalElementRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<XTerm>();
useEffect(() => {
const element = terminalElementRef.current!;
useEffect(() => {
const element = terminalElementRef.current!;
const fitAddon = new FitAddon();
const webLinksAddon = new WebLinksAddon();
const fitAddon = new FitAddon();
const webLinksAddon = new WebLinksAddon();
const terminal = new XTerm({
cursorBlink: true,
convertEol: true,
disableStdin: readonly,
theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
fontSize: 12,
fontFamily: 'Menlo, courier-new, courier, monospace',
});
const terminal = new XTerm({
cursorBlink: true,
convertEol: true,
disableStdin: readonly,
theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
fontSize: 12,
fontFamily: 'Menlo, courier-new, courier, monospace',
});
terminalRef.current = terminal;
terminalRef.current = terminal;
terminal.loadAddon(fitAddon);
terminal.loadAddon(webLinksAddon);
terminal.open(element);
terminal.loadAddon(fitAddon);
terminal.loadAddon(webLinksAddon);
terminal.open(element);
const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
onTerminalResize?.(terminal.cols, terminal.rows);
});
const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
onTerminalResize?.(terminal.cols, terminal.rows);
});
resizeObserver.observe(element);
resizeObserver.observe(element);
logger.info('Attach terminal');
logger.debug(`Attach [${id}]`);
onTerminalReady?.(terminal);
onTerminalReady?.(terminal);
return () => {
resizeObserver.disconnect();
terminal.dispose();
};
}, []);
return () => {
resizeObserver.disconnect();
terminal.dispose();
};
}, []);
useEffect(() => {
const terminal = terminalRef.current!;
useEffect(() => {
const terminal = terminalRef.current!;
// we render a transparent cursor in case the terminal is readonly
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
// we render a transparent cursor in case the terminal is readonly
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
terminal.options.disableStdin = readonly;
}, [theme, readonly]);
terminal.options.disableStdin = readonly;
}, [theme, readonly]);
useImperativeHandle(ref, () => {
return {
reloadStyles: () => {
const terminal = terminalRef.current!;
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
},
};
}, []);
useImperativeHandle(ref, () => {
return {
reloadStyles: () => {
const terminal = terminalRef.current!;
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
},
};
}, []);
return <div className={className} ref={terminalElementRef} />;
}),
return <div className={className} ref={terminalElementRef} />;
},
),
);

View File

@@ -0,0 +1,186 @@
import { useStore } from '@nanostores/react';
import React, { memo, useEffect, useRef, useState } from 'react';
import { Panel, type ImperativePanelHandle } from 'react-resizable-panels';
import { IconButton } from '~/components/ui/IconButton';
import { shortcutEventEmitter } from '~/lib/hooks';
import { themeStore } from '~/lib/stores/theme';
import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames';
import { Terminal, type TerminalRef } from './Terminal';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('Terminal');
const MAX_TERMINALS = 3;
export const DEFAULT_TERMINAL_SIZE = 25;
export const TerminalTabs = memo(() => {
const showTerminal = useStore(workbenchStore.showTerminal);
const theme = useStore(themeStore);
const terminalRefs = useRef<Array<TerminalRef | null>>([]);
const terminalPanelRef = useRef<ImperativePanelHandle>(null);
const terminalToggledByShortcut = useRef(false);
const [activeTerminal, setActiveTerminal] = useState(0);
const [terminalCount, setTerminalCount] = useState(1);
const addTerminal = () => {
if (terminalCount < MAX_TERMINALS) {
setTerminalCount(terminalCount + 1);
setActiveTerminal(terminalCount);
}
};
useEffect(() => {
const { current: terminal } = terminalPanelRef;
if (!terminal) {
return;
}
const isCollapsed = terminal.isCollapsed();
if (!showTerminal && !isCollapsed) {
terminal.collapse();
} else if (showTerminal && isCollapsed) {
terminal.resize(DEFAULT_TERMINAL_SIZE);
}
terminalToggledByShortcut.current = false;
}, [showTerminal]);
useEffect(() => {
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
terminalToggledByShortcut.current = true;
});
const unsubscribeFromThemeStore = themeStore.subscribe(() => {
for (const ref of Object.values(terminalRefs.current)) {
ref?.reloadStyles();
}
});
return () => {
unsubscribeFromEventEmitter();
unsubscribeFromThemeStore();
};
}, []);
return (
<Panel
ref={terminalPanelRef}
defaultSize={showTerminal ? DEFAULT_TERMINAL_SIZE : 0}
minSize={10}
collapsible
onExpand={() => {
if (!terminalToggledByShortcut.current) {
workbenchStore.toggleTerminal(true);
}
}}
onCollapse={() => {
if (!terminalToggledByShortcut.current) {
workbenchStore.toggleTerminal(false);
}
}}
>
<div className="h-full">
<div className="bg-bolt-elements-terminals-background h-full flex flex-col">
<div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
{Array.from({ length: terminalCount + 1 }, (_, index) => {
const isActive = activeTerminal === index;
return (
<React.Fragment key={index}>
{index == 0 ? (
<button
key={index}
className={classNames(
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
{
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
isActive,
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
!isActive,
},
)}
onClick={() => setActiveTerminal(index)}
>
<div className="i-ph:terminal-window-duotone text-lg" />
Bolt Terminal
</button>
) : (
<React.Fragment>
<button
key={index}
className={classNames(
'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
{
'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
!isActive,
},
)}
onClick={() => setActiveTerminal(index)}
>
<div className="i-ph:terminal-window-duotone text-lg" />
Terminal {terminalCount > 1 && index}
</button>
</React.Fragment>
)}
</React.Fragment>
);
})}
{terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
<IconButton
className="ml-auto"
icon="i-ph:caret-down"
title="Close"
size="md"
onClick={() => workbenchStore.toggleTerminal(false)}
/>
</div>
{Array.from({ length: terminalCount + 1 }, (_, index) => {
const isActive = activeTerminal === index;
logger.debug(`Starting bolt terminal [${index}]`);
if (index == 0) {
return (
<Terminal
key={index}
id={`terminal_${index}`}
className={classNames('h-full overflow-hidden', {
hidden: !isActive,
})}
ref={(ref) => {
terminalRefs.current.push(ref);
}}
onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
theme={theme}
/>
);
} else {
return (
<Terminal
key={index}
id={`terminal_${index}`}
className={classNames('h-full overflow-hidden', {
hidden: !isActive,
})}
ref={(ref) => {
terminalRefs.current.push(ref);
}}
onTerminalReady={(terminal) => workbenchStore.attachTerminal(terminal)}
onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
theme={theme}
/>
);
}
})}
</div>
</div>
</Panel>
);
});

View File

@@ -43,7 +43,7 @@ export default async function handleRequest(
.read()
.then(({ done, value }) => {
if (done) {
controller.enqueue(new Uint8Array(new TextEncoder().encode(`</div></body></html>`)));
controller.enqueue(new Uint8Array(new TextEncoder().encode('</div></body></html>')));
controller.close();
return;

View File

@@ -35,6 +35,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
return env.MISTRAL_API_KEY || cloudflareEnv.MISTRAL_API_KEY;
case 'OpenAILike':
return env.OPENAI_LIKE_API_KEY || cloudflareEnv.OPENAI_LIKE_API_KEY;
case 'Together':
return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
case 'xAI':
return env.XAI_API_KEY || cloudflareEnv.XAI_API_KEY;
case 'Cohere':
@@ -48,6 +50,8 @@ export function getAPIKey(cloudflareEnv: Env, provider: string, userApiKeys?: Re
export function getBaseURL(cloudflareEnv: Env, provider: string) {
switch (provider) {
case 'Together':
return env.TOGETHER_API_BASE_URL || cloudflareEnv.TOGETHER_API_BASE_URL;
case 'OpenAILike':
return env.OPENAI_LIKE_API_BASE_URL || cloudflareEnv.OPENAI_LIKE_API_BASE_URL;
case 'LMStudio':

View File

@@ -146,6 +146,8 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re
return getGoogleModel(apiKey, model);
case 'OpenAILike':
return getOpenAILikeModel(baseURL, apiKey, model);
case 'Together':
return getOpenAILikeModel(baseURL, apiKey, model);
case 'Deepseek':
return getDeepseekModel(apiKey, model);
case 'Mistral':

View File

@@ -0,0 +1,52 @@
import { useState, useMemo, useCallback } from 'react';
import { debounce } from '~/utils/debounce';
import type { ChatHistoryItem } from '~/lib/persistence';
interface UseSearchFilterOptions {
items: ChatHistoryItem[];
searchFields?: (keyof ChatHistoryItem)[];
debounceMs?: number;
}
export function useSearchFilter({
items = [],
searchFields = ['description'],
debounceMs = 300,
}: UseSearchFilterOptions) {
const [searchQuery, setSearchQuery] = useState('');
const debouncedSetSearch = useCallback(debounce(setSearchQuery, debounceMs), []);
const handleSearchChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearch(event.target.value);
},
[debouncedSetSearch],
);
const filteredItems = useMemo(() => {
if (!searchQuery.trim()) {
return items;
}
const query = searchQuery.toLowerCase();
return items.filter((item) =>
searchFields.some((field) => {
const value = item[field];
if (typeof value === 'string') {
return value.toLowerCase().includes(query);
}
return false;
}),
);
}, [items, searchQuery, searchFields]);
return {
searchQuery,
filteredItems,
handleSearchChange,
};
}

View File

@@ -6,6 +6,11 @@ const logger = createScopedLogger('ChatHistory');
// this is used at the top level and never rejects
export async function openDatabase(): Promise<IDBDatabase | undefined> {
if (typeof indexedDB === 'undefined') {
console.error('indexedDB is not available in this environment.');
return undefined;
}
return new Promise((resolve) => {
const request = indexedDB.open('boltHistory', 1);

View File

@@ -43,7 +43,7 @@ export function useChatHistory() {
setReady(true);
if (persistenceEnabled) {
toast.error(`Chat persistence is unavailable`);
toast.error('Chat persistence is unavailable');
}
return;
@@ -63,7 +63,7 @@ export function useChatHistory() {
description.set(storedMessages.description);
chatId.set(storedMessages.id);
} else {
navigate(`/`, { replace: true });
navigate('/', { replace: true });
}
setReady(true);

View File

@@ -84,18 +84,18 @@ export class ActionRunner {
}
if (action.executed) {
return;
return; // No return value here
}
if (isStreaming && action.type !== 'file') {
return;
return; // No return value here
}
this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
this.#currentExecutionPromise = this.#currentExecutionPromise
.then(() => {
this.#executeAction(actionId, isStreaming);
return this.#executeAction(actionId, isStreaming);
})
.catch((error) => {
console.error('Action failed:', error);

View File

@@ -14,6 +14,7 @@ import { saveAs } from 'file-saver';
import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
import * as nodePath from 'node:path';
import { extractRelativePath } from '~/utils/diff';
import { description } from '~/lib/persistence';
export interface ArtifactState {
id: string;
@@ -328,6 +329,13 @@ export class WorkbenchStore {
const zip = new JSZip();
const files = this.files.get();
// Get the project name from the description input, or use a default name
const projectName = (description.value ?? 'project').toLocaleLowerCase().split(' ').join('_');
// Generate a simple 6-character hash based on the current timestamp
const timestampHash = Date.now().toString(36).slice(-6);
const uniqueProjectName = `${projectName}_${timestampHash}`;
for (const [filePath, dirent] of Object.entries(files)) {
if (dirent?.type === 'file' && !dirent.isBinary) {
const relativePath = extractRelativePath(filePath);
@@ -350,8 +358,9 @@ export class WorkbenchStore {
}
}
// Generate the zip file and save it
const content = await zip.generateAsync({ type: 'blob' });
saveAs(content, 'project.zip');
saveAs(content, `${uniqueProjectName}.zip`);
}
async syncFiles(targetHandle: FileSystemDirectoryHandle) {
@@ -369,7 +378,9 @@ export class WorkbenchStore {
}
// create or get the file
const fileHandle = await currentHandle.getFileHandle(pathSegments[pathSegments.length - 1], { create: true });
const fileHandle = await currentHandle.getFileHandle(pathSegments[pathSegments.length - 1], {
create: true,
});
// write the file content
const writable = await fileHandle.createWritable();

View File

@@ -44,7 +44,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
content:
`[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
stripIndents`
You are a professional prompt engineer specializing in crafting precise, effective prompts.
You are a professional prompt engineer specializing in crafting precise, effective prompts.
Your task is to enhance prompts by making them more specific, actionable, and effective.
I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.

View File

@@ -1,3 +1,5 @@
@use '../z-index';
[data-resize-handle] {
position: relative;
@@ -8,7 +10,7 @@
bottom: 0;
left: -6px;
right: -5px;
z-index: $zIndexMax;
z-index: z-index.$zIndexMax;
}
&[data-panel-group-direction='vertical']:after {
@@ -18,7 +20,7 @@
right: 0;
top: -5px;
bottom: -6px;
z-index: $zIndexMax;
z-index: z-index.$zIndexMax;
}
&[data-resize-handle-state='hover']:after,

View File

@@ -1,11 +1,11 @@
@import './variables.scss';
@import './z-index.scss';
@import './animations.scss';
@import './components/terminal.scss';
@import './components/resize-handle.scss';
@import './components/code.scss';
@import './components/editor.scss';
@import './components/toast.scss';
@use 'variables.scss';
@use 'z-index.scss';
@use 'animations.scss';
@use 'components/terminal.scss';
@use 'components/resize-handle.scss';
@use 'components/code.scss';
@use 'components/editor.scss';
@use 'components/toast.scss';
html,
body {

View File

@@ -7,6 +7,7 @@ export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/;
export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
export const PROMPT_COOKIE_KEY = 'cachedPrompt';
const PROVIDER_LIST: ProviderInfo[] = [
{
@@ -259,6 +260,31 @@ const PROVIDER_LIST: ProviderInfo[] = [
labelForGetApiKey: 'Get LMStudio',
icon: 'i-ph:cloud-arrow-down',
},
{
name: 'Together',
staticModels: [
{
name: 'Qwen/Qwen2.5-Coder-32B-Instruct',
label: 'Qwen/Qwen2.5-Coder-32B-Instruct',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
label: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
provider: 'Together',
maxTokenAllowed: 8000,
},
{
name: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
label: 'Mixtral 8x7B Instruct',
provider: 'Together',
maxTokenAllowed: 8192,
},
],
getApiKeyLink: 'https://api.together.xyz/settings/api-keys',
},
];
export const DEFAULT_PROVIDER = PROVIDER_LIST[0];
@@ -283,9 +309,11 @@ const getOllamaBaseUrl = () => {
};
async function getOllamaModels(): Promise<ModelInfo[]> {
if (typeof window === 'undefined') {
return [];
}
/*
* if (typeof window === 'undefined') {
* return [];
* }
*/
try {
const baseUrl = getOllamaBaseUrl();

View File

@@ -11,7 +11,7 @@ interface Logger {
setLevel: (level: DebugLevel) => void;
}
let currentLevel: DebugLevel = import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV ? 'debug' : 'info';
let currentLevel: DebugLevel = (import.meta.env.VITE_LOG_LEVEL ?? import.meta.env.DEV) ? 'debug' : 'info';
const isWorker = 'HTMLRewriter' in globalThis;
const supportsColor = !isWorker;