Dominic Elm commited on
Commit
d5a29c2
·
unverified ·
1 Parent(s): 8fd9d44

feat(layout): allow to minimize chat (#35)

Browse files
packages/bolt/README.md CHANGED
@@ -30,10 +30,11 @@ pnpm install
30
  ANTHROPIC_API_KEY=XXX
31
  ```
32
 
33
- Optionally, you an set the debug level:
34
 
35
  ```
36
  VITE_LOG_LEVEL=debug
 
37
  ```
38
 
39
  If you want to run authentication against a local StackBlitz instance, add:
 
30
  ANTHROPIC_API_KEY=XXX
31
  ```
32
 
33
+ Optionally, you an set the debug level or disable authentication:
34
 
35
  ```
36
  VITE_LOG_LEVEL=debug
37
+ VITE_DISABLE_AUTH=1
38
  ```
39
 
40
  If you want to run authentication against a local StackBlitz instance, add:
packages/bolt/app/components/chat/BaseChat.module.scss ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .BaseChat {
2
+ &[data-chat-visible='false'] {
3
+ --workbench-inner-width: 100%;
4
+ --workbench-left: 0;
5
+
6
+ .Chat {
7
+ --at-apply: bolt-ease-cubic-bezier;
8
+ transition-property: transform, opacity;
9
+ transition-duration: 0.3s;
10
+ will-change: transform, opacity;
11
+ transform: translateX(-50%);
12
+ opacity: 0;
13
+ }
14
+ }
15
+ }
16
+
17
+ .Chat {
18
+ opacity: 1;
19
+ }
packages/bolt/app/components/chat/BaseChat.tsx CHANGED
@@ -8,10 +8,13 @@ import { classNames } from '~/utils/classNames';
8
  import { Messages } from './Messages.client';
9
  import { SendButton } from './SendButton.client';
10
 
 
 
11
  interface BaseChatProps {
12
  textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
13
  messageRef?: RefCallback<HTMLDivElement> | undefined;
14
  scrollRef?: RefCallback<HTMLDivElement> | undefined;
 
15
  chatStarted?: boolean;
16
  isStreaming?: boolean;
17
  messages?: Message[];
@@ -40,6 +43,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
40
  textareaRef,
41
  messageRef,
42
  scrollRef,
 
43
  chatStarted = false,
44
  isStreaming = false,
45
  enhancingPrompt = false,
@@ -56,12 +60,19 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
56
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
57
 
58
  return (
59
- <div ref={ref} className="relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1">
 
 
 
 
 
 
 
60
  <ClientOnly>{() => <Menu />}</ClientOnly>
61
  <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
62
- <div className="flex flex-col w-full h-full px-6">
63
  {!chatStarted && (
64
- <div id="intro" className="mt-[26vh] max-w-2xl mx-auto">
65
  <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
66
  Where ideas begin
67
  </h1>
@@ -71,7 +82,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
71
  </div>
72
  )}
73
  <div
74
- className={classNames('pt-6', {
75
  'h-full flex flex-col': chatStarted,
76
  })}
77
  >
@@ -80,7 +91,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
80
  return chatStarted ? (
81
  <Messages
82
  ref={messageRef}
83
- className="flex flex-col w-full flex-1 max-w-2xl px-4 pb-6 mx-auto z-1"
84
  messages={messages}
85
  isStreaming={isStreaming}
86
  />
@@ -88,7 +99,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
88
  }}
89
  </ClientOnly>
90
  <div
91
- className={classNames('relative w-full max-w-2xl md:mx-auto z-2', {
92
  'sticky bottom-0': chatStarted,
93
  })}
94
  >
@@ -174,7 +185,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
174
  </div>
175
  </div>
176
  {!chatStarted && (
177
- <div id="examples" className="relative w-full max-w-2xl mx-auto mt-8 flex justify-center">
178
  <div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
179
  {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
180
  return (
 
8
  import { Messages } from './Messages.client';
9
  import { SendButton } from './SendButton.client';
10
 
11
+ import styles from './BaseChat.module.scss';
12
+
13
  interface BaseChatProps {
14
  textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
15
  messageRef?: RefCallback<HTMLDivElement> | undefined;
16
  scrollRef?: RefCallback<HTMLDivElement> | undefined;
17
+ showChat?: boolean;
18
  chatStarted?: boolean;
19
  isStreaming?: boolean;
20
  messages?: Message[];
 
43
  textareaRef,
44
  messageRef,
45
  scrollRef,
46
+ showChat = true,
47
  chatStarted = false,
48
  isStreaming = false,
49
  enhancingPrompt = false,
 
60
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
61
 
62
  return (
63
+ <div
64
+ ref={ref}
65
+ className={classNames(
66
+ styles.BaseChat,
67
+ 'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
68
+ )}
69
+ data-chat-visible={showChat}
70
+ >
71
  <ClientOnly>{() => <Menu />}</ClientOnly>
72
  <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
73
+ <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
74
  {!chatStarted && (
75
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto">
76
  <h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
77
  Where ideas begin
78
  </h1>
 
82
  </div>
83
  )}
84
  <div
85
+ className={classNames('pt-6 px-6', {
86
  'h-full flex flex-col': chatStarted,
87
  })}
88
  >
 
91
  return chatStarted ? (
92
  <Messages
93
  ref={messageRef}
94
+ className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
95
  messages={messages}
96
  isStreaming={isStreaming}
97
  />
 
99
  }}
100
  </ClientOnly>
101
  <div
102
+ className={classNames('relative w-full max-w-chat mx-auto z-prompt', {
103
  'sticky bottom-0': chatStarted,
104
  })}
105
  >
 
185
  </div>
186
  </div>
187
  {!chatStarted && (
188
+ <div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
189
  <div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
190
  {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
191
  return (
packages/bolt/app/components/chat/Chat.client.tsx CHANGED
@@ -1,8 +1,10 @@
 
1
  import type { Message } from 'ai';
2
  import { useChat } from 'ai/react';
3
  import { useAnimate } from 'framer-motion';
4
  import { memo, useEffect, useRef, useState } from 'react';
5
  import { cssTransition, toast, ToastContainer } from 'react-toastify';
 
6
  import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
7
  import { useChatHistory } from '~/lib/persistence';
8
  import { chatStore } from '~/lib/stores/chat';
@@ -11,7 +13,6 @@ import { fileModificationsToHTML } from '~/utils/diff';
11
  import { cubicEasingFn } from '~/utils/easings';
12
  import { createScopedLogger, renderLogger } from '~/utils/logger';
13
  import { BaseChat } from './BaseChat';
14
- import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
15
 
16
  const toastAnimation = cssTransition({
17
  enter: 'animated fadeInRight',
@@ -71,6 +72,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
71
 
72
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
73
 
 
 
74
  const [animationScope, animate] = useAnimate();
75
 
76
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
@@ -213,6 +216,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
213
  ref={animationScope}
214
  textareaRef={textareaRef}
215
  input={input}
 
216
  chatStarted={chatStarted}
217
  isStreaming={isLoading}
218
  enhancingPrompt={enhancingPrompt}
 
1
+ import { useStore } from '@nanostores/react';
2
  import type { Message } from 'ai';
3
  import { useChat } from 'ai/react';
4
  import { useAnimate } from 'framer-motion';
5
  import { memo, useEffect, useRef, useState } from 'react';
6
  import { cssTransition, toast, ToastContainer } from 'react-toastify';
7
+ import { AnalyticsAction, AnalyticsTrackEvent, sendAnalyticsEvent } from '~/lib/analytics';
8
  import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
9
  import { useChatHistory } from '~/lib/persistence';
10
  import { chatStore } from '~/lib/stores/chat';
 
13
  import { cubicEasingFn } from '~/utils/easings';
14
  import { createScopedLogger, renderLogger } from '~/utils/logger';
15
  import { BaseChat } from './BaseChat';
 
16
 
17
  const toastAnimation = cssTransition({
18
  enter: 'animated fadeInRight',
 
72
 
73
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
74
 
75
+ const { showChat } = useStore(chatStore);
76
+
77
  const [animationScope, animate] = useAnimate();
78
 
79
  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
 
216
  ref={animationScope}
217
  textareaRef={textareaRef}
218
  input={input}
219
+ showChat={showChat}
220
  chatStarted={chatStarted}
221
  isStreaming={isLoading}
222
  enhancingPrompt={enhancingPrompt}
packages/bolt/app/components/header/Header.tsx CHANGED
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
2
  import { ClientOnly } from 'remix-utils/client-only';
3
  import { chatStore } from '~/lib/stores/chat';
4
  import { classNames } from '~/utils/classNames';
5
- import { OpenStackBlitz } from './OpenStackBlitz.client';
6
 
7
  export function Header() {
8
  const chat = useStore(chatStore);
@@ -17,14 +17,22 @@ export function Header() {
17
  },
18
  )}
19
  >
20
- <div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary">
 
21
  <a href="/" className="text-2xl font-semibold text-accent flex items-center">
22
  <span className="i-bolt:logo-text?mask w-[46px] inline-block" />
23
  </a>
24
  </div>
25
- <div className="ml-auto flex gap-2">
26
- <ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
27
- </div>
 
 
 
 
 
 
 
28
  </header>
29
  );
30
  }
 
2
  import { ClientOnly } from 'remix-utils/client-only';
3
  import { chatStore } from '~/lib/stores/chat';
4
  import { classNames } from '~/utils/classNames';
5
+ import { HeaderActionButtons } from './HeaderActionButtons.client';
6
 
7
  export function Header() {
8
  const chat = useStore(chatStore);
 
17
  },
18
  )}
19
  >
20
+ <div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
21
+ <div className="i-ph:sidebar-simple-duotone text-xl" />
22
  <a href="/" className="text-2xl font-semibold text-accent flex items-center">
23
  <span className="i-bolt:logo-text?mask w-[46px] inline-block" />
24
  </a>
25
  </div>
26
+ <div className="flex-1" />
27
+ {chat.started && (
28
+ <ClientOnly>
29
+ {() => (
30
+ <div className="mr-1">
31
+ <HeaderActionButtons />
32
+ </div>
33
+ )}
34
+ </ClientOnly>
35
+ )}
36
  </header>
37
  );
38
  }
packages/bolt/app/components/header/HeaderActionButtons.client.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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';
5
+ import { OpenStackBlitz } from './OpenStackBlitz.client';
6
+
7
+ interface HeaderActionButtonsProps {}
8
+
9
+ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
10
+ const showWorkbench = useStore(workbenchStore.showWorkbench);
11
+ const { showChat } = useStore(chatStore);
12
+
13
+ const canHideChat = showWorkbench || !showChat;
14
+
15
+ return (
16
+ <div className="flex">
17
+ <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
18
+ <Button
19
+ active={showChat}
20
+ disabled={!canHideChat}
21
+ onClick={() => {
22
+ if (canHideChat) {
23
+ chatStore.setKey('showChat', !showChat);
24
+ }
25
+ }}
26
+ >
27
+ <div className="i-bolt:chat text-sm" />
28
+ </Button>
29
+ <div className="w-[1px] bg-bolt-elements-borderColor" />
30
+ <Button
31
+ active={showWorkbench}
32
+ onClick={() => {
33
+ if (showWorkbench && !showChat) {
34
+ chatStore.setKey('showChat', true);
35
+ }
36
+
37
+ workbenchStore.showWorkbench.set(!showWorkbench);
38
+ }}
39
+ >
40
+ <div className="i-ph:code-bold" />
41
+ </Button>
42
+ </div>
43
+ <div className="flex ml-2">
44
+ <OpenStackBlitz />
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ interface ButtonProps {
51
+ active?: boolean;
52
+ disabled?: boolean;
53
+ children?: any;
54
+ onClick?: VoidFunction;
55
+ }
56
+
57
+ function Button({ active = false, disabled = false, children, onClick }: ButtonProps) {
58
+ return (
59
+ <button
60
+ className={classNames('flex items-center p-1.5', {
61
+ 'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
62
+ !active,
63
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
64
+ 'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
65
+ disabled,
66
+ })}
67
+ onClick={onClick}
68
+ >
69
+ {children}
70
+ </button>
71
+ );
72
+ }
packages/bolt/app/components/header/OpenStackBlitz.client.tsx CHANGED
@@ -1,10 +1,11 @@
1
- import path from 'path';
2
  import { useStore } from '@nanostores/react';
3
  import sdk from '@stackblitz/sdk';
 
 
4
  import type { FileMap } from '~/lib/stores/files';
5
  import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
 
6
  import { WORK_DIR } from '~/utils/constants';
7
- import { memo, useCallback, useEffect, useState } from 'react';
8
 
9
  // extract relative path and content from file, wrapped in array for flatMap use
10
  const extractContent = ([file, value]: [string, FileMap[string]]) => {
@@ -47,6 +48,8 @@ const useFirstArtifact = (): [boolean, ArtifactState | undefined] => {
47
  export const OpenStackBlitz = memo(() => {
48
  const [artifactLoaded, artifact] = useFirstArtifact();
49
 
 
 
50
  const handleClick = useCallback(() => {
51
  if (!artifact) {
52
  return;
@@ -66,13 +69,34 @@ export const OpenStackBlitz = memo(() => {
66
  });
67
  }, [artifact]);
68
 
69
- if (!artifactLoaded) {
70
- return null;
71
- }
72
-
73
  return (
74
- <a onClick={handleClick} className="cursor-pointer">
75
- <img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" />
76
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  );
78
  });
 
 
1
  import { useStore } from '@nanostores/react';
2
  import sdk from '@stackblitz/sdk';
3
+ import path from 'path';
4
+ import { memo, useCallback, useEffect, useState } from 'react';
5
  import type { FileMap } from '~/lib/stores/files';
6
  import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
7
+ import { classNames } from '~/utils/classNames';
8
  import { WORK_DIR } from '~/utils/constants';
 
9
 
10
  // extract relative path and content from file, wrapped in array for flatMap use
11
  const extractContent = ([file, value]: [string, FileMap[string]]) => {
 
48
  export const OpenStackBlitz = memo(() => {
49
  const [artifactLoaded, artifact] = useFirstArtifact();
50
 
51
+ const disabled = !artifactLoaded;
52
+
53
  const handleClick = useCallback(() => {
54
  if (!artifact) {
55
  return;
 
69
  });
70
  }, [artifact]);
71
 
 
 
 
 
72
  return (
73
+ <button
74
+ className={classNames(
75
+ 'relative flex items-stretch p-[1px] overflow-hidden text-xs text-bolt-elements-cta-text rounded-lg bg-bolt-elements-borderColor dark:bg-gray-800',
76
+ {
77
+ 'cursor-not-allowed opacity-50': disabled,
78
+ 'group hover:bg-gradient-to-t from-accent-900 to-accent-500 hover:text-white': !disabled,
79
+ },
80
+ )}
81
+ onClick={handleClick}
82
+ disabled={disabled}
83
+ >
84
+ <div
85
+ className={classNames(
86
+ 'flex items-center gap-1.5 px-3 bg-bolt-elements-cta-background dark:bg-alpha-gray-80 group-hover:bg-transparent rounded-[calc(0.5rem-1px)] group-hover:bg-opacity-0',
87
+ {
88
+ 'opacity-50': disabled,
89
+ },
90
+ )}
91
+ >
92
+ <svg width="11" height="16">
93
+ <path
94
+ fill="currentColor"
95
+ d="M4.67 9.85a.3.3 0 0 0-.27-.4H.67a.3.3 0 0 1-.21-.49l7.36-7.9c.22-.24.6 0 .5.3l-1.75 4.8a.3.3 0 0 0 .28.39h3.72c.26 0 .4.3.22.49l-7.37 7.9c-.21.24-.6 0-.49-.3l1.74-4.8Z"
96
+ />
97
+ </svg>
98
+ <span>Open in StackBlitz</span>
99
+ </div>
100
+ </button>
101
  );
102
  });
packages/bolt/app/components/sidebar/Menu.client.tsx CHANGED
@@ -50,7 +50,7 @@ export function Menu() {
50
  }, [open]);
51
 
52
  useEffect(() => {
53
- const enterThreshold = 80;
54
  const exitThreshold = 40;
55
 
56
  function onMouseMove(event: MouseEvent) {
 
50
  }, [open]);
51
 
52
  useEffect(() => {
53
+ const enterThreshold = 40;
54
  const exitThreshold = 40;
55
 
56
  function onMouseMove(event: MouseEvent) {
packages/bolt/app/components/ui/PanelHeaderButton.tsx CHANGED
@@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
14
  return (
15
  <button
16
  className={classNames(
17
- 'flex items-center gap-1.5 px-1.5 rounded-md py-0.5 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
18
  {
19
  [classNames('opacity-30', disabledClassName)]: disabled,
20
  },
 
14
  return (
15
  <button
16
  className={classNames(
17
+ 'flex items-center shrink-0 gap-1.5 px-1.5 rounded-md py-0.5 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
18
  {
19
  [classNames('opacity-30', disabledClassName)]: disabled,
20
  },
packages/bolt/app/components/ui/Slider.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import { motion } from 'framer-motion';
2
  import { memo } from 'react';
3
  import { classNames } from '~/utils/classNames';
 
4
  import { genericMemo } from '~/utils/react';
5
 
6
  interface SliderOption<T> {
@@ -23,7 +24,7 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
23
  const isLeftSelected = selected === options.left.value;
24
 
25
  return (
26
- <div className="flex items-center flex-wrap gap-1 bg-bolt-elements-background-depth-1 rounded-full p-1">
27
  <SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
28
  {options.left.text}
29
  </SliderButton>
@@ -55,7 +56,7 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
55
  {selected && (
56
  <motion.span
57
  layoutId="pill-tab"
58
- transition={{ type: 'spring', duration: 0.5 }}
59
  className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
60
  ></motion.span>
61
  )}
 
1
  import { motion } from 'framer-motion';
2
  import { memo } from 'react';
3
  import { classNames } from '~/utils/classNames';
4
+ import { cubicEasingFn } from '~/utils/easings';
5
  import { genericMemo } from '~/utils/react';
6
 
7
  interface SliderOption<T> {
 
24
  const isLeftSelected = selected === options.left.value;
25
 
26
  return (
27
+ <div className="flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1">
28
  <SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
29
  {options.left.text}
30
  </SliderButton>
 
56
  {selected && (
57
  <motion.span
58
  layoutId="pill-tab"
59
+ transition={{ duration: 0.2, ease: cubicEasingFn }}
60
  className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
61
  ></motion.span>
62
  )}
packages/bolt/app/components/workbench/EditorPanel.tsx CHANGED
@@ -17,9 +17,10 @@ import type { FileMap } from '~/lib/stores/files';
17
  import { themeStore } from '~/lib/stores/theme';
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
  import { classNames } from '~/utils/classNames';
 
20
  import { renderLogger } from '~/utils/logger';
21
  import { isMobile } from '~/utils/mobile';
22
- import { FileTreePanel } from './FileTreePanel';
23
  import { Terminal, type TerminalRef } from './terminal/Terminal';
24
 
25
  interface EditorPanelProps {
@@ -124,22 +125,24 @@ export const EditorPanel = memo(
124
  <PanelGroup direction="vertical">
125
  <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
126
  <PanelGroup direction="horizontal">
127
- <Panel defaultSize={25} minSize={10} collapsible>
128
  <div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
129
  <PanelHeader>
130
  <div className="i-ph:tree-structure-duotone shrink-0" />
131
  Files
132
  </PanelHeader>
133
- <FileTreePanel
 
134
  files={files}
135
  unsavedFiles={unsavedFiles}
 
136
  selectedFile={selectedFile}
137
  onFileSelect={onFileSelect}
138
  />
139
  </div>
140
  </Panel>
141
  <PanelResizeHandle />
142
- <Panel className="flex flex-col" defaultSize={75} minSize={20}>
143
  <PanelHeader>
144
  {activeFile && (
145
  <div className="flex items-center flex-1 text-sm">
 
17
  import { themeStore } from '~/lib/stores/theme';
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
  import { classNames } from '~/utils/classNames';
20
+ import { WORK_DIR } from '~/utils/constants';
21
  import { renderLogger } from '~/utils/logger';
22
  import { isMobile } from '~/utils/mobile';
23
+ import { FileTree } from './FileTree';
24
  import { Terminal, type TerminalRef } from './terminal/Terminal';
25
 
26
  interface EditorPanelProps {
 
125
  <PanelGroup direction="vertical">
126
  <Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
127
  <PanelGroup direction="horizontal">
128
+ <Panel defaultSize={20} minSize={10} collapsible>
129
  <div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
130
  <PanelHeader>
131
  <div className="i-ph:tree-structure-duotone shrink-0" />
132
  Files
133
  </PanelHeader>
134
+ <FileTree
135
+ className="h-full"
136
  files={files}
137
  unsavedFiles={unsavedFiles}
138
+ rootFolder={WORK_DIR}
139
  selectedFile={selectedFile}
140
  onFileSelect={onFileSelect}
141
  />
142
  </div>
143
  </Panel>
144
  <PanelResizeHandle />
145
+ <Panel className="flex flex-col" defaultSize={80} minSize={20}>
146
  <PanelHeader>
147
  {activeFile && (
148
  <div className="flex items-center flex-1 text-sm">
packages/bolt/app/components/workbench/FileTreePanel.tsx DELETED
@@ -1,29 +0,0 @@
1
- import { memo } from 'react';
2
- import type { FileMap } from '~/lib/stores/files';
3
- import { WORK_DIR } from '~/utils/constants';
4
- import { renderLogger } from '~/utils/logger';
5
- import { FileTree } from './FileTree';
6
-
7
- interface FileTreePanelProps {
8
- files?: FileMap;
9
- selectedFile?: string;
10
- unsavedFiles?: Set<string>;
11
- onFileSelect?: (value?: string) => void;
12
- }
13
-
14
- export const FileTreePanel = memo(({ files, unsavedFiles, selectedFile, onFileSelect }: FileTreePanelProps) => {
15
- renderLogger.trace('FileTreePanel');
16
-
17
- return (
18
- <div className="flex-1 overflow-y-scroll">
19
- <FileTree
20
- className="h-full"
21
- files={files}
22
- unsavedFiles={unsavedFiles}
23
- rootFolder={WORK_DIR}
24
- selectedFile={selectedFile}
25
- onFileSelect={onFileSelect}
26
- />
27
- </div>
28
- );
29
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/bolt/app/components/workbench/Preview.tsx CHANGED
@@ -82,11 +82,11 @@ export const Preview = memo(() => {
82
  />
83
  </div>
84
  </div>
85
- <div className="flex-1 bg-white border-t">
86
  {activePreview ? (
87
- <iframe ref={iframeRef} className="border-none w-full h-full" src={iframeUrl} />
88
  ) : (
89
- <div className="flex w-full h-full justify-center items-center">No preview available</div>
90
  )}
91
  </div>
92
  </div>
 
82
  />
83
  </div>
84
  </div>
85
+ <div className="flex-1 border-t border-bolt-elements-borderColor">
86
  {activePreview ? (
87
+ <iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
88
  ) : (
89
+ <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
90
  )}
91
  </div>
92
  </div>
packages/bolt/app/components/workbench/Workbench.client.tsx CHANGED
@@ -11,6 +11,7 @@ import { IconButton } from '~/components/ui/IconButton';
11
  import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
12
  import { Slider, type SliderOptions } from '~/components/ui/Slider';
13
  import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
 
14
  import { cubicEasingFn } from '~/utils/easings';
15
  import { renderLogger } from '~/utils/logger';
16
  import { EditorPanel } from './EditorPanel';
@@ -43,7 +44,7 @@ const workbenchVariants = {
43
  },
44
  },
45
  open: {
46
- width: '100%',
47
  transition: {
48
  duration: 0.2,
49
  ease: cubicEasingFn,
@@ -100,53 +101,71 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
100
 
101
  return (
102
  chatStarted && (
103
- <motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}>
104
- <div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
105
- <div className="flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8">
106
- <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
107
- <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
108
- <PanelHeaderButton
109
- className="ml-auto mr-1 text-sm"
110
- onClick={() => {
111
- workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
112
- }}
113
- >
114
- <div className="i-ph:terminal" />
115
- Toggle Terminal
116
- </PanelHeaderButton>
117
- <IconButton
118
- icon="i-ph:x-circle"
119
- className="-mr-1"
120
- size="xl"
121
- onClick={() => {
122
- workbenchStore.showWorkbench.set(false);
123
- }}
124
- />
125
- </div>
126
- <div className="relative flex-1 overflow-hidden">
127
- <View
128
- initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
129
- animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
130
- >
131
- <EditorPanel
132
- editorDocument={currentDocument}
133
- isStreaming={isStreaming}
134
- selectedFile={selectedFile}
135
- files={files}
136
- unsavedFiles={unsavedFiles}
137
- onFileSelect={onFileSelect}
138
- onEditorScroll={onEditorScroll}
139
- onEditorChange={onEditorChange}
140
- onFileSave={onFileSave}
141
- onFileReset={onFileReset}
142
  />
143
- </View>
144
- <View
145
- initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
146
- animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
147
- >
148
- <Preview />
149
- </View>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
  </div>
152
  </div>
 
11
  import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
12
  import { Slider, type SliderOptions } from '~/components/ui/Slider';
13
  import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
14
+ import { classNames } from '~/utils/classNames';
15
  import { cubicEasingFn } from '~/utils/easings';
16
  import { renderLogger } from '~/utils/logger';
17
  import { EditorPanel } from './EditorPanel';
 
44
  },
45
  },
46
  open: {
47
+ width: 'var(--workbench-width)',
48
  transition: {
49
  duration: 0.2,
50
  ease: cubicEasingFn,
 
101
 
102
  return (
103
  chatStarted && (
104
+ <motion.div
105
+ initial="closed"
106
+ animate={showWorkbench ? 'open' : 'closed'}
107
+ variants={workbenchVariants}
108
+ className="z-workbench"
109
+ >
110
+ <div
111
+ className={classNames(
112
+ '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',
113
+ {
114
+ 'left-[var(--workbench-left)]': showWorkbench,
115
+ 'left-[100%]': !showWorkbench,
116
+ },
117
+ )}
118
+ >
119
+ <div className="absolute inset-0 px-6">
120
+ <div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
121
+ <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
122
+ <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
123
+ <div className="ml-auto" />
124
+ {selectedView === 'code' && (
125
+ <PanelHeaderButton
126
+ className="mr-1 text-sm"
127
+ onClick={() => {
128
+ workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
129
+ }}
130
+ >
131
+ <div className="i-ph:terminal" />
132
+ Toggle Terminal
133
+ </PanelHeaderButton>
134
+ )}
135
+ <IconButton
136
+ icon="i-ph:x-circle"
137
+ className="-mr-1"
138
+ size="xl"
139
+ onClick={() => {
140
+ workbenchStore.showWorkbench.set(false);
141
+ }}
 
142
  />
143
+ </div>
144
+ <div className="relative flex-1 overflow-hidden">
145
+ <View
146
+ initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
147
+ animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
148
+ >
149
+ <EditorPanel
150
+ editorDocument={currentDocument}
151
+ isStreaming={isStreaming}
152
+ selectedFile={selectedFile}
153
+ files={files}
154
+ unsavedFiles={unsavedFiles}
155
+ onFileSelect={onFileSelect}
156
+ onEditorScroll={onEditorScroll}
157
+ onEditorChange={onEditorChange}
158
+ onFileSave={onFileSave}
159
+ onFileReset={onFileReset}
160
+ />
161
+ </View>
162
+ <View
163
+ initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
164
+ animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
165
+ >
166
+ <Preview />
167
+ </View>
168
+ </div>
169
  </div>
170
  </div>
171
  </div>
packages/bolt/app/lib/stores/chat.ts CHANGED
@@ -3,4 +3,5 @@ import { map } from 'nanostores';
3
  export const chatStore = map({
4
  started: false,
5
  aborted: false,
 
6
  });
 
3
  export const chatStore = map({
4
  started: false,
5
  aborted: false,
6
+ showChat: true,
7
  });
packages/bolt/app/styles/variables.scss CHANGED
@@ -161,8 +161,8 @@
161
  --bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
162
  --bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
163
 
164
- --bolt-elements-cta-background: theme('colors.gray.100');
165
- --bolt-elements-cta-text: theme('colors.gray.950');
166
 
167
  /* Terminal Colors */
168
  --bolt-terminal-background: var(--bolt-elements-terminals-background);
@@ -193,6 +193,11 @@
193
  */
194
  :root {
195
  --header-height: 54px;
 
 
 
 
 
196
 
197
  /* Toasts */
198
  --toastify-color-progress-success: var(--bolt-elements-icon-success);
 
161
  --bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
162
  --bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
163
 
164
+ --bolt-elements-cta-background: theme('colors.alpha.white.10');
165
+ --bolt-elements-cta-text: theme('colors.white');
166
 
167
  /* Terminal Colors */
168
  --bolt-terminal-background: var(--bolt-elements-terminals-background);
 
193
  */
194
  :root {
195
  --header-height: 54px;
196
+ --chat-max-width: 37rem;
197
+ --chat-min-width: 640px;
198
+ --workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);
199
+ --workbench-inner-width: var(--workbench-width);
200
+ --workbench-left: calc(100% - var(--workbench-width));
201
 
202
  /* Toasts */
203
  --toastify-color-progress-success: var(--bolt-elements-icon-success);
packages/bolt/app/styles/z-index.scss CHANGED
@@ -8,6 +8,14 @@ $zIndexMax: 999;
8
  z-index: $zIndexMax - 2;
9
  }
10
 
 
 
 
 
 
 
 
 
11
  .z-max {
12
  z-index: $zIndexMax;
13
  }
 
8
  z-index: $zIndexMax - 2;
9
  }
10
 
11
+ .z-prompt {
12
+ z-index: 2;
13
+ }
14
+
15
+ .z-workbench {
16
+ z-index: 3;
17
+ }
18
+
19
  .z-max {
20
  z-index: $zIndexMax;
21
  }
packages/bolt/uno.config.ts CHANGED
@@ -99,9 +99,10 @@ const COLOR_PRIMITIVES = {
99
 
100
  export default defineConfig({
101
  shortcuts: {
102
- 'transition-theme':
103
- 'transition-[background-color,border-color,color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]',
104
  kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
 
105
  },
106
  theme: {
107
  colors: {
 
99
 
100
  export default defineConfig({
101
  shortcuts: {
102
+ 'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
103
+ 'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
104
  kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
105
+ 'max-w-chat': 'max-w-[var(--chat-max-width)]',
106
  },
107
  theme: {
108
  colors: {