Merge branch 'main' into github-import

This commit is contained in:
Anirban Kar 2024-12-04 17:54:47 +05:30 committed by GitHub
commit b0743e00b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 388 additions and 48 deletions

View File

@ -56,6 +56,16 @@ body:
- OS: [e.g. macOS, Windows, Linux] - OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox] - Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1] - Version: [e.g. 91.1]
- type: input
id: provider
attributes:
label: Provider Used
description: Tell us the provider you are using.
- type: input
id: model
attributes:
label: Model Used
description: Tell us the model you are using.
- type: textarea - type: textarea
id: additional id: additional
attributes: attributes:

View File

@ -16,10 +16,10 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days." stale-issue-message: "This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days."
stale-pr-message: "This pull request has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days." stale-pr-message: "This pull request has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days."
days-before-stale: 14 # Number of days before marking an issue or PR as stale days-before-stale: 10 # Number of days before marking an issue or PR as stale
days-before-close: 7 # Number of days after being marked stale before closing days-before-close: 4 # Number of days after being marked stale before closing
stale-issue-label: "stale" # Label to apply to stale issues stale-issue-label: "stale" # Label to apply to stale issues
stale-pr-label: "stale" # Label to apply to stale pull requests stale-pr-label: "stale" # Label to apply to stale pull requests
exempt-issue-labels: "pinned,important" # Issues with these labels won't be marked stale exempt-issue-labels: "pinned,important" # Issues with these labels won't be marked stale
exempt-pr-labels: "pinned,important" # PRs with these labels won't be marked stale exempt-pr-labels: "pinned,important" # PRs with these labels won't be marked stale
operations-per-run: 90 # Limits the number of actions per run to avoid API rate limits operations-per-run: 75 # Limits the number of actions per run to avoid API rate limits

View File

@ -17,7 +17,7 @@ fi
echo "Running lint..." echo "Running lint..."
if ! pnpm lint; then if ! pnpm lint; then
echo "❌ Linting failed! 'pnpm lint:check' will help you fix the easy ones." echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
echo "Once you're done, don't forget to add your beautification to the commit! 🤩" echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
echo "lint exit code: $?" echo "lint exit code: $?"
exit 1 exit 1

View File

@ -4,10 +4,13 @@
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. 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.
Check the [oTToDev Docs](https://coleam00.github.io/bolt.new-any-llm/) for more information.
## Join the community for oTToDev! ## Join the community for oTToDev!
https://thinktank.ottomator.ai https://thinktank.ottomator.ai
## Requested Additions - Feel Free to Contribute! ## Requested Additions - Feel Free to Contribute!
- ✅ OpenRouter Integration (@coleam00) - ✅ OpenRouter Integration (@coleam00)
@ -31,6 +34,7 @@ https://thinktank.ottomator.ai
- ✅ Ability to revert code to earlier version (@wonderwhy-er) - ✅ Ability to revert code to earlier version (@wonderwhy-er)
- ✅ Cohere Integration (@hasanraiyan) - ✅ Cohere Integration (@hasanraiyan)
- ✅ Dynamic model max token length (@hasanraiyan) - ✅ Dynamic model max token length (@hasanraiyan)
- ✅ Better prompt enhancing (@SujalXplores)
- ✅ Prompt caching (@SujalXplores) - ✅ Prompt caching (@SujalXplores)
- ✅ Load local projects into the app (@wonderwhy-er) - ✅ Load local projects into the app (@wonderwhy-er)
- ✅ Together Integration (@mouimet-infinisoft) - ✅ Together Integration (@mouimet-infinisoft)

View File

@ -255,6 +255,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<span>Model Settings</span> <span>Model Settings</span>
</button> </button>
</div> </div>
<div className={isModelSettingsCollapsed ? 'hidden' : ''}> <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
<ModelSelector <ModelSelector
key={provider?.name + ':' + modelList.length} key={provider?.name + ':' + modelList.length}

View File

@ -24,10 +24,11 @@ export function Header() {
<span className="i-bolt:logo-text?mask w-[46px] inline-block" /> <span className="i-bolt:logo-text?mask w-[46px] inline-block" />
</a> </a>
</div> </div>
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
<>
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary"> <span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly> <ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span> </span>
{chat.started && (
<ClientOnly> <ClientOnly>
{() => ( {() => (
<div className="mr-1"> <div className="mr-1">
@ -35,6 +36,7 @@ export function Header() {
</div> </div>
)} )}
</ClientOnly> </ClientOnly>
</>
)} )}
</header> </header>
); );

View File

@ -19,7 +19,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden"> <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
<Button <Button
active={showChat} active={showChat}
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's needed disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
onClick={() => { onClick={() => {
if (canHideChat) { if (canHideChat) {
chatStore.setKey('showChat', !showChat); chatStore.setKey('showChat', !showChat);

View File

@ -1,6 +1,9 @@
import { useParams } from '@remix-run/react';
import { classNames } from '~/utils/classNames';
import * as Dialog from '@radix-ui/react-dialog'; import * as Dialog from '@radix-ui/react-dialog';
import { type ChatHistoryItem } from '~/lib/persistence'; import { type ChatHistoryItem } from '~/lib/persistence';
import WithTooltip from '~/components/ui/Tooltip'; import WithTooltip from '~/components/ui/Tooltip';
import { useEditChatDescription } from '~/lib/hooks';
interface HistoryItemProps { interface HistoryItemProps {
item: ChatHistoryItem; item: ChatHistoryItem;
@ -10,48 +13,115 @@ interface HistoryItemProps {
} }
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) { export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
return ( const { id: urlId } = useParams();
<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"> const isActiveChat = urlId === item.urlId;
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
{item.description} const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
<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%"> useEditChatDescription({
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity"> initialDescription: item.description,
<WithTooltip tooltip="Export chat"> customChatId: item.id,
syncWithGlobalStore: isActiveChat,
});
const renderDescriptionForm = (
<form onSubmit={handleSubmit} className="flex-1 flex items-center">
<input
type="text"
className="flex-1 bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2"
autoFocus
value={currentDescription}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
<button <button
type="button" type="submit"
className="i-ph:download-simple scale-110 mr-2 hover:text-bolt-elements-item-contentAccent" className="i-ph:check scale-110 hover:text-bolt-elements-item-contentAccent"
onMouseDown={handleSubmit}
/>
</form>
);
return (
<div
className={classNames(
'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',
{ '[&&]:text-bolt-elements-textPrimary bg-bolt-elements-background-depth-3': isActiveChat },
)}
>
{editing ? (
renderDescriptionForm
) : (
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
{currentDescription}
<div
className={classNames(
'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-22 group-hover:from-99%',
{ 'from-bolt-elements-background-depth-3 w-10 ': isActiveChat },
)}
>
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity">
<ChatActionButton
toolTipContent="Export chat"
icon="i-ph:download-simple"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
exportChat(item.id); exportChat(item.id);
}} }}
title="Export chat"
/> />
</WithTooltip>
{onDuplicate && ( {onDuplicate && (
<WithTooltip tooltip="Duplicate chat"> <ChatActionButton
<button toolTipContent="Duplicate chat"
type="button" icon="i-ph:copy"
className="i-ph:copy scale-110 mr-2 hover:text-bolt-elements-item-contentAccent"
onClick={() => onDuplicate?.(item.id)} onClick={() => onDuplicate?.(item.id)}
title="Duplicate chat"
/> />
</WithTooltip>
)} )}
<ChatActionButton
toolTipContent="Rename chat"
icon="i-ph:pencil-fill"
onClick={(event) => {
event.preventDefault();
toggleEditMode();
}}
/>
<Dialog.Trigger asChild> <Dialog.Trigger asChild>
<WithTooltip tooltip="Delete chat"> <ChatActionButton
<button toolTipContent="Delete chat"
type="button" icon="i-ph:trash"
className="i-ph:trash scale-110 hover:text-bolt-elements-button-danger-text" className="[&&]:hover:text-bolt-elements-button-danger-text"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
onDelete?.(event); onDelete?.(event);
}} }}
/> />
</WithTooltip>
</Dialog.Trigger> </Dialog.Trigger>
</div> </div>
</div> </div>
</a> </a>
)}
</div> </div>
); );
} }
const ChatActionButton = ({
toolTipContent,
icon,
className,
onClick,
}: {
toolTipContent: string;
icon: string;
className?: string;
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
btnTitle?: string;
}) => {
return (
<WithTooltip tooltip={toolTipContent}>
<button
type="button"
className={`scale-110 mr-2 hover:text-bolt-elements-item-contentAccent ${icon} ${className ? className : ''}`}
onClick={onClick}
/>
</WithTooltip>
);
};

View File

@ -2,4 +2,5 @@ export * from './useMessageParser';
export * from './usePromptEnhancer'; export * from './usePromptEnhancer';
export * from './useShortcuts'; export * from './useShortcuts';
export * from './useSnapScroll'; export * from './useSnapScroll';
export * from './useEditChatDescription';
export { default } from './useViewport'; export { default } from './useViewport';

View File

@ -0,0 +1,163 @@
import { useStore } from '@nanostores/react';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import {
chatId as chatIdStore,
description as descriptionStore,
db,
updateChatDescription,
getMessages,
} from '~/lib/persistence';
interface EditChatDescriptionOptions {
initialDescription?: string;
customChatId?: string;
syncWithGlobalStore?: boolean;
}
type EditChatDescriptionHook = {
editing: boolean;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleBlur: () => Promise<void>;
handleSubmit: (event: React.FormEvent) => Promise<void>;
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => Promise<void>;
currentDescription: string;
toggleEditMode: () => void;
};
/**
* Hook to manage the state and behavior for editing chat descriptions.
*
* Offers functions to:
* - Switch between edit and view modes.
* - Manage input changes, blur, and form submission events.
* - Save updates to IndexedDB and optionally to the global application state.
*
* @param {Object} options
* @param {string} options.initialDescription - The current chat description.
* @param {string} options.customChatId - Optional ID for updating the description via the sidebar.
* @param {boolean} options.syncWithGlobalStore - Flag to indicate global description store synchronization.
* @returns {EditChatDescriptionHook} Methods and state for managing description edits.
*/
export function useEditChatDescription({
initialDescription = descriptionStore.get()!,
customChatId,
syncWithGlobalStore,
}: EditChatDescriptionOptions): EditChatDescriptionHook {
const chatIdFromStore = useStore(chatIdStore);
const [editing, setEditing] = useState(false);
const [currentDescription, setCurrentDescription] = useState(initialDescription);
const [chatId, setChatId] = useState<string>();
useEffect(() => {
setChatId(customChatId || chatIdFromStore);
}, [customChatId, chatIdFromStore]);
useEffect(() => {
setCurrentDescription(initialDescription);
}, [initialDescription]);
const toggleEditMode = useCallback(() => setEditing((prev) => !prev), []);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setCurrentDescription(e.target.value);
}, []);
const fetchLatestDescription = useCallback(async () => {
if (!db || !chatId) {
return initialDescription;
}
try {
const chat = await getMessages(db, chatId);
return chat?.description || initialDescription;
} catch (error) {
console.error('Failed to fetch latest description:', error);
return initialDescription;
}
}, [db, chatId, initialDescription]);
const handleBlur = useCallback(async () => {
const latestDescription = await fetchLatestDescription();
setCurrentDescription(latestDescription);
toggleEditMode();
}, [fetchLatestDescription, toggleEditMode]);
const isValidDescription = useCallback((desc: string): boolean => {
const trimmedDesc = desc.trim();
if (trimmedDesc === initialDescription) {
toggleEditMode();
return false; // No change, skip validation
}
const lengthValid = trimmedDesc.length > 0 && trimmedDesc.length <= 100;
const characterValid = /^[a-zA-Z0-9\s]+$/.test(trimmedDesc);
if (!lengthValid) {
toast.error('Description must be between 1 and 100 characters.');
return false;
}
if (!characterValid) {
toast.error('Description can only contain alphanumeric characters and spaces.');
return false;
}
return true;
}, []);
const handleSubmit = useCallback(
async (event: React.FormEvent) => {
event.preventDefault();
if (!isValidDescription(currentDescription)) {
return;
}
try {
if (!db) {
toast.error('Chat persistence is not available');
return;
}
if (!chatId) {
toast.error('Chat Id is not available');
return;
}
await updateChatDescription(db, chatId, currentDescription);
if (syncWithGlobalStore) {
descriptionStore.set(currentDescription);
}
toast.success('Chat description updated successfully');
} catch (error) {
toast.error('Failed to update chat description: ' + (error as Error).message);
}
toggleEditMode();
},
[currentDescription, db, chatId, initialDescription, customChatId],
);
const handleKeyDown = useCallback(
async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
await handleBlur();
}
},
[handleBlur],
);
return {
editing,
handleChange,
handleBlur,
handleSubmit,
handleKeyDown,
currentDescription,
toggleEditMode,
};
}

View File

@ -1,6 +1,68 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { description } from './useChatHistory'; import { TooltipProvider } from '@radix-ui/react-tooltip';
import WithTooltip from '~/components/ui/Tooltip';
import { useEditChatDescription } from '~/lib/hooks';
import { description as descriptionStore } from '~/lib/persistence';
export function ChatDescription() { export function ChatDescription() {
return useStore(description); const initialDescription = useStore(descriptionStore)!;
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
useEditChatDescription({
initialDescription,
syncWithGlobalStore: true,
});
if (!initialDescription) {
// doing this to prevent showing edit button until chat description is set
return null;
}
return (
<div className="flex items-center justify-center">
{editing ? (
<form onSubmit={handleSubmit} className="flex items-center justify-center">
<input
type="text"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2 w-fit"
autoFocus
value={currentDescription}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
style={{ width: `${Math.max(currentDescription.length * 8, 100)}px` }}
/>
<TooltipProvider>
<WithTooltip tooltip="Save title">
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent">
<button
type="submit"
className="i-ph:check-bold scale-110 hover:text-bolt-elements-item-contentAccent"
onMouseDown={handleSubmit}
/>
</div>
</WithTooltip>
</TooltipProvider>
</form>
) : (
<>
{currentDescription}
<TooltipProvider>
<WithTooltip tooltip="Rename chat">
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent ml-2">
<button
type="button"
className="i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent"
onClick={(event) => {
event.preventDefault();
toggleEditMode();
}}
/>
</div>
</WithTooltip>
</TooltipProvider>
</>
)}
</div>
);
} }

View File

@ -52,17 +52,23 @@ export async function setMessages(
messages: Message[], messages: Message[],
urlId?: string, urlId?: string,
description?: string, description?: string,
timestamp?: string,
): Promise<void> { ): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readwrite'); const transaction = db.transaction('chats', 'readwrite');
const store = transaction.objectStore('chats'); const store = transaction.objectStore('chats');
if (timestamp && isNaN(Date.parse(timestamp))) {
reject(new Error('Invalid timestamp'));
return;
}
const request = store.put({ const request = store.put({
id, id,
messages, messages,
urlId, urlId,
description, description,
timestamp: new Date().toISOString(), timestamp: timestamp ?? new Date().toISOString(),
}); });
request.onsuccess = () => resolve(); request.onsuccess = () => resolve();
@ -212,3 +218,17 @@ export async function createChatFromMessages(
return newUrlId; // Return the urlId instead of id for navigation return newUrlId; // Return the urlId instead of id for navigation
} }
export async function updateChatDescription(db: IDBDatabase, id: string, description: string): Promise<void> {
const chat = await getMessages(db, id);
if (!chat) {
throw new Error('Chat not found');
}
if (!description.trim()) {
throw new Error('Description cannot be empty');
}
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp);
}

View File

@ -100,6 +100,10 @@ export class ActionRunner {
.catch((error) => { .catch((error) => {
console.error('Action failed:', error); console.error('Action failed:', error);
}); });
await this.#currentExecutionPromise;
return;
} }
async #executeAction(actionId: string, isStreaming: boolean = false) { async #executeAction(actionId: string, isStreaming: boolean = false) {

View File

@ -46,6 +46,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
stripIndents` 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. 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. I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
For valid prompts: For valid prompts:
@ -55,12 +56,14 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
- Maintain the core intent - Maintain the core intent
- Ensure the prompt is self-contained - Ensure the prompt is self-contained
- Use professional language - Use professional language
For invalid or unclear prompts: For invalid or unclear prompts:
- Respond with a clear, professional guidance message - Respond with a clear, professional guidance message
- Keep responses concise and actionable - Keep responses concise and actionable
- Maintain a helpful, constructive tone - Maintain a helpful, constructive tone
- Focus on what the user should provide - Focus on what the user should provide
- Use a standard template for consistency - Use a standard template for consistency
IMPORTANT: Your response must ONLY contain the enhanced prompt text. IMPORTANT: Your response must ONLY contain the enhanced prompt text.
Do not include any explanations, metadata, or wrapper tags. Do not include any explanations, metadata, or wrapper tags.