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

Stable additions #491

Merged
merged 25 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
604ab87
feat: add custom unique filename when doanload as zip
lassecapel Nov 17, 2024
399affd
update comment to reflect the the codeline
lassecapel Nov 17, 2024
8978ed0
use a descriptive anique filename when downloading the files to zip
lassecapel Nov 20, 2024
43839b1
[fix]: artifact actionlist rendering in chat
PuneetP16 Nov 26, 2024
651a4f8
Updated README Headings and Ollama Section
dustinwloring1988 Dec 1, 2024
cb1fd38
liniting fix
dustinwloring1988 Dec 1, 2024
88479fe
Merge pull request #9 from lassecapel/feat-add-custom-project-name
dustinwloring1988 Dec 1, 2024
416f7f9
Update constants.ts
dustinwloring1988 Dec 1, 2024
9d82637
Update docker-compose.yaml
dustinwloring1988 Dec 1, 2024
c4347cb
added collapsable chat area
dustinwloring1988 Dec 1, 2024
35107ad
Update ExamplePrompts.tsx
dustinwloring1988 Dec 1, 2024
2803f85
Merge pull request #11 from PuneetP16/fix-artifact-code-block-rendering
dustinwloring1988 Dec 1, 2024
b3d6181
Merge pull request #16 from dustinwloring1988/default-prompt-change
dustinwloring1988 Dec 1, 2024
7066bfc
Merge pull request #17 from dustinwloring1988/collapsible-model-and-p…
dustinwloring1988 Dec 1, 2024
a70c0ea
Merge pull request #18 from dustinwloring1988/pretty-up
dustinwloring1988 Dec 1, 2024
ca4bcba
Merge pull request #20 from dustinwloring1988/readme-heading-ollama-s…
dustinwloring1988 Dec 1, 2024
5a7f491
Merge pull request #15 from dustinwloring1988/artifact-code-block
dustinwloring1988 Dec 1, 2024
a79aa1f
Merge pull request #19 from dustinwloring1988/unique-name-on-download…
dustinwloring1988 Dec 1, 2024
54cb475
Merge branch 'stable-additions' into linting-fix
dustinwloring1988 Dec 1, 2024
8a6d32f
Merge pull request #21 from dustinwloring1988/linting-fix
dustinwloring1988 Dec 1, 2024
b0b617d
lint fix
dustinwloring1988 Dec 1, 2024
7574337
fixed path
dustinwloring1988 Dec 1, 2024
6aaa2f0
Update ImportFolderButton.tsx
dustinwloring1988 Dec 1, 2024
4566d9a
last test fix
dustinwloring1988 Dec 1, 2024
dcad4d3
updated packages
dustinwloring1988 Dec 1, 2024
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
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