Skip to content

Commit

Permalink
Merge pull request #491 from dustinwloring1988/stable-additions
Browse files Browse the repository at this point in the history
Stable additions
  • Loading branch information
coleam00 authored Dec 1, 2024
2 parents eb76765 + dcad4d3 commit 2120aea
Show file tree
Hide file tree
Showing 15 changed files with 28,537 additions and 2,858 deletions.
38 changes: 10 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

This fork of Bolt.new (oTToDev) allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.

Join the community for oTToDev!
## Join the community for oTToDev!

https://thinktank.ottomator.ai

# Requested Additions to this Fork - Feel Free to Contribute!!
## Requested Additions to this Fork - Feel Free to Contribute!!

- ✅ OpenRouter Integration (@coleam00)
- ✅ Gemini Integration (@jonathands)
Expand Down Expand Up @@ -49,7 +49,7 @@ https://thinktank.ottomator.ai
- ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
- ⬜ Voice prompting

# Bolt.new: AI-Powered Full-Stack Web Development in the Browser
## Bolt.new: AI-Powered Full-Stack Web Development in the Browser

Bolt.new is an AI-powered web development agent that allows you to prompt, run, edit, and deploy full-stack applications directly from your browser—no local setup required. If you're here to build your own AI-powered web dev agent using the Bolt open source codebase, [click here to get started!](./CONTRIBUTING.md)

Expand Down Expand Up @@ -124,6 +124,13 @@ Optionally, you can set the debug level:
VITE_LOG_LEVEL=debug
```

And if using Ollama set the DEFAULT_NUM_CTX, the example below uses 8K context and ollama running on localhost port 11434:

```
OLLAMA_API_BASE_URL=http://localhost:11434
DEFAULT_NUM_CTX=8192
```

**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.

## Run with Docker
Expand Down Expand Up @@ -192,31 +199,6 @@ sudo npm install -g pnpm
pnpm run dev
```

## Super Important Note on Running Ollama Models

Ollama models by default only have 2048 tokens for their context window. Even for large models that can easily handle way more.
This is not a large enough window to handle the Bolt.new/oTToDev prompt! You have to create a version of any model you want
to use where you specify a larger context window. Luckily it's super easy to do that.

All you have to do is:

- Create a file called "Modelfile" (no file extension) anywhere on your computer
- Put in the two lines:

```
FROM [Ollama model ID such as qwen2.5-coder:7b]
PARAMETER num_ctx 32768
```

- Run the command:

```
ollama create -f Modelfile [your new model ID, can be whatever you want (example: qwen2.5-coder-extra-ctx:7b)]
```

Now you have a new Ollama model that isn't heavily limited in the context length like Ollama models are by default for some reason.
You'll see this new model in the list of Ollama models along with all the others you pulled!

## Adding New LLMs:

To make new LLMs available to use in this version of Bolt.new, head on over to `app/utils/constants.ts` and find the constant MODEL_LIST. Each element in this array is an object that has the model ID for the name (get this from the provider's API documentation), a label for the frontend model dropdown, and the provider.
Expand Down
57 changes: 38 additions & 19 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -199,30 +200,48 @@ 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}
/>
<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(
Expand Down
2 changes: 1 addition & 1 deletion app/components/chat/ExamplePrompts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 }) {
Expand Down
2 changes: 1 addition & 1 deletion app/components/chat/ImportFolderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ ${fileArtifacts.join('\n\n')}
}}
className={className}
>
<div className="i-ph:folder-simple-upload" />
<div className="i-ph:upload-simple" />
Import Folder
</button>
</>
Expand Down
48 changes: 48 additions & 0 deletions app/components/chat/Markdown.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
46 changes: 45 additions & 1 deletion app/components/chat/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
};
5 changes: 5 additions & 0 deletions app/lib/persistence/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 2 additions & 3 deletions app/lib/runtime/action-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ 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 });
Expand All @@ -100,7 +100,6 @@ export class ActionRunner {
.catch((error) => {
console.error('Action failed:', error);
});
return this.#currentExecutionPromise;
}

async #executeAction(actionId: string, isStreaming: boolean = false) {
Expand Down
11 changes: 10 additions & 1 deletion app/lib/stores/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
8 changes: 5 additions & 3 deletions app/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,11 @@ const getOllamaBaseUrl = () => {
};

async function getOllamaModels(): Promise<ModelInfo[]> {
//if (typeof window === 'undefined') {
//return [];
//}
/*
* if (typeof window === 'undefined') {
* return [];
* }
*/

try {
const baseUrl = getOllamaBaseUrl();
Expand Down
2 changes: 1 addition & 1 deletion app/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 2120aea

Please sign in to comment.