diff --git a/README.md b/README.md index 0127dd0..c51aaa6 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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) @@ -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 @@ -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. diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index dc6aecc..be44ef7 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -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( const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; const [apiKeys, setApiKeys] = useState>({}); const [modelList, setModelList] = useState(MODEL_LIST); + const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false); useEffect(() => { // Load API keys from cookies on component mount @@ -199,30 +200,48 @@ export const BaseChat = React.forwardRef(
- +
+
+ +
- {provider && ( - updateApiKey(provider.name, key)} - /> - )} +
+ + {provider && ( + updateApiKey(provider.name, key)} + /> + )} +
+
{ + it('should remove code fences around artifact element', () => { + const input = "```xml\n
\n```"; + const expected = "\n
\n"; + expect(stripCodeFenceFromArtifact(input)).toBe(expected); + }); + + it('should handle code fence with language specification', () => { + const input = "```typescript\n
\n```"; + const expected = "\n
\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 = "
"; + expect(stripCodeFenceFromArtifact(input)).toBe(input); + }); + + it('should handle multiple artifacts but only remove fences around them', () => { + const input = [ + 'Some text', + '```typescript', + "
", + '```', + '```', + 'regular code', + '```', + ].join('\n'); + + const expected = ['Some text', '', "
", '', '```', 'regular code', '```'].join( + '\n', + ); + + expect(stripCodeFenceFromArtifact(input)).toBe(expected); + }); +}); diff --git a/app/components/chat/Markdown.tsx b/app/components/chat/Markdown.tsx index a91df43..07b6a67 100644 --- a/app/components/chat/Markdown.tsx +++ b/app/components/chat/Markdown.tsx @@ -68,7 +68,51 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false remarkPlugins={remarkPlugins(limitedMarkdown)} rehypePlugins={rehypePlugins(html)} > - {children} + {stripCodeFenceFromArtifact(children)} ); }); + +/** + * 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
\n```"; + * stripCodeFenceFromArtifact(input); + * // Returns: "\n
\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'); +}; diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index cbb3f8a..75e328a 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -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 '../persistence'; export interface ArtifactState { id: string; @@ -168,6 +169,7 @@ export class WorkbenchStore { this.#editorStore.setSelectedFile(filePath); } + async saveFile(filePath: string) { const documents = this.#editorStore.documents.get(); const document = documents[filePath]; @@ -327,6 +329,12 @@ export class WorkbenchStore { async downloadZip() { 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) { @@ -349,9 +357,10 @@ 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) { diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 96db70f..54b78c4 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -283,7 +283,7 @@ const getOllamaBaseUrl = () => { }; async function getOllamaModels(): Promise { - /* + /* * if (typeof window === 'undefined') { * return []; * } diff --git a/docker-compose.yaml b/docker-compose.yaml index 4a3cc0a..f2412fc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,5 @@ services: - bolt-ai: + app-prod: image: bolt-ai:production build: context: . @@ -24,12 +24,12 @@ services: - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768} - RUNNING_IN_DOCKER=true extra_hosts: - - "host.docker.internal:host-gateway" + - "host.docker.internal:host-gateway" command: pnpm run dockerstart profiles: - - production # This service only runs in the production profile + - production - bolt-ai-dev: + app-dev: image: bolt-ai:development build: target: bolt-ai-development @@ -39,7 +39,7 @@ services: - VITE_HMR_HOST=localhost - VITE_HMR_PORT=5173 - CHOKIDAR_USEPOLLING=true - - WATCHPACK_POLLING=true + - WATCHPACK_POLLING=true - PORT=5173 - GROQ_API_KEY=${GROQ_API_KEY} - HuggingFace_API_KEY=${HuggingFace_API_KEY} @@ -52,7 +52,7 @@ services: - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768} - RUNNING_IN_DOCKER=true extra_hosts: - - "host.docker.internal:host-gateway" + - "host.docker.internal:host-gateway" volumes: - type: bind source: . @@ -60,6 +60,6 @@ services: consistency: cached - /app/node_modules ports: - - "5173:5173" # Same port, no conflict as only one runs at a time + - "5173:5173" command: pnpm run dev --host 0.0.0.0 - profiles: ["development", "default"] # Make development the default profile + profiles: ["development", "default"]