Merge pull request #361 from qwikode/feature/mobile-friendly
Browse files- app/components/chat/APIKeyManager.tsx +26 -21
- app/components/chat/BaseChat.tsx +19 -17
- app/components/chat/Messages.client.tsx +50 -47
- app/components/header/HeaderActionButtons.client.tsx +4 -1
- app/components/workbench/Workbench.client.tsx +19 -11
- app/lib/hooks/index.ts +1 -0
- app/lib/hooks/useViewport.ts +18 -0
app/components/chat/APIKeyManager.tsx
CHANGED
@@ -10,11 +10,7 @@ interface APIKeyManagerProps {
|
|
10 |
labelForGetApiKey?: string;
|
11 |
}
|
12 |
|
13 |
-
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
|
14 |
-
provider,
|
15 |
-
apiKey,
|
16 |
-
setApiKey,
|
17 |
-
}) => {
|
18 |
const [isEditing, setIsEditing] = useState(false);
|
19 |
const [tempKey, setTempKey] = useState(apiKey);
|
20 |
|
@@ -24,15 +20,29 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
|
|
24 |
};
|
25 |
|
26 |
return (
|
27 |
-
<div className="flex items-
|
28 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
{isEditing ? (
|
30 |
-
|
31 |
<input
|
32 |
type="password"
|
33 |
value={tempKey}
|
|
|
34 |
onChange={(e) => setTempKey(e.target.value)}
|
35 |
-
className="flex-1
|
36 |
/>
|
37 |
<IconButton onClick={handleSave} title="Save API Key">
|
38 |
<div className="i-ph:check" />
|
@@ -40,20 +50,15 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
|
|
40 |
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
|
41 |
<div className="i-ph:x" />
|
42 |
</IconButton>
|
43 |
-
|
44 |
) : (
|
45 |
<>
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
{provider?.getApiKeyLink && <IconButton onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
|
54 |
-
<span className="mr-2">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
55 |
-
<div className={provider?.icon || "i-ph:key"} />
|
56 |
-
</IconButton>}
|
57 |
</>
|
58 |
)}
|
59 |
</div>
|
|
|
10 |
labelForGetApiKey?: string;
|
11 |
}
|
12 |
|
13 |
+
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
|
|
|
|
|
|
|
|
|
14 |
const [isEditing, setIsEditing] = useState(false);
|
15 |
const [tempKey, setTempKey] = useState(apiKey);
|
16 |
|
|
|
20 |
};
|
21 |
|
22 |
return (
|
23 |
+
<div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
|
24 |
+
<div>
|
25 |
+
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
26 |
+
{!isEditing && (
|
27 |
+
<div className="flex items-center mb-4">
|
28 |
+
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
|
29 |
+
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
|
30 |
+
</span>
|
31 |
+
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
|
32 |
+
<div className="i-ph:pencil-simple" />
|
33 |
+
</IconButton>
|
34 |
+
</div>
|
35 |
+
)}
|
36 |
+
</div>
|
37 |
+
|
38 |
{isEditing ? (
|
39 |
+
<div className="flex items-center gap-3 mt-2">
|
40 |
<input
|
41 |
type="password"
|
42 |
value={tempKey}
|
43 |
+
placeholder="Your API Key"
|
44 |
onChange={(e) => setTempKey(e.target.value)}
|
45 |
+
className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
|
46 |
/>
|
47 |
<IconButton onClick={handleSave} title="Save API Key">
|
48 |
<div className="i-ph:check" />
|
|
|
50 |
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
|
51 |
<div className="i-ph:x" />
|
52 |
</IconButton>
|
53 |
+
</div>
|
54 |
) : (
|
55 |
<>
|
56 |
+
{provider?.getApiKeyLink && (
|
57 |
+
<IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
|
58 |
+
<span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
59 |
+
<div className={provider?.icon || 'i-ph:key'} />
|
60 |
+
</IconButton>
|
61 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
62 |
</>
|
63 |
)}
|
64 |
</div>
|
app/components/chat/BaseChat.tsx
CHANGED
@@ -27,9 +27,9 @@ const EXAMPLE_PROMPTS = [
|
|
27 |
|
28 |
const providerList = PROVIDER_LIST;
|
29 |
|
30 |
-
const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
|
31 |
return (
|
32 |
-
<div className="mb-2 flex gap-2">
|
33 |
<select
|
34 |
value={provider?.name}
|
35 |
onChange={(e) => {
|
@@ -49,8 +49,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
|
|
49 |
key={provider?.name}
|
50 |
value={model}
|
51 |
onChange={(e) => setModel(e.target.value)}
|
52 |
-
|
53 |
-
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
|
54 |
>
|
55 |
{[...modelList]
|
56 |
.filter((e) => e.provider == provider?.name && e.name)
|
@@ -157,25 +156,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
157 |
ref={ref}
|
158 |
className={classNames(
|
159 |
styles.BaseChat,
|
160 |
-
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
161 |
)}
|
162 |
data-chat-visible={showChat}
|
163 |
>
|
164 |
<ClientOnly>{() => <Menu />}</ClientOnly>
|
165 |
-
<div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
|
166 |
-
<div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
|
167 |
{!chatStarted && (
|
168 |
-
<div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
|
169 |
-
<h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
|
170 |
Where ideas begin
|
171 |
</h1>
|
172 |
-
<p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
|
173 |
Bring ideas to life in seconds or get help on existing projects.
|
174 |
</p>
|
175 |
</div>
|
176 |
)}
|
177 |
<div
|
178 |
-
className={classNames('pt-6 px-6', {
|
179 |
'h-full flex flex-col': chatStarted,
|
180 |
})}
|
181 |
>
|
@@ -184,7 +183,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
184 |
return chatStarted ? (
|
185 |
<Messages
|
186 |
ref={messageRef}
|
187 |
-
className="flex flex-col w-full flex-1 max-w-chat
|
188 |
messages={messages}
|
189 |
isStreaming={isStreaming}
|
190 |
/>
|
@@ -193,10 +192,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
193 |
</ClientOnly>
|
194 |
<div
|
195 |
className={classNames(
|
196 |
-
'bg-bolt-elements-background-depth-2
|
197 |
{
|
198 |
-
'sticky bottom-
|
199 |
-
}
|
|
|
200 |
>
|
201 |
<ModelSelector
|
202 |
key={provider?.name + ':' + modelList.length}
|
@@ -206,7 +206,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
206 |
provider={provider}
|
207 |
setProvider={setProvider}
|
208 |
providerList={PROVIDER_LIST}
|
|
|
209 |
/>
|
|
|
210 |
{provider && (
|
211 |
<APIKeyManager
|
212 |
provider={provider}
|
@@ -214,6 +216,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
214 |
setApiKey={(key) => updateApiKey(provider.name, key)}
|
215 |
/>
|
216 |
)}
|
|
|
217 |
<div
|
218 |
className={classNames(
|
219 |
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
|
@@ -221,7 +224,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
221 |
>
|
222 |
<textarea
|
223 |
ref={textareaRef}
|
224 |
-
className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-
|
225 |
onKeyDown={(event) => {
|
226 |
if (event.key === 'Enter') {
|
227 |
if (event.shiftKey) {
|
@@ -294,7 +297,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
294 |
) : null}
|
295 |
</div>
|
296 |
</div>
|
297 |
-
<div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
|
298 |
</div>
|
299 |
</div>
|
300 |
{!chatStarted && (
|
|
|
27 |
|
28 |
const providerList = PROVIDER_LIST;
|
29 |
|
30 |
+
const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
|
31 |
return (
|
32 |
+
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
|
33 |
<select
|
34 |
value={provider?.name}
|
35 |
onChange={(e) => {
|
|
|
49 |
key={provider?.name}
|
50 |
value={model}
|
51 |
onChange={(e) => setModel(e.target.value)}
|
52 |
+
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%] "
|
|
|
53 |
>
|
54 |
{[...modelList]
|
55 |
.filter((e) => e.provider == provider?.name && e.name)
|
|
|
156 |
ref={ref}
|
157 |
className={classNames(
|
158 |
styles.BaseChat,
|
159 |
+
'relative flex flex-col lg:flex-row h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
160 |
)}
|
161 |
data-chat-visible={showChat}
|
162 |
>
|
163 |
<ClientOnly>{() => <Menu />}</ClientOnly>
|
164 |
+
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
|
165 |
+
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
|
166 |
{!chatStarted && (
|
167 |
+
<div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
|
168 |
+
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
|
169 |
Where ideas begin
|
170 |
</h1>
|
171 |
+
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
|
172 |
Bring ideas to life in seconds or get help on existing projects.
|
173 |
</p>
|
174 |
</div>
|
175 |
)}
|
176 |
<div
|
177 |
+
className={classNames('pt-6 px-2 sm:px-6', {
|
178 |
'h-full flex flex-col': chatStarted,
|
179 |
})}
|
180 |
>
|
|
|
183 |
return chatStarted ? (
|
184 |
<Messages
|
185 |
ref={messageRef}
|
186 |
+
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
|
187 |
messages={messages}
|
188 |
isStreaming={isStreaming}
|
189 |
/>
|
|
|
192 |
</ClientOnly>
|
193 |
<div
|
194 |
className={classNames(
|
195 |
+
' bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
|
196 |
{
|
197 |
+
'sticky bottom-2': chatStarted,
|
198 |
+
},
|
199 |
+
)}
|
200 |
>
|
201 |
<ModelSelector
|
202 |
key={provider?.name + ':' + modelList.length}
|
|
|
206 |
provider={provider}
|
207 |
setProvider={setProvider}
|
208 |
providerList={PROVIDER_LIST}
|
209 |
+
apiKeys={apiKeys}
|
210 |
/>
|
211 |
+
|
212 |
{provider && (
|
213 |
<APIKeyManager
|
214 |
provider={provider}
|
|
|
216 |
setApiKey={(key) => updateApiKey(provider.name, key)}
|
217 |
/>
|
218 |
)}
|
219 |
+
|
220 |
<div
|
221 |
className={classNames(
|
222 |
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
|
|
|
224 |
>
|
225 |
<textarea
|
226 |
ref={textareaRef}
|
227 |
+
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`}
|
228 |
onKeyDown={(event) => {
|
229 |
if (event.key === 'Enter') {
|
230 |
if (event.shiftKey) {
|
|
|
297 |
) : null}
|
298 |
</div>
|
299 |
</div>
|
|
|
300 |
</div>
|
301 |
</div>
|
302 |
{!chatStarted && (
|
app/components/chat/Messages.client.tsx
CHANGED
@@ -4,7 +4,7 @@ import { classNames } from '~/utils/classNames';
|
|
4 |
import { AssistantMessage } from './AssistantMessage';
|
5 |
import { UserMessage } from './UserMessage';
|
6 |
import * as Tooltip from '@radix-ui/react-tooltip';
|
7 |
-
import { useLocation
|
8 |
import { db, chatId } from '~/lib/persistence/useChatHistory';
|
9 |
import { forkChat } from '~/lib/persistence/db';
|
10 |
import { toast } from 'react-toastify';
|
@@ -19,7 +19,6 @@ interface MessagesProps {
|
|
19 |
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
20 |
const { id, isStreaming = false, messages = [] } = props;
|
21 |
const location = useLocation();
|
22 |
-
const navigate = useNavigate();
|
23 |
|
24 |
const handleRewind = (messageId: string) => {
|
25 |
const searchParams = new URLSearchParams(location.search);
|
@@ -69,53 +68,57 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|
69 |
<div className="grid grid-col-1 w-full">
|
70 |
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
71 |
</div>
|
72 |
-
{!isUserMessage && (
|
73 |
-
<
|
74 |
-
<Tooltip.
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
81 |
)}
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
</Tooltip.
|
93 |
-
</Tooltip.
|
94 |
-
</Tooltip.Root>
|
95 |
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
|
|
119 |
</div>
|
120 |
);
|
121 |
})
|
|
|
4 |
import { AssistantMessage } from './AssistantMessage';
|
5 |
import { UserMessage } from './UserMessage';
|
6 |
import * as Tooltip from '@radix-ui/react-tooltip';
|
7 |
+
import { useLocation } from '@remix-run/react';
|
8 |
import { db, chatId } from '~/lib/persistence/useChatHistory';
|
9 |
import { forkChat } from '~/lib/persistence/db';
|
10 |
import { toast } from 'react-toastify';
|
|
|
19 |
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
20 |
const { id, isStreaming = false, messages = [] } = props;
|
21 |
const location = useLocation();
|
|
|
22 |
|
23 |
const handleRewind = (messageId: string) => {
|
24 |
const searchParams = new URLSearchParams(location.search);
|
|
|
68 |
<div className="grid grid-col-1 w-full">
|
69 |
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
70 |
</div>
|
71 |
+
{!isUserMessage && (
|
72 |
+
<div className="flex gap-2 flex-col lg:flex-row">
|
73 |
+
<Tooltip.Root>
|
74 |
+
<Tooltip.Trigger asChild>
|
75 |
+
{messageId && (
|
76 |
+
<button
|
77 |
+
onClick={() => handleRewind(messageId)}
|
78 |
+
key="i-ph:arrow-u-up-left"
|
79 |
+
className={classNames(
|
80 |
+
'i-ph:arrow-u-up-left',
|
81 |
+
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
82 |
+
)}
|
83 |
+
/>
|
84 |
)}
|
85 |
+
</Tooltip.Trigger>
|
86 |
+
<Tooltip.Portal>
|
87 |
+
<Tooltip.Content
|
88 |
+
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
89 |
+
sideOffset={5}
|
90 |
+
style={{ zIndex: 1000 }}
|
91 |
+
>
|
92 |
+
Revert to this message
|
93 |
+
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
94 |
+
</Tooltip.Content>
|
95 |
+
</Tooltip.Portal>
|
96 |
+
</Tooltip.Root>
|
|
|
97 |
|
98 |
+
<Tooltip.Root>
|
99 |
+
<Tooltip.Trigger asChild>
|
100 |
+
<button
|
101 |
+
onClick={() => handleFork(messageId)}
|
102 |
+
key="i-ph:git-fork"
|
103 |
+
className={classNames(
|
104 |
+
'i-ph:git-fork',
|
105 |
+
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
106 |
+
)}
|
107 |
+
/>
|
108 |
+
</Tooltip.Trigger>
|
109 |
+
<Tooltip.Portal>
|
110 |
+
<Tooltip.Content
|
111 |
+
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
112 |
+
sideOffset={5}
|
113 |
+
style={{ zIndex: 1000 }}
|
114 |
+
>
|
115 |
+
Fork chat from this message
|
116 |
+
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
117 |
+
</Tooltip.Content>
|
118 |
+
</Tooltip.Portal>
|
119 |
+
</Tooltip.Root>
|
120 |
+
</div>
|
121 |
+
)}
|
122 |
</div>
|
123 |
);
|
124 |
})
|
app/components/header/HeaderActionButtons.client.tsx
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
import { useStore } from '@nanostores/react';
|
|
|
2 |
import { chatStore } from '~/lib/stores/chat';
|
3 |
import { workbenchStore } from '~/lib/stores/workbench';
|
4 |
import { classNames } from '~/utils/classNames';
|
@@ -9,6 +10,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
9 |
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
10 |
const { showChat } = useStore(chatStore);
|
11 |
|
|
|
|
|
12 |
const canHideChat = showWorkbench || !showChat;
|
13 |
|
14 |
return (
|
@@ -16,7 +19,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
16 |
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
17 |
<Button
|
18 |
active={showChat}
|
19 |
-
disabled={!canHideChat}
|
20 |
onClick={() => {
|
21 |
if (canHideChat) {
|
22 |
chatStore.setKey('showChat', !showChat);
|
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
+
import useViewport from '~/lib/hooks';
|
3 |
import { chatStore } from '~/lib/stores/chat';
|
4 |
import { workbenchStore } from '~/lib/stores/workbench';
|
5 |
import { classNames } from '~/utils/classNames';
|
|
|
10 |
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
11 |
const { showChat } = useStore(chatStore);
|
12 |
|
13 |
+
const isSmallViewport = useViewport(1024);
|
14 |
+
|
15 |
const canHideChat = showWorkbench || !showChat;
|
16 |
|
17 |
return (
|
|
|
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);
|
app/components/workbench/Workbench.client.tsx
CHANGED
@@ -16,6 +16,7 @@ import { cubicEasingFn } from '~/utils/easings';
|
|
16 |
import { renderLogger } from '~/utils/logger';
|
17 |
import { EditorPanel } from './EditorPanel';
|
18 |
import { Preview } from './Preview';
|
|
|
19 |
|
20 |
interface WorkspaceProps {
|
21 |
chatStarted?: boolean;
|
@@ -65,6 +66,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|
65 |
const files = useStore(workbenchStore.files);
|
66 |
const selectedView = useStore(workbenchStore.currentView);
|
67 |
|
|
|
|
|
68 |
const setSelectedView = (view: WorkbenchViewType) => {
|
69 |
workbenchStore.currentView.set(view);
|
70 |
};
|
@@ -128,18 +131,20 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|
128 |
className={classNames(
|
129 |
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
130 |
{
|
|
|
|
|
131 |
'left-[var(--workbench-left)]': showWorkbench,
|
132 |
'left-[100%]': !showWorkbench,
|
133 |
},
|
134 |
)}
|
135 |
>
|
136 |
-
<div className="absolute inset-0 px-6">
|
137 |
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
138 |
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
139 |
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
140 |
<div className="ml-auto" />
|
141 |
{selectedView === 'code' && (
|
142 |
-
|
143 |
<PanelHeaderButton
|
144 |
className="mr-1 text-sm"
|
145 |
onClick={() => {
|
@@ -165,29 +170,32 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|
165 |
<PanelHeaderButton
|
166 |
className="mr-1 text-sm"
|
167 |
onClick={() => {
|
168 |
-
const repoName = prompt(
|
|
|
|
|
|
|
169 |
if (!repoName) {
|
170 |
-
alert(
|
171 |
return;
|
172 |
}
|
173 |
-
const githubUsername = prompt(
|
174 |
if (!githubUsername) {
|
175 |
-
alert(
|
176 |
return;
|
177 |
}
|
178 |
-
const githubToken = prompt(
|
179 |
if (!githubToken) {
|
180 |
-
alert(
|
181 |
return;
|
182 |
}
|
183 |
-
|
184 |
-
|
185 |
}}
|
186 |
>
|
187 |
<div className="i-ph:github-logo" />
|
188 |
Push to GitHub
|
189 |
</PanelHeaderButton>
|
190 |
-
|
191 |
)}
|
192 |
<IconButton
|
193 |
icon="i-ph:x-circle"
|
|
|
16 |
import { renderLogger } from '~/utils/logger';
|
17 |
import { EditorPanel } from './EditorPanel';
|
18 |
import { Preview } from './Preview';
|
19 |
+
import useViewport from '~/lib/hooks';
|
20 |
|
21 |
interface WorkspaceProps {
|
22 |
chatStarted?: boolean;
|
|
|
66 |
const files = useStore(workbenchStore.files);
|
67 |
const selectedView = useStore(workbenchStore.currentView);
|
68 |
|
69 |
+
const isSmallViewport = useViewport(1024);
|
70 |
+
|
71 |
const setSelectedView = (view: WorkbenchViewType) => {
|
72 |
workbenchStore.currentView.set(view);
|
73 |
};
|
|
|
131 |
className={classNames(
|
132 |
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
133 |
{
|
134 |
+
'w-full': isSmallViewport,
|
135 |
+
'left-0': showWorkbench && isSmallViewport,
|
136 |
'left-[var(--workbench-left)]': showWorkbench,
|
137 |
'left-[100%]': !showWorkbench,
|
138 |
},
|
139 |
)}
|
140 |
>
|
141 |
+
<div className="absolute inset-0 px-2 lg:px-6">
|
142 |
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
143 |
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
144 |
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
145 |
<div className="ml-auto" />
|
146 |
{selectedView === 'code' && (
|
147 |
+
<div className="flex overflow-y-auto">
|
148 |
<PanelHeaderButton
|
149 |
className="mr-1 text-sm"
|
150 |
onClick={() => {
|
|
|
170 |
<PanelHeaderButton
|
171 |
className="mr-1 text-sm"
|
172 |
onClick={() => {
|
173 |
+
const repoName = prompt(
|
174 |
+
'Please enter a name for your new GitHub repository:',
|
175 |
+
'bolt-generated-project',
|
176 |
+
);
|
177 |
if (!repoName) {
|
178 |
+
alert('Repository name is required. Push to GitHub cancelled.');
|
179 |
return;
|
180 |
}
|
181 |
+
const githubUsername = prompt('Please enter your GitHub username:');
|
182 |
if (!githubUsername) {
|
183 |
+
alert('GitHub username is required. Push to GitHub cancelled.');
|
184 |
return;
|
185 |
}
|
186 |
+
const githubToken = prompt('Please enter your GitHub personal access token:');
|
187 |
if (!githubToken) {
|
188 |
+
alert('GitHub token is required. Push to GitHub cancelled.');
|
189 |
return;
|
190 |
}
|
191 |
+
|
192 |
+
workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
|
193 |
}}
|
194 |
>
|
195 |
<div className="i-ph:github-logo" />
|
196 |
Push to GitHub
|
197 |
</PanelHeaderButton>
|
198 |
+
</div>
|
199 |
)}
|
200 |
<IconButton
|
201 |
icon="i-ph:x-circle"
|
app/lib/hooks/index.ts
CHANGED
@@ -2,3 +2,4 @@ export * from './useMessageParser';
|
|
2 |
export * from './usePromptEnhancer';
|
3 |
export * from './useShortcuts';
|
4 |
export * from './useSnapScroll';
|
|
|
|
2 |
export * from './usePromptEnhancer';
|
3 |
export * from './useShortcuts';
|
4 |
export * from './useSnapScroll';
|
5 |
+
export { default } from './useViewport';
|
app/lib/hooks/useViewport.ts
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
|
3 |
+
const useViewport = (threshold = 1024) => {
|
4 |
+
const [isSmallViewport, setIsSmallViewport] = useState(window.innerWidth < threshold);
|
5 |
+
|
6 |
+
useEffect(() => {
|
7 |
+
const handleResize = () => setIsSmallViewport(window.innerWidth < threshold);
|
8 |
+
window.addEventListener('resize', handleResize);
|
9 |
+
|
10 |
+
return () => {
|
11 |
+
window.removeEventListener('resize', handleResize);
|
12 |
+
};
|
13 |
+
}, [threshold]);
|
14 |
+
|
15 |
+
return isSmallViewport;
|
16 |
+
};
|
17 |
+
|
18 |
+
export default useViewport;
|