Merge branch 'main' into github-import
Browse files- .github/ISSUE_TEMPLATE/bug_report.yml +10 -0
- .github/workflows/stale.yml +3 -3
- .husky/pre-commit +1 -1
- README.md +4 -0
- app/components/chat/BaseChat.tsx +1 -0
- app/components/header/Header.tsx +13 -11
- app/components/header/HeaderActionButtons.client.tsx +1 -1
- app/components/sidebar/HistoryItem.tsx +98 -28
- app/lib/hooks/index.ts +1 -0
- app/lib/hooks/useEditChatDescription.ts +163 -0
- app/lib/persistence/ChatDescription.client.tsx +64 -2
- app/lib/persistence/db.ts +21 -1
- app/lib/runtime/action-runner.ts +4 -0
- app/routes/api.enhancer.ts +4 -1
.github/ISSUE_TEMPLATE/bug_report.yml
CHANGED
@@ -56,6 +56,16 @@ body:
|
|
56 |
- OS: [e.g. macOS, Windows, Linux]
|
57 |
- Browser: [e.g. Chrome, Safari, Firefox]
|
58 |
- Version: [e.g. 91.1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
- type: textarea
|
60 |
id: additional
|
61 |
attributes:
|
|
|
56 |
- OS: [e.g. macOS, Windows, Linux]
|
57 |
- Browser: [e.g. Chrome, Safari, Firefox]
|
58 |
- Version: [e.g. 91.1]
|
59 |
+
- type: input
|
60 |
+
id: provider
|
61 |
+
attributes:
|
62 |
+
label: Provider Used
|
63 |
+
description: Tell us the provider you are using.
|
64 |
+
- type: input
|
65 |
+
id: model
|
66 |
+
attributes:
|
67 |
+
label: Model Used
|
68 |
+
description: Tell us the model you are using.
|
69 |
- type: textarea
|
70 |
id: additional
|
71 |
attributes:
|
.github/workflows/stale.yml
CHANGED
@@ -16,10 +16,10 @@ jobs:
|
|
16 |
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
17 |
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."
|
18 |
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."
|
19 |
-
days-before-stale:
|
20 |
-
days-before-close:
|
21 |
stale-issue-label: "stale" # Label to apply to stale issues
|
22 |
stale-pr-label: "stale" # Label to apply to stale pull requests
|
23 |
exempt-issue-labels: "pinned,important" # Issues with these labels won't be marked stale
|
24 |
exempt-pr-labels: "pinned,important" # PRs with these labels won't be marked stale
|
25 |
-
operations-per-run:
|
|
|
16 |
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
17 |
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."
|
18 |
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."
|
19 |
+
days-before-stale: 10 # Number of days before marking an issue or PR as stale
|
20 |
+
days-before-close: 4 # Number of days after being marked stale before closing
|
21 |
stale-issue-label: "stale" # Label to apply to stale issues
|
22 |
stale-pr-label: "stale" # Label to apply to stale pull requests
|
23 |
exempt-issue-labels: "pinned,important" # Issues with these labels won't be marked stale
|
24 |
exempt-pr-labels: "pinned,important" # PRs with these labels won't be marked stale
|
25 |
+
operations-per-run: 75 # Limits the number of actions per run to avoid API rate limits
|
.husky/pre-commit
CHANGED
@@ -17,7 +17,7 @@ fi
|
|
17 |
|
18 |
echo "Running lint..."
|
19 |
if ! pnpm lint; then
|
20 |
-
echo "❌ Linting failed! 'pnpm lint:
|
21 |
echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
|
22 |
echo "lint exit code: $?"
|
23 |
exit 1
|
|
|
17 |
|
18 |
echo "Running lint..."
|
19 |
if ! pnpm lint; then
|
20 |
+
echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
|
21 |
echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
|
22 |
echo "lint exit code: $?"
|
23 |
exit 1
|
README.md
CHANGED
@@ -4,10 +4,13 @@
|
|
4 |
|
5 |
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.
|
6 |
|
|
|
|
|
7 |
## Join the community for oTToDev!
|
8 |
|
9 |
https://thinktank.ottomator.ai
|
10 |
|
|
|
11 |
## Requested Additions - Feel Free to Contribute!
|
12 |
|
13 |
- ✅ OpenRouter Integration (@coleam00)
|
@@ -31,6 +34,7 @@ https://thinktank.ottomator.ai
|
|
31 |
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
|
32 |
- ✅ Cohere Integration (@hasanraiyan)
|
33 |
- ✅ Dynamic model max token length (@hasanraiyan)
|
|
|
34 |
- ✅ Prompt caching (@SujalXplores)
|
35 |
- ✅ Load local projects into the app (@wonderwhy-er)
|
36 |
- ✅ Together Integration (@mouimet-infinisoft)
|
|
|
4 |
|
5 |
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.
|
6 |
|
7 |
+
Check the [oTToDev Docs](https://coleam00.github.io/bolt.new-any-llm/) for more information.
|
8 |
+
|
9 |
## Join the community for oTToDev!
|
10 |
|
11 |
https://thinktank.ottomator.ai
|
12 |
|
13 |
+
|
14 |
## Requested Additions - Feel Free to Contribute!
|
15 |
|
16 |
- ✅ OpenRouter Integration (@coleam00)
|
|
|
34 |
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
|
35 |
- ✅ Cohere Integration (@hasanraiyan)
|
36 |
- ✅ Dynamic model max token length (@hasanraiyan)
|
37 |
+
- ✅ Better prompt enhancing (@SujalXplores)
|
38 |
- ✅ Prompt caching (@SujalXplores)
|
39 |
- ✅ Load local projects into the app (@wonderwhy-er)
|
40 |
- ✅ Together Integration (@mouimet-infinisoft)
|
app/components/chat/BaseChat.tsx
CHANGED
@@ -255,6 +255,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
255 |
<span>Model Settings</span>
|
256 |
</button>
|
257 |
</div>
|
|
|
258 |
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
259 |
<ModelSelector
|
260 |
key={provider?.name + ':' + modelList.length}
|
|
|
255 |
<span>Model Settings</span>
|
256 |
</button>
|
257 |
</div>
|
258 |
+
|
259 |
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
260 |
<ModelSelector
|
261 |
key={provider?.name + ':' + modelList.length}
|
app/components/header/Header.tsx
CHANGED
@@ -24,17 +24,19 @@ export function Header() {
|
|
24 |
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
25 |
</a>
|
26 |
</div>
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
<
|
35 |
-
|
36 |
-
|
37 |
-
|
|
|
|
|
38 |
)}
|
39 |
</header>
|
40 |
);
|
|
|
24 |
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
25 |
</a>
|
26 |
</div>
|
27 |
+
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
|
28 |
+
<>
|
29 |
+
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
|
30 |
+
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
|
31 |
+
</span>
|
32 |
+
<ClientOnly>
|
33 |
+
{() => (
|
34 |
+
<div className="mr-1">
|
35 |
+
<HeaderActionButtons />
|
36 |
+
</div>
|
37 |
+
)}
|
38 |
+
</ClientOnly>
|
39 |
+
</>
|
40 |
)}
|
41 |
</header>
|
42 |
);
|
app/components/header/HeaderActionButtons.client.tsx
CHANGED
@@ -19,7 +19,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
19 |
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
20 |
<Button
|
21 |
active={showChat}
|
22 |
-
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's needed
|
23 |
onClick={() => {
|
24 |
if (canHideChat) {
|
25 |
chatStore.setKey('showChat', !showChat);
|
|
|
19 |
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
20 |
<Button
|
21 |
active={showChat}
|
22 |
+
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
|
23 |
onClick={() => {
|
24 |
if (canHideChat) {
|
25 |
chatStore.setKey('showChat', !showChat);
|
app/components/sidebar/HistoryItem.tsx
CHANGED
@@ -1,6 +1,9 @@
|
|
|
|
|
|
1 |
import * as Dialog from '@radix-ui/react-dialog';
|
2 |
import { type ChatHistoryItem } from '~/lib/persistence';
|
3 |
import WithTooltip from '~/components/ui/Tooltip';
|
|
|
4 |
|
5 |
interface HistoryItemProps {
|
6 |
item: ChatHistoryItem;
|
@@ -10,48 +13,115 @@ interface HistoryItemProps {
|
|
10 |
}
|
11 |
|
12 |
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
return (
|
14 |
-
<div
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
onClick={(event) => {
|
24 |
event.preventDefault();
|
25 |
exportChat(item.id);
|
26 |
}}
|
27 |
-
title="Export chat"
|
28 |
/>
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
type="button"
|
34 |
-
className="i-ph:copy scale-110 mr-2 hover:text-bolt-elements-item-contentAccent"
|
35 |
onClick={() => onDuplicate?.(item.id)}
|
36 |
-
title="Duplicate chat"
|
37 |
/>
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
onClick={(event) => {
|
46 |
event.preventDefault();
|
47 |
onDelete?.(event);
|
48 |
}}
|
49 |
/>
|
50 |
-
</
|
51 |
-
</
|
52 |
</div>
|
53 |
-
</
|
54 |
-
|
55 |
</div>
|
56 |
);
|
57 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useParams } from '@remix-run/react';
|
2 |
+
import { classNames } from '~/utils/classNames';
|
3 |
import * as Dialog from '@radix-ui/react-dialog';
|
4 |
import { type ChatHistoryItem } from '~/lib/persistence';
|
5 |
import WithTooltip from '~/components/ui/Tooltip';
|
6 |
+
import { useEditChatDescription } from '~/lib/hooks';
|
7 |
|
8 |
interface HistoryItemProps {
|
9 |
item: ChatHistoryItem;
|
|
|
13 |
}
|
14 |
|
15 |
export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
|
16 |
+
const { id: urlId } = useParams();
|
17 |
+
const isActiveChat = urlId === item.urlId;
|
18 |
+
|
19 |
+
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
|
20 |
+
useEditChatDescription({
|
21 |
+
initialDescription: item.description,
|
22 |
+
customChatId: item.id,
|
23 |
+
syncWithGlobalStore: isActiveChat,
|
24 |
+
});
|
25 |
+
|
26 |
+
const renderDescriptionForm = (
|
27 |
+
<form onSubmit={handleSubmit} className="flex-1 flex items-center">
|
28 |
+
<input
|
29 |
+
type="text"
|
30 |
+
className="flex-1 bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2"
|
31 |
+
autoFocus
|
32 |
+
value={currentDescription}
|
33 |
+
onChange={handleChange}
|
34 |
+
onBlur={handleBlur}
|
35 |
+
onKeyDown={handleKeyDown}
|
36 |
+
/>
|
37 |
+
<button
|
38 |
+
type="submit"
|
39 |
+
className="i-ph:check scale-110 hover:text-bolt-elements-item-contentAccent"
|
40 |
+
onMouseDown={handleSubmit}
|
41 |
+
/>
|
42 |
+
</form>
|
43 |
+
);
|
44 |
+
|
45 |
return (
|
46 |
+
<div
|
47 |
+
className={classNames(
|
48 |
+
'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',
|
49 |
+
{ '[&&]:text-bolt-elements-textPrimary bg-bolt-elements-background-depth-3': isActiveChat },
|
50 |
+
)}
|
51 |
+
>
|
52 |
+
{editing ? (
|
53 |
+
renderDescriptionForm
|
54 |
+
) : (
|
55 |
+
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
|
56 |
+
{currentDescription}
|
57 |
+
<div
|
58 |
+
className={classNames(
|
59 |
+
'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%',
|
60 |
+
{ 'from-bolt-elements-background-depth-3 w-10 ': isActiveChat },
|
61 |
+
)}
|
62 |
+
>
|
63 |
+
<div className="flex items-center p-1 text-bolt-elements-textSecondary opacity-0 group-hover:opacity-100 transition-opacity">
|
64 |
+
<ChatActionButton
|
65 |
+
toolTipContent="Export chat"
|
66 |
+
icon="i-ph:download-simple"
|
67 |
onClick={(event) => {
|
68 |
event.preventDefault();
|
69 |
exportChat(item.id);
|
70 |
}}
|
|
|
71 |
/>
|
72 |
+
{onDuplicate && (
|
73 |
+
<ChatActionButton
|
74 |
+
toolTipContent="Duplicate chat"
|
75 |
+
icon="i-ph:copy"
|
|
|
|
|
76 |
onClick={() => onDuplicate?.(item.id)}
|
|
|
77 |
/>
|
78 |
+
)}
|
79 |
+
<ChatActionButton
|
80 |
+
toolTipContent="Rename chat"
|
81 |
+
icon="i-ph:pencil-fill"
|
82 |
+
onClick={(event) => {
|
83 |
+
event.preventDefault();
|
84 |
+
toggleEditMode();
|
85 |
+
}}
|
86 |
+
/>
|
87 |
+
<Dialog.Trigger asChild>
|
88 |
+
<ChatActionButton
|
89 |
+
toolTipContent="Delete chat"
|
90 |
+
icon="i-ph:trash"
|
91 |
+
className="[&&]:hover:text-bolt-elements-button-danger-text"
|
92 |
onClick={(event) => {
|
93 |
event.preventDefault();
|
94 |
onDelete?.(event);
|
95 |
}}
|
96 |
/>
|
97 |
+
</Dialog.Trigger>
|
98 |
+
</div>
|
99 |
</div>
|
100 |
+
</a>
|
101 |
+
)}
|
102 |
</div>
|
103 |
);
|
104 |
}
|
105 |
+
|
106 |
+
const ChatActionButton = ({
|
107 |
+
toolTipContent,
|
108 |
+
icon,
|
109 |
+
className,
|
110 |
+
onClick,
|
111 |
+
}: {
|
112 |
+
toolTipContent: string;
|
113 |
+
icon: string;
|
114 |
+
className?: string;
|
115 |
+
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
116 |
+
btnTitle?: string;
|
117 |
+
}) => {
|
118 |
+
return (
|
119 |
+
<WithTooltip tooltip={toolTipContent}>
|
120 |
+
<button
|
121 |
+
type="button"
|
122 |
+
className={`scale-110 mr-2 hover:text-bolt-elements-item-contentAccent ${icon} ${className ? className : ''}`}
|
123 |
+
onClick={onClick}
|
124 |
+
/>
|
125 |
+
</WithTooltip>
|
126 |
+
);
|
127 |
+
};
|
app/lib/hooks/index.ts
CHANGED
@@ -2,4 +2,5 @@ export * from './useMessageParser';
|
|
2 |
export * from './usePromptEnhancer';
|
3 |
export * from './useShortcuts';
|
4 |
export * from './useSnapScroll';
|
|
|
5 |
export { default } from './useViewport';
|
|
|
2 |
export * from './usePromptEnhancer';
|
3 |
export * from './useShortcuts';
|
4 |
export * from './useSnapScroll';
|
5 |
+
export * from './useEditChatDescription';
|
6 |
export { default } from './useViewport';
|
app/lib/hooks/useEditChatDescription.ts
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useStore } from '@nanostores/react';
|
2 |
+
import { useCallback, useEffect, useState } from 'react';
|
3 |
+
import { toast } from 'react-toastify';
|
4 |
+
import {
|
5 |
+
chatId as chatIdStore,
|
6 |
+
description as descriptionStore,
|
7 |
+
db,
|
8 |
+
updateChatDescription,
|
9 |
+
getMessages,
|
10 |
+
} from '~/lib/persistence';
|
11 |
+
|
12 |
+
interface EditChatDescriptionOptions {
|
13 |
+
initialDescription?: string;
|
14 |
+
customChatId?: string;
|
15 |
+
syncWithGlobalStore?: boolean;
|
16 |
+
}
|
17 |
+
|
18 |
+
type EditChatDescriptionHook = {
|
19 |
+
editing: boolean;
|
20 |
+
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
21 |
+
handleBlur: () => Promise<void>;
|
22 |
+
handleSubmit: (event: React.FormEvent) => Promise<void>;
|
23 |
+
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => Promise<void>;
|
24 |
+
currentDescription: string;
|
25 |
+
toggleEditMode: () => void;
|
26 |
+
};
|
27 |
+
|
28 |
+
/**
|
29 |
+
* Hook to manage the state and behavior for editing chat descriptions.
|
30 |
+
*
|
31 |
+
* Offers functions to:
|
32 |
+
* - Switch between edit and view modes.
|
33 |
+
* - Manage input changes, blur, and form submission events.
|
34 |
+
* - Save updates to IndexedDB and optionally to the global application state.
|
35 |
+
*
|
36 |
+
* @param {Object} options
|
37 |
+
* @param {string} options.initialDescription - The current chat description.
|
38 |
+
* @param {string} options.customChatId - Optional ID for updating the description via the sidebar.
|
39 |
+
* @param {boolean} options.syncWithGlobalStore - Flag to indicate global description store synchronization.
|
40 |
+
* @returns {EditChatDescriptionHook} Methods and state for managing description edits.
|
41 |
+
*/
|
42 |
+
export function useEditChatDescription({
|
43 |
+
initialDescription = descriptionStore.get()!,
|
44 |
+
customChatId,
|
45 |
+
syncWithGlobalStore,
|
46 |
+
}: EditChatDescriptionOptions): EditChatDescriptionHook {
|
47 |
+
const chatIdFromStore = useStore(chatIdStore);
|
48 |
+
const [editing, setEditing] = useState(false);
|
49 |
+
const [currentDescription, setCurrentDescription] = useState(initialDescription);
|
50 |
+
|
51 |
+
const [chatId, setChatId] = useState<string>();
|
52 |
+
|
53 |
+
useEffect(() => {
|
54 |
+
setChatId(customChatId || chatIdFromStore);
|
55 |
+
}, [customChatId, chatIdFromStore]);
|
56 |
+
useEffect(() => {
|
57 |
+
setCurrentDescription(initialDescription);
|
58 |
+
}, [initialDescription]);
|
59 |
+
|
60 |
+
const toggleEditMode = useCallback(() => setEditing((prev) => !prev), []);
|
61 |
+
|
62 |
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
63 |
+
setCurrentDescription(e.target.value);
|
64 |
+
}, []);
|
65 |
+
|
66 |
+
const fetchLatestDescription = useCallback(async () => {
|
67 |
+
if (!db || !chatId) {
|
68 |
+
return initialDescription;
|
69 |
+
}
|
70 |
+
|
71 |
+
try {
|
72 |
+
const chat = await getMessages(db, chatId);
|
73 |
+
return chat?.description || initialDescription;
|
74 |
+
} catch (error) {
|
75 |
+
console.error('Failed to fetch latest description:', error);
|
76 |
+
return initialDescription;
|
77 |
+
}
|
78 |
+
}, [db, chatId, initialDescription]);
|
79 |
+
|
80 |
+
const handleBlur = useCallback(async () => {
|
81 |
+
const latestDescription = await fetchLatestDescription();
|
82 |
+
setCurrentDescription(latestDescription);
|
83 |
+
toggleEditMode();
|
84 |
+
}, [fetchLatestDescription, toggleEditMode]);
|
85 |
+
|
86 |
+
const isValidDescription = useCallback((desc: string): boolean => {
|
87 |
+
const trimmedDesc = desc.trim();
|
88 |
+
|
89 |
+
if (trimmedDesc === initialDescription) {
|
90 |
+
toggleEditMode();
|
91 |
+
return false; // No change, skip validation
|
92 |
+
}
|
93 |
+
|
94 |
+
const lengthValid = trimmedDesc.length > 0 && trimmedDesc.length <= 100;
|
95 |
+
const characterValid = /^[a-zA-Z0-9\s]+$/.test(trimmedDesc);
|
96 |
+
|
97 |
+
if (!lengthValid) {
|
98 |
+
toast.error('Description must be between 1 and 100 characters.');
|
99 |
+
return false;
|
100 |
+
}
|
101 |
+
|
102 |
+
if (!characterValid) {
|
103 |
+
toast.error('Description can only contain alphanumeric characters and spaces.');
|
104 |
+
return false;
|
105 |
+
}
|
106 |
+
|
107 |
+
return true;
|
108 |
+
}, []);
|
109 |
+
|
110 |
+
const handleSubmit = useCallback(
|
111 |
+
async (event: React.FormEvent) => {
|
112 |
+
event.preventDefault();
|
113 |
+
|
114 |
+
if (!isValidDescription(currentDescription)) {
|
115 |
+
return;
|
116 |
+
}
|
117 |
+
|
118 |
+
try {
|
119 |
+
if (!db) {
|
120 |
+
toast.error('Chat persistence is not available');
|
121 |
+
return;
|
122 |
+
}
|
123 |
+
|
124 |
+
if (!chatId) {
|
125 |
+
toast.error('Chat Id is not available');
|
126 |
+
return;
|
127 |
+
}
|
128 |
+
|
129 |
+
await updateChatDescription(db, chatId, currentDescription);
|
130 |
+
|
131 |
+
if (syncWithGlobalStore) {
|
132 |
+
descriptionStore.set(currentDescription);
|
133 |
+
}
|
134 |
+
|
135 |
+
toast.success('Chat description updated successfully');
|
136 |
+
} catch (error) {
|
137 |
+
toast.error('Failed to update chat description: ' + (error as Error).message);
|
138 |
+
}
|
139 |
+
|
140 |
+
toggleEditMode();
|
141 |
+
},
|
142 |
+
[currentDescription, db, chatId, initialDescription, customChatId],
|
143 |
+
);
|
144 |
+
|
145 |
+
const handleKeyDown = useCallback(
|
146 |
+
async (e: React.KeyboardEvent<HTMLInputElement>) => {
|
147 |
+
if (e.key === 'Escape') {
|
148 |
+
await handleBlur();
|
149 |
+
}
|
150 |
+
},
|
151 |
+
[handleBlur],
|
152 |
+
);
|
153 |
+
|
154 |
+
return {
|
155 |
+
editing,
|
156 |
+
handleChange,
|
157 |
+
handleBlur,
|
158 |
+
handleSubmit,
|
159 |
+
handleKeyDown,
|
160 |
+
currentDescription,
|
161 |
+
toggleEditMode,
|
162 |
+
};
|
163 |
+
}
|
app/lib/persistence/ChatDescription.client.tsx
CHANGED
@@ -1,6 +1,68 @@
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
-
import {
|
|
|
|
|
|
|
3 |
|
4 |
export function ChatDescription() {
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
}
|
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
+
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
3 |
+
import WithTooltip from '~/components/ui/Tooltip';
|
4 |
+
import { useEditChatDescription } from '~/lib/hooks';
|
5 |
+
import { description as descriptionStore } from '~/lib/persistence';
|
6 |
|
7 |
export function ChatDescription() {
|
8 |
+
const initialDescription = useStore(descriptionStore)!;
|
9 |
+
|
10 |
+
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
|
11 |
+
useEditChatDescription({
|
12 |
+
initialDescription,
|
13 |
+
syncWithGlobalStore: true,
|
14 |
+
});
|
15 |
+
|
16 |
+
if (!initialDescription) {
|
17 |
+
// doing this to prevent showing edit button until chat description is set
|
18 |
+
return null;
|
19 |
+
}
|
20 |
+
|
21 |
+
return (
|
22 |
+
<div className="flex items-center justify-center">
|
23 |
+
{editing ? (
|
24 |
+
<form onSubmit={handleSubmit} className="flex items-center justify-center">
|
25 |
+
<input
|
26 |
+
type="text"
|
27 |
+
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 mr-2 w-fit"
|
28 |
+
autoFocus
|
29 |
+
value={currentDescription}
|
30 |
+
onChange={handleChange}
|
31 |
+
onBlur={handleBlur}
|
32 |
+
onKeyDown={handleKeyDown}
|
33 |
+
style={{ width: `${Math.max(currentDescription.length * 8, 100)}px` }}
|
34 |
+
/>
|
35 |
+
<TooltipProvider>
|
36 |
+
<WithTooltip tooltip="Save title">
|
37 |
+
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent">
|
38 |
+
<button
|
39 |
+
type="submit"
|
40 |
+
className="i-ph:check-bold scale-110 hover:text-bolt-elements-item-contentAccent"
|
41 |
+
onMouseDown={handleSubmit}
|
42 |
+
/>
|
43 |
+
</div>
|
44 |
+
</WithTooltip>
|
45 |
+
</TooltipProvider>
|
46 |
+
</form>
|
47 |
+
) : (
|
48 |
+
<>
|
49 |
+
{currentDescription}
|
50 |
+
<TooltipProvider>
|
51 |
+
<WithTooltip tooltip="Rename chat">
|
52 |
+
<div className="flex justify-between items-center p-2 rounded-md bg-bolt-elements-item-backgroundAccent ml-2">
|
53 |
+
<button
|
54 |
+
type="button"
|
55 |
+
className="i-ph:pencil-fill scale-110 hover:text-bolt-elements-item-contentAccent"
|
56 |
+
onClick={(event) => {
|
57 |
+
event.preventDefault();
|
58 |
+
toggleEditMode();
|
59 |
+
}}
|
60 |
+
/>
|
61 |
+
</div>
|
62 |
+
</WithTooltip>
|
63 |
+
</TooltipProvider>
|
64 |
+
</>
|
65 |
+
)}
|
66 |
+
</div>
|
67 |
+
);
|
68 |
}
|
app/lib/persistence/db.ts
CHANGED
@@ -52,17 +52,23 @@ export async function setMessages(
|
|
52 |
messages: Message[],
|
53 |
urlId?: string,
|
54 |
description?: string,
|
|
|
55 |
): Promise<void> {
|
56 |
return new Promise((resolve, reject) => {
|
57 |
const transaction = db.transaction('chats', 'readwrite');
|
58 |
const store = transaction.objectStore('chats');
|
59 |
|
|
|
|
|
|
|
|
|
|
|
60 |
const request = store.put({
|
61 |
id,
|
62 |
messages,
|
63 |
urlId,
|
64 |
description,
|
65 |
-
timestamp: new Date().toISOString(),
|
66 |
});
|
67 |
|
68 |
request.onsuccess = () => resolve();
|
@@ -212,3 +218,17 @@ export async function createChatFromMessages(
|
|
212 |
|
213 |
return newUrlId; // Return the urlId instead of id for navigation
|
214 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
messages: Message[],
|
53 |
urlId?: string,
|
54 |
description?: string,
|
55 |
+
timestamp?: string,
|
56 |
): Promise<void> {
|
57 |
return new Promise((resolve, reject) => {
|
58 |
const transaction = db.transaction('chats', 'readwrite');
|
59 |
const store = transaction.objectStore('chats');
|
60 |
|
61 |
+
if (timestamp && isNaN(Date.parse(timestamp))) {
|
62 |
+
reject(new Error('Invalid timestamp'));
|
63 |
+
return;
|
64 |
+
}
|
65 |
+
|
66 |
const request = store.put({
|
67 |
id,
|
68 |
messages,
|
69 |
urlId,
|
70 |
description,
|
71 |
+
timestamp: timestamp ?? new Date().toISOString(),
|
72 |
});
|
73 |
|
74 |
request.onsuccess = () => resolve();
|
|
|
218 |
|
219 |
return newUrlId; // Return the urlId instead of id for navigation
|
220 |
}
|
221 |
+
|
222 |
+
export async function updateChatDescription(db: IDBDatabase, id: string, description: string): Promise<void> {
|
223 |
+
const chat = await getMessages(db, id);
|
224 |
+
|
225 |
+
if (!chat) {
|
226 |
+
throw new Error('Chat not found');
|
227 |
+
}
|
228 |
+
|
229 |
+
if (!description.trim()) {
|
230 |
+
throw new Error('Description cannot be empty');
|
231 |
+
}
|
232 |
+
|
233 |
+
await setMessages(db, id, chat.messages, chat.urlId, description, chat.timestamp);
|
234 |
+
}
|
app/lib/runtime/action-runner.ts
CHANGED
@@ -100,6 +100,10 @@ export class ActionRunner {
|
|
100 |
.catch((error) => {
|
101 |
console.error('Action failed:', error);
|
102 |
});
|
|
|
|
|
|
|
|
|
103 |
}
|
104 |
|
105 |
async #executeAction(actionId: string, isStreaming: boolean = false) {
|
|
|
100 |
.catch((error) => {
|
101 |
console.error('Action failed:', error);
|
102 |
});
|
103 |
+
|
104 |
+
await this.#currentExecutionPromise;
|
105 |
+
|
106 |
+
return;
|
107 |
}
|
108 |
|
109 |
async #executeAction(actionId: string, isStreaming: boolean = false) {
|
app/routes/api.enhancer.ts
CHANGED
@@ -44,8 +44,9 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
|
44 |
content:
|
45 |
`[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
|
46 |
stripIndents`
|
47 |
-
|
48 |
Your task is to enhance prompts by making them more specific, actionable, and effective.
|
|
|
49 |
I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
|
50 |
|
51 |
For valid prompts:
|
@@ -55,12 +56,14 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
|
|
55 |
- Maintain the core intent
|
56 |
- Ensure the prompt is self-contained
|
57 |
- Use professional language
|
|
|
58 |
For invalid or unclear prompts:
|
59 |
- Respond with a clear, professional guidance message
|
60 |
- Keep responses concise and actionable
|
61 |
- Maintain a helpful, constructive tone
|
62 |
- Focus on what the user should provide
|
63 |
- Use a standard template for consistency
|
|
|
64 |
IMPORTANT: Your response must ONLY contain the enhanced prompt text.
|
65 |
Do not include any explanations, metadata, or wrapper tags.
|
66 |
|
|
|
44 |
content:
|
45 |
`[Model: ${model}]\n\n[Provider: ${providerName}]\n\n` +
|
46 |
stripIndents`
|
47 |
+
You are a professional prompt engineer specializing in crafting precise, effective prompts.
|
48 |
Your task is to enhance prompts by making them more specific, actionable, and effective.
|
49 |
+
|
50 |
I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
|
51 |
|
52 |
For valid prompts:
|
|
|
56 |
- Maintain the core intent
|
57 |
- Ensure the prompt is self-contained
|
58 |
- Use professional language
|
59 |
+
|
60 |
For invalid or unclear prompts:
|
61 |
- Respond with a clear, professional guidance message
|
62 |
- Keep responses concise and actionable
|
63 |
- Maintain a helpful, constructive tone
|
64 |
- Focus on what the user should provide
|
65 |
- Use a standard template for consistency
|
66 |
+
|
67 |
IMPORTANT: Your response must ONLY contain the enhanced prompt text.
|
68 |
Do not include any explanations, metadata, or wrapper tags.
|
69 |
|