Skip to content

Commit f20173f

Browse files
authored
Merge pull request stackblitz-labs#493 from dustinwloring1988/stable-plus-ui-glow
UI glow and Prompt Caching
2 parents c8b4eee + 5ee2e90 commit f20173f

File tree

5 files changed

+170
-6
lines changed

5 files changed

+170
-6
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ https://thinktank.ottomator.ai
3131
- ✅ Ability to revert code to earlier version (@wonderwhy-er)
3232
- ✅ Cohere Integration (@hasanraiyan)
3333
- ✅ Dynamic model max token length (@hasanraiyan)
34+
- ✅ Prompt caching (@SujalXplores)
3435
-**HIGH PRIORITY** - Load local projects into the app (@wonderwhy-er)
3536
-**HIGH PRIORITY** - ALMOST DONE - Attach images to prompts (@atrokhym)
3637
-**HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
@@ -42,7 +43,6 @@ https://thinktank.ottomator.ai
4243
- ⬜ Perplexity Integration
4344
- ⬜ Vertex AI Integration
4445
- ⬜ Deploy directly to Vercel/Netlify/other similar platforms
45-
- ⬜ Prompt caching
4646
- ⬜ Better prompt enhancing
4747
- ⬜ Have LLM plan the project in a MD file for better results/transparency
4848
- ⬜ VSCode Integration with git-like confirmations

app/components/chat/BaseChat.module.scss

+104
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,107 @@
1717
.Chat {
1818
opacity: 1;
1919
}
20+
21+
.RayContainer {
22+
--gradient-opacity: 0.85;
23+
--ray-gradient: radial-gradient(rgba(83, 196, 255, var(--gradient-opacity)) 0%, rgba(43, 166, 255, 0) 100%);
24+
transition: opacity 0.25s linear;
25+
position: fixed;
26+
inset: 0;
27+
pointer-events: none;
28+
user-select: none;
29+
}
30+
31+
.LightRayOne {
32+
width: 480px;
33+
height: 680px;
34+
transform: rotate(80deg);
35+
top: -540px;
36+
left: 250px;
37+
filter: blur(110px);
38+
position: absolute;
39+
border-radius: 100%;
40+
background: var(--ray-gradient);
41+
}
42+
43+
.LightRayTwo {
44+
width: 110px;
45+
height: 400px;
46+
transform: rotate(-20deg);
47+
top: -280px;
48+
left: 350px;
49+
mix-blend-mode: overlay;
50+
opacity: 0.6;
51+
filter: blur(60px);
52+
position: absolute;
53+
border-radius: 100%;
54+
background: var(--ray-gradient);
55+
}
56+
57+
.LightRayThree {
58+
width: 400px;
59+
height: 370px;
60+
top: -350px;
61+
left: 200px;
62+
mix-blend-mode: overlay;
63+
opacity: 0.6;
64+
filter: blur(21px);
65+
position: absolute;
66+
border-radius: 100%;
67+
background: var(--ray-gradient);
68+
}
69+
70+
.LightRayFour {
71+
position: absolute;
72+
width: 330px;
73+
height: 370px;
74+
top: -330px;
75+
left: 50px;
76+
mix-blend-mode: overlay;
77+
opacity: 0.5;
78+
filter: blur(21px);
79+
border-radius: 100%;
80+
background: var(--ray-gradient);
81+
}
82+
83+
.LightRayFive {
84+
position: absolute;
85+
width: 110px;
86+
height: 400px;
87+
transform: rotate(-40deg);
88+
top: -280px;
89+
left: -10px;
90+
mix-blend-mode: overlay;
91+
opacity: 0.8;
92+
filter: blur(60px);
93+
border-radius: 100%;
94+
background: var(--ray-gradient);
95+
}
96+
97+
.PromptEffectContainer {
98+
--prompt-container-offset: 50px;
99+
--prompt-line-stroke-width: 1px;
100+
position: absolute;
101+
pointer-events: none;
102+
inset: calc(var(--prompt-container-offset) / -2);
103+
width: calc(100% + var(--prompt-container-offset));
104+
height: calc(100% + var(--prompt-container-offset));
105+
}
106+
107+
.PromptEffectLine {
108+
width: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
109+
height: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
110+
x: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
111+
y: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
112+
rx: calc(8px - var(--prompt-line-stroke-width));
113+
fill: transparent;
114+
stroke-width: var(--prompt-line-stroke-width);
115+
stroke: url(#line-gradient);
116+
stroke-dasharray: 35px 65px;
117+
stroke-dashoffset: 10;
118+
}
119+
120+
.PromptShine {
121+
fill: url(#shine-gradient);
122+
mix-blend-mode: overlay;
123+
}

app/components/chat/BaseChat.tsx

+35-2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
168168
)}
169169
data-chat-visible={showChat}
170170
>
171+
<div className={classNames(styles.RayContainer)}>
172+
<div className={classNames(styles.LightRayOne)}></div>
173+
<div className={classNames(styles.LightRayTwo)}></div>
174+
<div className={classNames(styles.LightRayThree)}></div>
175+
<div className={classNames(styles.LightRayFour)}></div>
176+
<div className={classNames(styles.LightRayFive)}></div>
177+
</div>
171178
<ClientOnly>{() => <Menu />}</ClientOnly>
172179
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
173180
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
@@ -206,6 +213,32 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
206213
},
207214
)}
208215
>
216+
<svg className={classNames(styles.PromptEffectContainer)}>
217+
<defs>
218+
<linearGradient
219+
id="line-gradient"
220+
x1="20%"
221+
y1="0%"
222+
x2="-14%"
223+
y2="10%"
224+
gradientUnits="userSpaceOnUse"
225+
gradientTransform="rotate(-45)"
226+
>
227+
<stop offset="0%" stopColor="#1488fc" stopOpacity="0%"></stop>
228+
<stop offset="40%" stopColor="#1488fc" stopOpacity="80%"></stop>
229+
<stop offset="50%" stopColor="#1488fc" stopOpacity="80%"></stop>
230+
<stop offset="100%" stopColor="#1488fc" stopOpacity="0%"></stop>
231+
</linearGradient>
232+
<linearGradient id="shine-gradient">
233+
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
234+
<stop offset="40%" stopColor="#8adaff" stopOpacity="80%"></stop>
235+
<stop offset="50%" stopColor="#8adaff" stopOpacity="80%"></stop>
236+
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
237+
</linearGradient>
238+
</defs>
239+
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
240+
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
241+
</svg>
209242
<div>
210243
<div className="flex justify-between items-center mb-2">
211244
<button
@@ -245,12 +278,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
245278

246279
<div
247280
className={classNames(
248-
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
281+
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
249282
)}
250283
>
251284
<textarea
252285
ref={textareaRef}
253-
className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-0 focus:border-none focus:shadow-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
286+
className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm`}
254287
onKeyDown={(event) => {
255288
if (event.key === 'Enter') {
256289
if (event.shiftKey) {

app/components/chat/Chat.client.tsx

+29-3
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@ import { useStore } from '@nanostores/react';
66
import type { Message } from 'ai';
77
import { useChat } from 'ai/react';
88
import { useAnimate } from 'framer-motion';
9-
import { memo, useEffect, useRef, useState } from 'react';
9+
import { memo, useCallback, useEffect, useRef, useState } from 'react';
1010
import { cssTransition, toast, ToastContainer } from 'react-toastify';
1111
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
1212
import { description, useChatHistory } from '~/lib/persistence';
1313
import { chatStore } from '~/lib/stores/chat';
1414
import { workbenchStore } from '~/lib/stores/workbench';
1515
import { fileModificationsToHTML } from '~/utils/diff';
16-
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants';
16+
import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROMPT_COOKIE_KEY, PROVIDER_LIST } from '~/utils/constants';
1717
import { cubicEasingFn } from '~/utils/easings';
1818
import { createScopedLogger, renderLogger } from '~/utils/logger';
1919
import { BaseChat } from './BaseChat';
2020
import Cookies from 'js-cookie';
2121
import type { ProviderInfo } from '~/utils/types';
22+
import { debounce } from '~/utils/debounce';
2223

2324
const toastAnimation = cssTransition({
2425
enter: 'animated fadeInRight',
@@ -120,6 +121,7 @@ export const ChatImpl = memo(
120121
logger.debug('Finished streaming');
121122
},
122123
initialMessages,
124+
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
123125
});
124126

125127
const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
@@ -225,12 +227,33 @@ export const ChatImpl = memo(
225227
}
226228

227229
setInput('');
230+
Cookies.remove(PROMPT_COOKIE_KEY);
228231

229232
resetEnhancer();
230233

231234
textareaRef.current?.blur();
232235
};
233236

237+
/**
238+
* Handles the change event for the textarea and updates the input state.
239+
* @param event - The change event from the textarea.
240+
*/
241+
const onTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
242+
handleInputChange(event);
243+
};
244+
245+
/**
246+
* Debounced function to cache the prompt in cookies.
247+
* Caches the trimmed value of the textarea input after a delay to optimize performance.
248+
*/
249+
const debouncedCachePrompt = useCallback(
250+
debounce((event: React.ChangeEvent<HTMLTextAreaElement>) => {
251+
const trimmedValue = event.target.value.trim();
252+
Cookies.set(PROMPT_COOKIE_KEY, trimmedValue, { expires: 30 });
253+
}, 1000),
254+
[],
255+
);
256+
234257
const [messageRef, scrollRef] = useSnapScroll();
235258

236259
useEffect(() => {
@@ -268,7 +291,10 @@ export const ChatImpl = memo(
268291
setProvider={handleProviderChange}
269292
messageRef={messageRef}
270293
scrollRef={scrollRef}
271-
handleInputChange={handleInputChange}
294+
handleInputChange={(e) => {
295+
onTextareaChange(e);
296+
debouncedCachePrompt(e);
297+
}}
272298
handleStop={abort}
273299
description={description}
274300
importChat={importChat}

app/utils/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
77
export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/;
88
export const PROVIDER_REGEX = /\[Provider: (.*?)\]\n\n/;
99
export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest';
10+
export const PROMPT_COOKIE_KEY = 'cachedPrompt';
1011

1112
const PROVIDER_LIST: ProviderInfo[] = [
1213
{

0 commit comments

Comments
 (0)