mirror of
https://github.com/coleam00/bolt.new-any-llm
synced 2024-12-27 22:33:03 +00:00
Merge branch 'main' into docs
This commit is contained in:
commit
151fdb1a28
17
.husky/pre-commit
Normal file
17
.husky/pre-commit
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "🔍 Running pre-commit hook to check the code looks good... 🔍"
|
||||
|
||||
if ! pnpm typecheck; then
|
||||
echo "❌ Type checking failed! Please review TypeScript types."
|
||||
echo "Once you're done, don't forget to add your changes to the commit! 🚀"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! pnpm lint; then
|
||||
echo "❌ Linting failed! 'pnpm lint:check' will help you fix the easy ones."
|
||||
echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "👍 All good! Committing changes..."
|
@ -1,9 +1,6 @@
|
||||
# Contributing to Bolt.new Fork
|
||||
## DEFAULT_NUM_CTX
|
||||
# Contributing to oTToDev
|
||||
|
||||
The `DEFAULT_NUM_CTX` environment variable can be used to limit the maximum number of context values used by the qwen2.5-coder model. For example, to limit the context to 24576 values (which uses 32GB of VRAM), set `DEFAULT_NUM_CTX=24576` in your `.env.local` file.
|
||||
|
||||
First off, thank you for considering contributing to Bolt.new! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make Bolt.new a better tool for developers worldwide.
|
||||
First off, thank you for considering contributing to oTToDev! This fork aims to expand the capabilities of the original project by integrating multiple LLM providers and enhancing functionality. Every contribution helps make oTToDev a better tool for developers worldwide.
|
||||
|
||||
## 📋 Table of Contents
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
@ -56,6 +53,8 @@ We're looking for dedicated contributors to help maintain and grow this project.
|
||||
- Comment complex logic
|
||||
- Keep functions focused and small
|
||||
- Use meaningful variable names
|
||||
- Lint your code. This repo contains a pre-commit-hook that will verify your code is linted properly,
|
||||
so set up your IDE to do that for you!
|
||||
|
||||
## Development Setup
|
||||
|
||||
|
53
README.md
53
README.md
@ -29,6 +29,8 @@ https://thinktank.ottomator.ai
|
||||
- ✅ Bolt terminal to see the output of LLM run commands (@thecodacus)
|
||||
- ✅ Streaming of code output (@thecodacus)
|
||||
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
|
||||
- ✅ Cohere Integration (@hasanraiyan)
|
||||
- ✅ Dynamic model max token length (@hasanraiyan)
|
||||
- ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
|
||||
- ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
|
||||
- ⬜ **HIGH PRIORITY** - Load local projects into the app
|
||||
@ -39,8 +41,6 @@ https://thinktank.ottomator.ai
|
||||
- ⬜ Azure Open AI API Integration
|
||||
- ⬜ Perplexity Integration
|
||||
- ⬜ Vertex AI Integration
|
||||
- ✅ Cohere Integration (@hasanraiyan)
|
||||
- ✅ Dynamic model max token length (@hasanraiyan)
|
||||
- ⬜ Deploy directly to Vercel/Netlify/other similar platforms
|
||||
- ⬜ Prompt caching
|
||||
- ⬜ Better prompt enhancing
|
||||
@ -246,14 +246,55 @@ pnpm run dev
|
||||
|
||||
This will start the Remix Vite development server. You will need Google Chrome Canary to run this locally if you use Chrome! It's an easy install and a good browser for web development anyway.
|
||||
|
||||
## Tips and Tricks
|
||||
## FAQ
|
||||
|
||||
Here are some tips to get the most out of Bolt.new:
|
||||
### How do I get the best results with oTToDev?
|
||||
|
||||
- **Be specific about your stack**: If you want to use specific frameworks or libraries (like Astro, Tailwind, ShadCN, or any other popular JavaScript framework), mention them in your initial prompt to ensure Bolt scaffolds the project accordingly.
|
||||
|
||||
- **Use the enhance prompt icon**: Before sending your prompt, try clicking the 'enhance' icon to have the AI model help you refine your prompt, then edit the results before submitting.
|
||||
|
||||
- **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps Bolt understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality.
|
||||
- **Scaffold the basics first, then add features**: Make sure the basic structure of your application is in place before diving into more advanced functionality. This helps oTToDev understand the foundation of your project and ensure everything is wired up right before building out more advanced functionality.
|
||||
|
||||
- **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask Bolt to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly.
|
||||
- **Batch simple instructions**: Save time by combining simple instructions into one message. For example, you can ask oTToDev to change the color scheme, add mobile responsiveness, and restart the dev server, all in one go saving you time and reducing API credit consumption significantly.
|
||||
|
||||
### How do I contribute to oTToDev?
|
||||
|
||||
[Please check out our dedicated page for contributing to oTToDev here!](CONTRIBUTING.md)
|
||||
|
||||
### Do you plan on merging oTToDev back into the official Bolt.new repo?
|
||||
|
||||
More news coming on this coming early next month - stay tuned!
|
||||
|
||||
### What are the future plans for oTToDev?
|
||||
|
||||
[Check out our Roadmap here!](https://roadmap.sh/r/ottodev-roadmap-2ovzo)
|
||||
|
||||
Lot more updates to this roadmap coming soon!
|
||||
|
||||
### Why are there so many open issues/pull requests?
|
||||
|
||||
oTToDev was started simply to showcase how to edit an open source project and to do something cool with local LLMs on my (@ColeMedin) YouTube channel! However, it quickly
|
||||
grew into a massive community project that I am working hard to keep up with the demand of by forming a team of maintainers and getting as many people involved as I can.
|
||||
That effort is going well and all of our maintainers are ABSOLUTE rockstars, but it still takes time to organize everything so we can efficiently get through all
|
||||
the issues and PRs. But rest assured, we are working hard and even working on some partnerships behind the scenes to really help this project take off!
|
||||
|
||||
### How do local LLMs fair compared to larger models like Claude 3.5 Sonnet for oTToDev/Bolt.new?
|
||||
|
||||
As much as the gap is quickly closing between open source and massive close source models, you’re still going to get the best results with the very large models like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b. This is one of the big tasks we have at hand - figuring out how to prompt better, use agents, and improve the platform as a whole to make it work better for even the smaller local LLMs!
|
||||
|
||||
### I'm getting the error: "There was an error processing this request"
|
||||
|
||||
If you see this error within oTToDev, that is just the application telling you there is a problem at a high level, and this could mean a number of different things. To find the actual error, please check BOTH the terminal where you started the application (with Docker or pnpm) and the developer console in the browser. For most browsers, you can access the developer console by pressing F12 or right clicking anywhere in the browser and selecting “Inspect”. Then go to the “console” tab in the top right.
|
||||
|
||||
### I'm getting the error: "x-api-key header missing"
|
||||
|
||||
We have seen this error a couple times and for some reason just restarting the Docker container has fixed it. This seems to be Ollama specific. Another thing to try is try to run oTToDev with Docker or pnpm, whichever you didn’t run first. We are still on the hunt for why this happens once and a while!
|
||||
|
||||
### I'm getting a blank preview when oTToDev runs my app!
|
||||
|
||||
We promise you that we are constantly testing new PRs coming into oTToDev and the preview is core functionality, so the application is not broken! When you get a blank preview or don’t get a preview, this is generally because the LLM hallucinated bad code or incorrect commands. We are working on making this more transparent so it is obvious. Sometimes the error will appear in developer console too so check that as well.
|
||||
|
||||
### Everything works but the results are bad
|
||||
|
||||
This goes to the point above about how local LLMs are getting very powerful but you still are going to see better (sometimes much better) results with the largest LLMs like GPT-4o, Claude 3.5 Sonnet, and DeepSeek Coder V2 236b. If you are using smaller LLMs like Qwen-2.5-Coder, consider it more experimental and educational at this point. It can build smaller applications really well, which is super impressive for a local LLM, but for larger scale applications you want to use the larger LLMs still!
|
||||
|
@ -3,7 +3,7 @@
|
||||
* Preventing TS checks with files presented in the video for a better presentation.
|
||||
*/
|
||||
import type { Message } from 'ai';
|
||||
import React, { type RefCallback, useEffect } from 'react';
|
||||
import React, { type RefCallback, useEffect, useState } from 'react';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { Menu } from '~/components/sidebar/Menu.client';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
@ -12,23 +12,15 @@ import { classNames } from '~/utils/classNames';
|
||||
import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
|
||||
import { Messages } from './Messages.client';
|
||||
import { SendButton } from './SendButton.client';
|
||||
import { useState } from 'react';
|
||||
import { APIKeyManager } from './APIKeyManager';
|
||||
import Cookies from 'js-cookie';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
import styles from './BaseChat.module.scss';
|
||||
import type { ProviderInfo } from '~/utils/types';
|
||||
|
||||
const EXAMPLE_PROMPTS = [
|
||||
{ text: 'Build a todo app in React using Tailwind' },
|
||||
{ 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?' },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const providerList = PROVIDER_LIST;
|
||||
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
|
||||
import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
|
||||
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
||||
|
||||
// @ts-ignore TODO: Introduce proper types
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -79,6 +71,7 @@ interface BaseChatProps {
|
||||
chatStarted?: boolean;
|
||||
isStreaming?: boolean;
|
||||
messages?: Message[];
|
||||
description?: string;
|
||||
enhancingPrompt?: boolean;
|
||||
promptEnhanced?: boolean;
|
||||
input?: string;
|
||||
@ -90,6 +83,8 @@ interface BaseChatProps {
|
||||
sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
|
||||
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
enhancePrompt?: () => void;
|
||||
importChat?: (description: string, messages: Message[]) => Promise<void>;
|
||||
exportChat?: () => void;
|
||||
}
|
||||
|
||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
@ -113,6 +108,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
handleInputChange,
|
||||
enhancePrompt,
|
||||
handleStop,
|
||||
importChat,
|
||||
exportChat,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@ -161,7 +158,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
const baseChat = (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
@ -297,6 +294,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
|
||||
</div>
|
||||
{input.length > 3 ? (
|
||||
<div className="text-xs text-bolt-elements-textTertiary">
|
||||
@ -309,30 +307,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!chatStarted && (
|
||||
<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]">
|
||||
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
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"
|
||||
>
|
||||
{examplePrompt.text}
|
||||
<div className="i-ph:arrow-bend-down-left" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!chatStarted && ImportButtons(importChat)}
|
||||
{!chatStarted && ExamplePrompts(sendMessage)}
|
||||
</div>
|
||||
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <Tooltip.Provider delayDuration={200}>{baseChat}</Tooltip.Provider>;
|
||||
},
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ import { useAnimate } from 'framer-motion';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
||||
import { useChatHistory } from '~/lib/persistence';
|
||||
import { description, useChatHistory } from '~/lib/persistence';
|
||||
import { chatStore } from '~/lib/stores/chat';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { fileModificationsToHTML } from '~/utils/diff';
|
||||
@ -30,11 +30,20 @@ const logger = createScopedLogger('Chat');
|
||||
export function Chat() {
|
||||
renderLogger.trace('Chat');
|
||||
|
||||
const { ready, initialMessages, storeMessageHistory } = useChatHistory();
|
||||
const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
|
||||
const title = useStore(description);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ready && <ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}
|
||||
{ready && (
|
||||
<ChatImpl
|
||||
description={title}
|
||||
initialMessages={initialMessages}
|
||||
exportChat={exportChat}
|
||||
storeMessageHistory={storeMessageHistory}
|
||||
importChat={importChat}
|
||||
/>
|
||||
)}
|
||||
<ToastContainer
|
||||
closeButton={({ closeToast }) => {
|
||||
return (
|
||||
@ -69,9 +78,13 @@ export function Chat() {
|
||||
interface ChatProps {
|
||||
initialMessages: Message[];
|
||||
storeMessageHistory: (messages: Message[]) => Promise<void>;
|
||||
importChat: (description: string, messages: Message[]) => Promise<void>;
|
||||
exportChat: () => void;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProps) => {
|
||||
export const ChatImpl = memo(
|
||||
({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
|
||||
useShortcuts();
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@ -257,6 +270,9 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
||||
scrollRef={scrollRef}
|
||||
handleInputChange={handleInputChange}
|
||||
handleStop={abort}
|
||||
description={description}
|
||||
importChat={importChat}
|
||||
exportChat={exportChat}
|
||||
messages={messages.map((message, i) => {
|
||||
if (message.role === 'user') {
|
||||
return message;
|
||||
@ -281,4 +297,5 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
32
app/components/chat/ExamplePrompts.tsx
Normal file
32
app/components/chat/ExamplePrompts.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
const EXAMPLE_PROMPTS = [
|
||||
{ text: 'Build a todo app in React using Tailwind' },
|
||||
{ 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?' },
|
||||
];
|
||||
|
||||
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]">
|
||||
{EXAMPLE_PROMPTS.map((examplePrompt, index: number) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
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"
|
||||
>
|
||||
{examplePrompt.text}
|
||||
<div className="i-ph:arrow-bend-down-left" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
164
app/components/chat/ImportFolderButton.tsx
Normal file
164
app/components/chat/ImportFolderButton.tsx
Normal 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:folder-simple-upload" />
|
||||
Import Folder
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,11 +3,11 @@ import React from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { AssistantMessage } from './AssistantMessage';
|
||||
import { UserMessage } from './UserMessage';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import { useLocation } from '@remix-run/react';
|
||||
import { db, chatId } from '~/lib/persistence/useChatHistory';
|
||||
import { forkChat } from '~/lib/persistence/db';
|
||||
import { toast } from 'react-toastify';
|
||||
import WithTooltip from '~/components/ui/Tooltip';
|
||||
|
||||
interface MessagesProps {
|
||||
id?: string;
|
||||
@ -41,7 +41,6 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<div id={id} ref={ref} className={props.className}>
|
||||
{messages.length > 0
|
||||
? messages.map((message, index) => {
|
||||
@ -70,8 +69,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
</div>
|
||||
{!isUserMessage && (
|
||||
<div className="flex gap-2 flex-col lg:flex-row">
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<WithTooltip tooltip="Revert to this message">
|
||||
{messageId && (
|
||||
<button
|
||||
onClick={() => handleRewind(messageId)}
|
||||
@ -82,21 +80,9 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
||||
sideOffset={5}
|
||||
style={{ zIndex: 1000 }}
|
||||
>
|
||||
Revert to this message
|
||||
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</WithTooltip>
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<WithTooltip tooltip="Fork chat from this message">
|
||||
<button
|
||||
onClick={() => handleFork(messageId)}
|
||||
key="i-ph:git-fork"
|
||||
@ -105,18 +91,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
||||
)}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
||||
sideOffset={5}
|
||||
style={{ zIndex: 1000 }}
|
||||
>
|
||||
Fork chat from this message
|
||||
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</WithTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -127,6 +102,5 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
<div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
});
|
||||
|
13
app/components/chat/chatExportAndImport/ExportChatButton.tsx
Normal file
13
app/components/chat/chatExportAndImport/ExportChatButton.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import WithTooltip from '~/components/ui/Tooltip';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import React from 'react';
|
||||
|
||||
export const ExportChatButton = ({ exportChat }: { exportChat?: () => void }) => {
|
||||
return (
|
||||
<WithTooltip tooltip="Export Chat">
|
||||
<IconButton title="Export Chat" onClick={() => exportChat?.()}>
|
||||
<div className="i-ph:download-simple text-xl"></div>
|
||||
</IconButton>
|
||||
</WithTooltip>
|
||||
);
|
||||
};
|
71
app/components/chat/chatExportAndImport/ImportButtons.tsx
Normal file
71
app/components/chat/chatExportAndImport/ImportButtons.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import React from 'react';
|
||||
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
||||
|
||||
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
|
||||
type="file"
|
||||
id="chat-import"
|
||||
className="hidden"
|
||||
accept=".json"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (file && importChat) {
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const content = e.target?.result as string;
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (!Array.isArray(data.messages)) {
|
||||
toast.error('Invalid chat file format');
|
||||
}
|
||||
|
||||
await importChat(data.description, data.messages);
|
||||
toast.success('Chat imported successfully');
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error('Failed to parse chat file: ' + error.message);
|
||||
} else {
|
||||
toast.error('Failed to parse chat file');
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.onerror = () => toast.error('Failed to read chat file');
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to import chat');
|
||||
}
|
||||
e.target.value = ''; // Reset file input
|
||||
} else {
|
||||
toast.error('Something went wrong');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-4 max-w-2xl text-center">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const input = document.getElementById('chat-import');
|
||||
input?.click();
|
||||
}}
|
||||
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 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>
|
||||
);
|
||||
}
|
@ -1,70 +1,55 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type ChatHistoryItem } from '~/lib/persistence';
|
||||
import WithTooltip from '~/components/ui/Tooltip';
|
||||
|
||||
interface HistoryItemProps {
|
||||
item: ChatHistoryItem;
|
||||
onDelete?: (event: React.UIEvent) => void;
|
||||
onDuplicate?: (id: string) => void;
|
||||
exportChat: (id?: string) => void;
|
||||
}
|
||||
|
||||
export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const hoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
function mouseEnter() {
|
||||
setHovering(true);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
setHovering(false);
|
||||
}
|
||||
|
||||
hoverRef.current?.addEventListener('mouseenter', mouseEnter);
|
||||
hoverRef.current?.addEventListener('mouseleave', mouseLeave);
|
||||
|
||||
return () => {
|
||||
hoverRef.current?.removeEventListener('mouseenter', mouseEnter);
|
||||
hoverRef.current?.removeEventListener('mouseleave', mouseLeave);
|
||||
};
|
||||
}, []);
|
||||
|
||||
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
|
||||
return (
|
||||
<div
|
||||
ref={hoverRef}
|
||||
className="group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1"
|
||||
>
|
||||
<div className="group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1">
|
||||
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
|
||||
{item.description}
|
||||
<div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
|
||||
{hovering && (
|
||||
<div className="flex items-center p-1 text-bolt-elements-textSecondary">
|
||||
{onDuplicate && (
|
||||
<div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-99%">
|
||||
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<WithTooltip tooltip="Export chat">
|
||||
<button
|
||||
className="i-ph:copy scale-110 mr-2"
|
||||
type="button"
|
||||
className="i-ph:download-simple scale-110 mr-2 hover:text-bolt-elements-item-contentAccent"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
exportChat(item.id);
|
||||
}}
|
||||
title="Export chat"
|
||||
/>
|
||||
</WithTooltip>
|
||||
{onDuplicate && (
|
||||
<WithTooltip tooltip="Duplicate chat">
|
||||
<button
|
||||
type="button"
|
||||
className="i-ph:copy scale-110 mr-2 hover:text-bolt-elements-item-contentAccent"
|
||||
onClick={() => onDuplicate?.(item.id)}
|
||||
title="Duplicate chat"
|
||||
/>
|
||||
</WithTooltip>
|
||||
)}
|
||||
<Dialog.Trigger asChild>
|
||||
<WithTooltip tooltip="Delete chat">
|
||||
<button
|
||||
className="i-ph:trash scale-110"
|
||||
type="button"
|
||||
className="i-ph:trash scale-110 hover:text-bolt-elements-button-danger-text"
|
||||
onClick={(event) => {
|
||||
// we prevent the default so we don't trigger the anchor above
|
||||
event.preventDefault();
|
||||
onDelete?.(event);
|
||||
}}
|
||||
/>
|
||||
</WithTooltip>
|
||||
</Dialog.Trigger>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@ const menuVariants = {
|
||||
type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
|
||||
|
||||
export function Menu() {
|
||||
const { duplicateCurrentChat } = useChatHistory();
|
||||
const { duplicateCurrentChat, exportChat } = useChatHistory();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [list, setList] = useState<ChatHistoryItem[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -101,7 +101,6 @@ export function Menu() {
|
||||
|
||||
const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
|
||||
event.preventDefault();
|
||||
|
||||
setDialogContent({ type: 'delete', item });
|
||||
};
|
||||
|
||||
@ -130,7 +129,7 @@ export function Menu() {
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
|
||||
<div className="flex-1 overflow-scroll pl-4 pr-5 pb-5">
|
||||
<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>}
|
||||
<DialogRoot open={dialogContent !== null}>
|
||||
{binDates(list).map(({ category, items }) => (
|
||||
@ -142,6 +141,7 @@ export function Menu() {
|
||||
<HistoryItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
exportChat={exportChat}
|
||||
onDelete={(event) => handleDeleteClick(event, item)}
|
||||
onDuplicate={() => handleDuplicate(item.id)}
|
||||
/>
|
||||
|
73
app/components/ui/Tooltip.tsx
Normal file
73
app/components/ui/Tooltip.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
interface TooltipProps {
|
||||
tooltip: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
sideOffset?: number;
|
||||
className?: string;
|
||||
arrowClassName?: string;
|
||||
tooltipStyle?: React.CSSProperties;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
maxWidth?: number;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const WithTooltip = ({
|
||||
tooltip,
|
||||
children,
|
||||
sideOffset = 5,
|
||||
className = '',
|
||||
arrowClassName = '',
|
||||
tooltipStyle = {},
|
||||
position = 'top',
|
||||
maxWidth = 250,
|
||||
delay = 0,
|
||||
}: TooltipProps) => {
|
||||
return (
|
||||
<Tooltip.Root delayDuration={delay}>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side={position}
|
||||
className={`
|
||||
z-[2000]
|
||||
px-2.5
|
||||
py-1.5
|
||||
max-h-[300px]
|
||||
select-none
|
||||
rounded-md
|
||||
bg-bolt-elements-background-depth-3
|
||||
text-bolt-elements-textPrimary
|
||||
text-sm
|
||||
leading-tight
|
||||
shadow-lg
|
||||
animate-in
|
||||
fade-in-0
|
||||
zoom-in-95
|
||||
data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0
|
||||
data-[state=closed]:zoom-out-95
|
||||
${className}
|
||||
`}
|
||||
sideOffset={sideOffset}
|
||||
style={{
|
||||
maxWidth,
|
||||
...tooltipStyle,
|
||||
}}
|
||||
>
|
||||
<div className="break-words">{tooltip}</div>
|
||||
<Tooltip.Arrow
|
||||
className={`
|
||||
fill-bolt-elements-background-depth-3
|
||||
${arrowClassName}
|
||||
`}
|
||||
width={12}
|
||||
height={6}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithTooltip;
|
@ -1,11 +1,11 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck – TODO: Provider proper types
|
||||
|
||||
import { streamText as _streamText, convertToCoreMessages } from 'ai';
|
||||
import { convertToCoreMessages, streamText as _streamText } from 'ai';
|
||||
import { getModel } from '~/lib/.server/llm/model';
|
||||
import { MAX_TOKENS } from './constants';
|
||||
import { getSystemPrompt } from './prompts';
|
||||
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_LIST, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants';
|
||||
|
||||
interface ToolResult<Name extends string, Args, Result> {
|
||||
toolCallId: string;
|
||||
|
@ -176,14 +176,7 @@ export async function forkChat(db: IDBDatabase, chatId: string, messageId: strin
|
||||
// Get messages up to and including the selected message
|
||||
const messages = chat.messages.slice(0, messageIndex + 1);
|
||||
|
||||
// Generate new IDs
|
||||
const newId = await getNextId(db);
|
||||
const urlId = await getUrlId(db, newId);
|
||||
|
||||
// Create the forked chat
|
||||
await setMessages(db, newId, messages, urlId, chat.description ? `${chat.description} (fork)` : 'Forked chat');
|
||||
|
||||
return urlId;
|
||||
return createChatFromMessages(db, chat.description ? `${chat.description} (fork)` : 'Forked chat', messages);
|
||||
}
|
||||
|
||||
export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
|
||||
@ -193,15 +186,23 @@ export async function duplicateChat(db: IDBDatabase, id: string): Promise<string
|
||||
throw new Error('Chat not found');
|
||||
}
|
||||
|
||||
return createChatFromMessages(db, `${chat.description || 'Chat'} (copy)`, chat.messages);
|
||||
}
|
||||
|
||||
export async function createChatFromMessages(
|
||||
db: IDBDatabase,
|
||||
description: string,
|
||||
messages: Message[],
|
||||
): Promise<string> {
|
||||
const newId = await getNextId(db);
|
||||
const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
|
||||
|
||||
await setMessages(
|
||||
db,
|
||||
newId,
|
||||
chat.messages,
|
||||
messages,
|
||||
newUrlId, // Use the new urlId
|
||||
`${chat.description || 'Chat'} (copy)`,
|
||||
description,
|
||||
);
|
||||
|
||||
return newUrlId; // Return the urlId instead of id for navigation
|
||||
|
@ -4,7 +4,15 @@ import { atom } from 'nanostores';
|
||||
import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { getMessages, getNextId, getUrlId, openDatabase, setMessages, duplicateChat } from './db';
|
||||
import {
|
||||
getMessages,
|
||||
getNextId,
|
||||
getUrlId,
|
||||
openDatabase,
|
||||
setMessages,
|
||||
duplicateChat,
|
||||
createChatFromMessages,
|
||||
} from './db';
|
||||
|
||||
export interface ChatHistoryItem {
|
||||
id: string;
|
||||
@ -113,6 +121,45 @@ export function useChatHistory() {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
importChat: async (description: string, messages: Message[]) => {
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newId = await createChatFromMessages(db, description, messages);
|
||||
window.location.href = `/chat/${newId}`;
|
||||
toast.success('Chat imported successfully');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error('Failed to import chat: ' + error.message);
|
||||
} else {
|
||||
toast.error('Failed to import chat');
|
||||
}
|
||||
}
|
||||
},
|
||||
exportChat: async (id = urlId) => {
|
||||
if (!db || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = await getMessages(db, id);
|
||||
const chatData = {
|
||||
messages: chat.messages,
|
||||
description: chat.description,
|
||||
exportDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `chat-${new Date().toISOString()}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,14 +93,13 @@ export class ActionRunner {
|
||||
|
||||
this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return (this.#currentExecutionPromise = this.#currentExecutionPromise
|
||||
this.#currentExecutionPromise = this.#currentExecutionPromise
|
||||
.then(() => {
|
||||
this.#executeAction(actionId, isStreaming);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Action failed:', error);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async #executeAction(actionId: string, isStreaming: boolean = false) {
|
||||
|
@ -283,6 +283,10 @@ const getOllamaBaseUrl = () => {
|
||||
};
|
||||
|
||||
async function getOllamaModels(): Promise<ModelInfo[]> {
|
||||
if (typeof window === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = getOllamaBaseUrl();
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
@ -294,8 +298,8 @@ async function getOllamaModels(): Promise<ModelInfo[]> {
|
||||
provider: 'Ollama',
|
||||
maxTokenAllowed: 8000,
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error('Error getting Ollama models:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -321,8 +325,8 @@ async function getOpenAILikeModels(): Promise<ModelInfo[]> {
|
||||
label: model.id,
|
||||
provider: 'OpenAILike',
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error('Error getting OpenAILike models:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -361,6 +365,10 @@ async function getOpenRouterModels(): Promise<ModelInfo[]> {
|
||||
}
|
||||
|
||||
async function getLMStudioModels(): Promise<ModelInfo[]> {
|
||||
if (typeof window === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = import.meta.env.LMSTUDIO_API_BASE_URL || 'http://localhost:1234';
|
||||
const response = await fetch(`${baseUrl}/v1/models`);
|
||||
@ -371,8 +379,8 @@ async function getLMStudioModels(): Promise<ModelInfo[]> {
|
||||
label: model.id,
|
||||
provider: 'LMStudio',
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error('Error getting LMStudio models:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,8 @@
|
||||
"dockerbuild": "docker build -t bolt-ai:development -t bolt-ai:latest --target bolt-ai-development .",
|
||||
"typecheck": "tsc",
|
||||
"typegen": "wrangler types",
|
||||
"preview": "pnpm run build && pnpm run start"
|
||||
"preview": "pnpm run build && pnpm run start",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
@ -70,6 +71,7 @@
|
||||
"diff": "^5.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.2.12",
|
||||
"ignore": "^6.0.2",
|
||||
"isbot": "^4.1.0",
|
||||
"istextorbinary": "^9.5.0",
|
||||
"jose": "^5.6.3",
|
||||
@ -101,6 +103,7 @@
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"fast-glob": "^3.3.2",
|
||||
"husky": "9.1.7",
|
||||
"is-ci": "^3.0.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"prettier": "^3.3.2",
|
||||
|
@ -143,6 +143,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: ^11.2.12
|
||||
version: 11.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
ignore:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
isbot:
|
||||
specifier: ^4.1.0
|
||||
version: 4.4.0
|
||||
@ -231,6 +234,9 @@ importers:
|
||||
fast-glob:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
husky:
|
||||
specifier: 9.1.7
|
||||
version: 9.1.7
|
||||
is-ci:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@ -2482,7 +2488,7 @@ packages:
|
||||
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
|
||||
|
||||
bytes@3.0.0:
|
||||
resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
|
||||
resolution: {integrity: sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
bytes@3.1.2:
|
||||
@ -3382,6 +3388,11 @@ packages:
|
||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
husky@9.1.7:
|
||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3399,6 +3410,10 @@ packages:
|
||||
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
ignore@6.0.2:
|
||||
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
@ -9278,6 +9293,8 @@ snapshots:
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
husky@9.1.7: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@ -9290,6 +9307,8 @@ snapshots:
|
||||
|
||||
ignore@5.3.1: {}
|
||||
|
||||
ignore@6.0.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immutable@4.3.7: {}
|
||||
|
Loading…
Reference in New Issue
Block a user