mirror of
https://github.com/stackblitz/bolt.new
synced 2024-11-27 14:32:46 +00:00
feat(layout): allow to minimize chat (#35)
This commit is contained in:
parent
8fd9d4477e
commit
d5a29c2427
@ -30,10 +30,11 @@ pnpm install
|
|||||||
ANTHROPIC_API_KEY=XXX
|
ANTHROPIC_API_KEY=XXX
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally, you an set the debug level:
|
Optionally, you an set the debug level or disable authentication:
|
||||||
|
|
||||||
```
|
```
|
||||||
VITE_LOG_LEVEL=debug
|
VITE_LOG_LEVEL=debug
|
||||||
|
VITE_DISABLE_AUTH=1
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to run authentication against a local StackBlitz instance, add:
|
If you want to run authentication against a local StackBlitz instance, add:
|
||||||
|
19
packages/bolt/app/components/chat/BaseChat.module.scss
Normal file
19
packages/bolt/app/components/chat/BaseChat.module.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.BaseChat {
|
||||||
|
&[data-chat-visible='false'] {
|
||||||
|
--workbench-inner-width: 100%;
|
||||||
|
--workbench-left: 0;
|
||||||
|
|
||||||
|
.Chat {
|
||||||
|
--at-apply: bolt-ease-cubic-bezier;
|
||||||
|
transition-property: transform, opacity;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Chat {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
@ -8,10 +8,13 @@ import { classNames } from '~/utils/classNames';
|
|||||||
import { Messages } from './Messages.client';
|
import { Messages } from './Messages.client';
|
||||||
import { SendButton } from './SendButton.client';
|
import { SendButton } from './SendButton.client';
|
||||||
|
|
||||||
|
import styles from './BaseChat.module.scss';
|
||||||
|
|
||||||
interface BaseChatProps {
|
interface BaseChatProps {
|
||||||
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
||||||
messageRef?: RefCallback<HTMLDivElement> | undefined;
|
messageRef?: RefCallback<HTMLDivElement> | undefined;
|
||||||
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
||||||
|
showChat?: boolean;
|
||||||
chatStarted?: boolean;
|
chatStarted?: boolean;
|
||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
@ -40,6 +43,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
textareaRef,
|
textareaRef,
|
||||||
messageRef,
|
messageRef,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
|
showChat = true,
|
||||||
chatStarted = false,
|
chatStarted = false,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
enhancingPrompt = false,
|
enhancingPrompt = false,
|
||||||
@ -56,12 +60,19 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1">
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(
|
||||||
|
styles.BaseChat,
|
||||||
|
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
||||||
|
)}
|
||||||
|
data-chat-visible={showChat}
|
||||||
|
>
|
||||||
<ClientOnly>{() => <Menu />}</ClientOnly>
|
<ClientOnly>{() => <Menu />}</ClientOnly>
|
||||||
<div ref={scrollRef} className="flex overflow-scroll w-full h-full">
|
<div ref={scrollRef} className="flex overflow-scroll w-full h-full">
|
||||||
<div className="flex flex-col w-full h-full px-6">
|
<div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
|
||||||
{!chatStarted && (
|
{!chatStarted && (
|
||||||
<div id="intro" className="mt-[26vh] max-w-2xl mx-auto">
|
<div id="intro" className="mt-[26vh] max-w-chat mx-auto">
|
||||||
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
|
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
|
||||||
Where ideas begin
|
Where ideas begin
|
||||||
</h1>
|
</h1>
|
||||||
@ -71,7 +82,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames('pt-6', {
|
className={classNames('pt-6 px-6', {
|
||||||
'h-full flex flex-col': chatStarted,
|
'h-full flex flex-col': chatStarted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -80,7 +91,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
return chatStarted ? (
|
return chatStarted ? (
|
||||||
<Messages
|
<Messages
|
||||||
ref={messageRef}
|
ref={messageRef}
|
||||||
className="flex flex-col w-full flex-1 max-w-2xl px-4 pb-6 mx-auto z-1"
|
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
|
||||||
messages={messages}
|
messages={messages}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
/>
|
/>
|
||||||
@ -88,7 +99,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
}}
|
}}
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div
|
<div
|
||||||
className={classNames('relative w-full max-w-2xl md:mx-auto z-2', {
|
className={classNames('relative w-full max-w-chat mx-auto z-prompt', {
|
||||||
'sticky bottom-0': chatStarted,
|
'sticky bottom-0': chatStarted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -174,7 +185,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!chatStarted && (
|
{!chatStarted && (
|
||||||
<div id="examples" className="relative w-full max-w-2xl mx-auto mt-8 flex justify-center">
|
<div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
|
||||||
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
|
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
|
||||||
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import type { Message } from 'ai';
|
import type { Message } from 'ai';
|
||||||
import { useChat } from 'ai/react';
|
import { useChat } from 'ai/react';
|
||||||
import { useAnimate } from 'framer-motion';
|
import { useAnimate } from 'framer-motion';
|
||||||
import { memo, useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||||
|
import { AnalyticsAction, AnalyticsTrackEvent, sendAnalyticsEvent } from '~/lib/analytics';
|
||||||
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
|
||||||
import { useChatHistory } from '~/lib/persistence';
|
import { useChatHistory } from '~/lib/persistence';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
@ -11,7 +13,6 @@ import { fileModificationsToHTML } from '~/utils/diff';
|
|||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
||||||
import { BaseChat } from './BaseChat';
|
import { BaseChat } from './BaseChat';
|
||||||
import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
|
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
@ -71,6 +72,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
|||||||
|
|
||||||
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
||||||
|
|
||||||
|
const { showChat } = useStore(chatStore);
|
||||||
|
|
||||||
const [animationScope, animate] = useAnimate();
|
const [animationScope, animate] = useAnimate();
|
||||||
|
|
||||||
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
|
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
|
||||||
@ -213,6 +216,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
|||||||
ref={animationScope}
|
ref={animationScope}
|
||||||
textareaRef={textareaRef}
|
textareaRef={textareaRef}
|
||||||
input={input}
|
input={input}
|
||||||
|
showChat={showChat}
|
||||||
chatStarted={chatStarted}
|
chatStarted={chatStarted}
|
||||||
isStreaming={isLoading}
|
isStreaming={isLoading}
|
||||||
enhancingPrompt={enhancingPrompt}
|
enhancingPrompt={enhancingPrompt}
|
||||||
|
@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { ClientOnly } from 'remix-utils/client-only';
|
import { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
import { HeaderActionButtons } from './HeaderActionButtons.client';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const chat = useStore(chatStore);
|
const chat = useStore(chatStore);
|
||||||
@ -17,14 +17,22 @@ export function Header() {
|
|||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary">
|
<div className="flex items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
|
||||||
|
<div className="i-ph:sidebar-simple-duotone text-xl" />
|
||||||
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
|
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
|
||||||
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
<span className="i-bolt:logo-text?mask w-[46px] inline-block" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex gap-2">
|
<div className="flex-1" />
|
||||||
<ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
|
{chat.started && (
|
||||||
</div>
|
<ClientOnly>
|
||||||
|
{() => (
|
||||||
|
<div className="mr-1">
|
||||||
|
<HeaderActionButtons />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ClientOnly>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { OpenStackBlitz } from './OpenStackBlitz.client';
|
||||||
|
|
||||||
|
interface HeaderActionButtonsProps {}
|
||||||
|
|
||||||
|
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
||||||
|
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||||
|
const { showChat } = useStore(chatStore);
|
||||||
|
|
||||||
|
const canHideChat = showWorkbench || !showChat;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
||||||
|
<Button
|
||||||
|
active={showChat}
|
||||||
|
disabled={!canHideChat}
|
||||||
|
onClick={() => {
|
||||||
|
if (canHideChat) {
|
||||||
|
chatStore.setKey('showChat', !showChat);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="i-bolt:chat text-sm" />
|
||||||
|
</Button>
|
||||||
|
<div className="w-[1px] bg-bolt-elements-borderColor" />
|
||||||
|
<Button
|
||||||
|
active={showWorkbench}
|
||||||
|
onClick={() => {
|
||||||
|
if (showWorkbench && !showChat) {
|
||||||
|
chatStore.setKey('showChat', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
workbenchStore.showWorkbench.set(!showWorkbench);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="i-ph:code-bold" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex ml-2">
|
||||||
|
<OpenStackBlitz />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
children?: any;
|
||||||
|
onClick?: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Button({ active = false, disabled = false, children, onClick }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={classNames('flex items-center p-1.5', {
|
||||||
|
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
||||||
|
!active,
|
||||||
|
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
||||||
|
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
||||||
|
disabled,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import path from 'path';
|
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import sdk from '@stackblitz/sdk';
|
import sdk from '@stackblitz/sdk';
|
||||||
|
import path from 'path';
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import type { FileMap } from '~/lib/stores/files';
|
import type { FileMap } from '~/lib/stores/files';
|
||||||
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
import { workbenchStore, type ArtifactState } from '~/lib/stores/workbench';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
// extract relative path and content from file, wrapped in array for flatMap use
|
// extract relative path and content from file, wrapped in array for flatMap use
|
||||||
const extractContent = ([file, value]: [string, FileMap[string]]) => {
|
const extractContent = ([file, value]: [string, FileMap[string]]) => {
|
||||||
@ -47,6 +48,8 @@ const useFirstArtifact = (): [boolean, ArtifactState | undefined] => {
|
|||||||
export const OpenStackBlitz = memo(() => {
|
export const OpenStackBlitz = memo(() => {
|
||||||
const [artifactLoaded, artifact] = useFirstArtifact();
|
const [artifactLoaded, artifact] = useFirstArtifact();
|
||||||
|
|
||||||
|
const disabled = !artifactLoaded;
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
return;
|
return;
|
||||||
@ -66,13 +69,34 @@ export const OpenStackBlitz = memo(() => {
|
|||||||
});
|
});
|
||||||
}, [artifact]);
|
}, [artifact]);
|
||||||
|
|
||||||
if (!artifactLoaded) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a onClick={handleClick} className="cursor-pointer">
|
<button
|
||||||
<img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" />
|
className={classNames(
|
||||||
</a>
|
'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',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed opacity-50': disabled,
|
||||||
|
'group hover:bg-gradient-to-t from-accent-900 to-accent-500 hover:text-white': !disabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'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',
|
||||||
|
{
|
||||||
|
'opacity-50': disabled,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg width="11" height="16">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Open in StackBlitz</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,7 @@ export function Menu() {
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const enterThreshold = 80;
|
const enterThreshold = 40;
|
||||||
const exitThreshold = 40;
|
const exitThreshold = 40;
|
||||||
|
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
|
@ -14,7 +14,7 @@ export const PanelHeaderButton = memo(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'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',
|
'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',
|
||||||
{
|
{
|
||||||
[classNames('opacity-30', disabledClassName)]: disabled,
|
[classNames('opacity-30', disabledClassName)]: disabled,
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { genericMemo } from '~/utils/react';
|
import { genericMemo } from '~/utils/react';
|
||||||
|
|
||||||
interface SliderOption<T> {
|
interface SliderOption<T> {
|
||||||
@ -23,7 +24,7 @@ export const Slider = genericMemo(<T,>({ selected, options, setSelected }: Slide
|
|||||||
const isLeftSelected = selected === options.left.value;
|
const isLeftSelected = selected === options.left.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center flex-wrap gap-1 bg-bolt-elements-background-depth-1 rounded-full p-1">
|
<div className="flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1">
|
||||||
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
|
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
|
||||||
{options.left.text}
|
{options.left.text}
|
||||||
</SliderButton>
|
</SliderButton>
|
||||||
@ -55,7 +56,7 @@ const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProp
|
|||||||
{selected && (
|
{selected && (
|
||||||
<motion.span
|
<motion.span
|
||||||
layoutId="pill-tab"
|
layoutId="pill-tab"
|
||||||
transition={{ type: 'spring', duration: 0.5 }}
|
transition={{ duration: 0.2, ease: cubicEasingFn }}
|
||||||
className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
|
className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
|
||||||
></motion.span>
|
></motion.span>
|
||||||
)}
|
)}
|
||||||
|
@ -17,9 +17,10 @@ import type { FileMap } from '~/lib/stores/files';
|
|||||||
import { themeStore } from '~/lib/stores/theme';
|
import { themeStore } from '~/lib/stores/theme';
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { renderLogger } from '~/utils/logger';
|
import { renderLogger } from '~/utils/logger';
|
||||||
import { isMobile } from '~/utils/mobile';
|
import { isMobile } from '~/utils/mobile';
|
||||||
import { FileTreePanel } from './FileTreePanel';
|
import { FileTree } from './FileTree';
|
||||||
import { Terminal, type TerminalRef } from './terminal/Terminal';
|
import { Terminal, type TerminalRef } from './terminal/Terminal';
|
||||||
|
|
||||||
interface EditorPanelProps {
|
interface EditorPanelProps {
|
||||||
@ -124,22 +125,24 @@ export const EditorPanel = memo(
|
|||||||
<PanelGroup direction="vertical">
|
<PanelGroup direction="vertical">
|
||||||
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
<Panel defaultSize={showTerminal ? DEFAULT_EDITOR_SIZE : 100} minSize={20}>
|
||||||
<PanelGroup direction="horizontal">
|
<PanelGroup direction="horizontal">
|
||||||
<Panel defaultSize={25} minSize={10} collapsible>
|
<Panel defaultSize={20} minSize={10} collapsible>
|
||||||
<div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
|
<div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
|
||||||
<PanelHeader>
|
<PanelHeader>
|
||||||
<div className="i-ph:tree-structure-duotone shrink-0" />
|
<div className="i-ph:tree-structure-duotone shrink-0" />
|
||||||
Files
|
Files
|
||||||
</PanelHeader>
|
</PanelHeader>
|
||||||
<FileTreePanel
|
<FileTree
|
||||||
|
className="h-full"
|
||||||
files={files}
|
files={files}
|
||||||
unsavedFiles={unsavedFiles}
|
unsavedFiles={unsavedFiles}
|
||||||
|
rootFolder={WORK_DIR}
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
onFileSelect={onFileSelect}
|
onFileSelect={onFileSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
<PanelResizeHandle />
|
<PanelResizeHandle />
|
||||||
<Panel className="flex flex-col" defaultSize={75} minSize={20}>
|
<Panel className="flex flex-col" defaultSize={80} minSize={20}>
|
||||||
<PanelHeader>
|
<PanelHeader>
|
||||||
{activeFile && (
|
{activeFile && (
|
||||||
<div className="flex items-center flex-1 text-sm">
|
<div className="flex items-center flex-1 text-sm">
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import type { FileMap } from '~/lib/stores/files';
|
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
|
||||||
import { renderLogger } from '~/utils/logger';
|
|
||||||
import { FileTree } from './FileTree';
|
|
||||||
|
|
||||||
interface FileTreePanelProps {
|
|
||||||
files?: FileMap;
|
|
||||||
selectedFile?: string;
|
|
||||||
unsavedFiles?: Set<string>;
|
|
||||||
onFileSelect?: (value?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileTreePanel = memo(({ files, unsavedFiles, selectedFile, onFileSelect }: FileTreePanelProps) => {
|
|
||||||
renderLogger.trace('FileTreePanel');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-1 overflow-y-scroll">
|
|
||||||
<FileTree
|
|
||||||
className="h-full"
|
|
||||||
files={files}
|
|
||||||
unsavedFiles={unsavedFiles}
|
|
||||||
rootFolder={WORK_DIR}
|
|
||||||
selectedFile={selectedFile}
|
|
||||||
onFileSelect={onFileSelect}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -82,11 +82,11 @@ export const Preview = memo(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 bg-white border-t">
|
<div className="flex-1 border-t border-bolt-elements-borderColor">
|
||||||
{activePreview ? (
|
{activePreview ? (
|
||||||
<iframe ref={iframeRef} className="border-none w-full h-full" src={iframeUrl} />
|
<iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full h-full justify-center items-center">No preview available</div>
|
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ import { IconButton } from '~/components/ui/IconButton';
|
|||||||
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton';
|
||||||
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
import { Slider, type SliderOptions } from '~/components/ui/Slider';
|
||||||
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
import { workbenchStore, type WorkbenchViewType } from '~/lib/stores/workbench';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { renderLogger } from '~/utils/logger';
|
import { renderLogger } from '~/utils/logger';
|
||||||
import { EditorPanel } from './EditorPanel';
|
import { EditorPanel } from './EditorPanel';
|
||||||
@ -43,7 +44,7 @@ const workbenchVariants = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
open: {
|
open: {
|
||||||
width: '100%',
|
width: 'var(--workbench-width)',
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
ease: cubicEasingFn,
|
ease: cubicEasingFn,
|
||||||
@ -100,53 +101,71 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
chatStarted && (
|
chatStarted && (
|
||||||
<motion.div initial="closed" animate={showWorkbench ? 'open' : 'closed'} variants={workbenchVariants}>
|
<motion.div
|
||||||
<div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-[calc(1.5rem-1px)] w-[50vw] mr-4 z-0">
|
initial="closed"
|
||||||
<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">
|
animate={showWorkbench ? 'open' : 'closed'}
|
||||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
variants={workbenchVariants}
|
||||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
className="z-workbench"
|
||||||
<PanelHeaderButton
|
>
|
||||||
className="ml-auto mr-1 text-sm"
|
<div
|
||||||
onClick={() => {
|
className={classNames(
|
||||||
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
'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',
|
||||||
}}
|
{
|
||||||
>
|
'left-[var(--workbench-left)]': showWorkbench,
|
||||||
<div className="i-ph:terminal" />
|
'left-[100%]': !showWorkbench,
|
||||||
Toggle Terminal
|
},
|
||||||
</PanelHeaderButton>
|
)}
|
||||||
<IconButton
|
>
|
||||||
icon="i-ph:x-circle"
|
<div className="absolute inset-0 px-6">
|
||||||
className="-mr-1"
|
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
||||||
size="xl"
|
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
||||||
onClick={() => {
|
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||||
workbenchStore.showWorkbench.set(false);
|
<div className="ml-auto" />
|
||||||
}}
|
{selectedView === 'code' && (
|
||||||
/>
|
<PanelHeaderButton
|
||||||
</div>
|
className="mr-1 text-sm"
|
||||||
<div className="relative flex-1 overflow-hidden">
|
onClick={() => {
|
||||||
<View
|
workbenchStore.toggleTerminal(!workbenchStore.showTerminal.get());
|
||||||
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
}}
|
||||||
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
>
|
||||||
>
|
<div className="i-ph:terminal" />
|
||||||
<EditorPanel
|
Toggle Terminal
|
||||||
editorDocument={currentDocument}
|
</PanelHeaderButton>
|
||||||
isStreaming={isStreaming}
|
)}
|
||||||
selectedFile={selectedFile}
|
<IconButton
|
||||||
files={files}
|
icon="i-ph:x-circle"
|
||||||
unsavedFiles={unsavedFiles}
|
className="-mr-1"
|
||||||
onFileSelect={onFileSelect}
|
size="xl"
|
||||||
onEditorScroll={onEditorScroll}
|
onClick={() => {
|
||||||
onEditorChange={onEditorChange}
|
workbenchStore.showWorkbench.set(false);
|
||||||
onFileSave={onFileSave}
|
}}
|
||||||
onFileReset={onFileReset}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</div>
|
||||||
<View
|
<div className="relative flex-1 overflow-hidden">
|
||||||
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
<View
|
||||||
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
initial={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||||
>
|
animate={{ x: selectedView === 'code' ? 0 : '-100%' }}
|
||||||
<Preview />
|
>
|
||||||
</View>
|
<EditorPanel
|
||||||
|
editorDocument={currentDocument}
|
||||||
|
isStreaming={isStreaming}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
files={files}
|
||||||
|
unsavedFiles={unsavedFiles}
|
||||||
|
onFileSelect={onFileSelect}
|
||||||
|
onEditorScroll={onEditorScroll}
|
||||||
|
onEditorChange={onEditorChange}
|
||||||
|
onFileSave={onFileSave}
|
||||||
|
onFileReset={onFileReset}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
initial={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||||
|
animate={{ x: selectedView === 'preview' ? 0 : '100%' }}
|
||||||
|
>
|
||||||
|
<Preview />
|
||||||
|
</View>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,4 +3,5 @@ import { map } from 'nanostores';
|
|||||||
export const chatStore = map({
|
export const chatStore = map({
|
||||||
started: false,
|
started: false,
|
||||||
aborted: false,
|
aborted: false,
|
||||||
|
showChat: true,
|
||||||
});
|
});
|
||||||
|
@ -161,8 +161,8 @@
|
|||||||
--bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
|
--bolt-elements-terminals-background: var(--bolt-elements-bg-depth-1);
|
||||||
--bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
|
--bolt-elements-terminals-buttonBackground: var(--bolt-elements-bg-depth-3);
|
||||||
|
|
||||||
--bolt-elements-cta-background: theme('colors.gray.100');
|
--bolt-elements-cta-background: theme('colors.alpha.white.10');
|
||||||
--bolt-elements-cta-text: theme('colors.gray.950');
|
--bolt-elements-cta-text: theme('colors.white');
|
||||||
|
|
||||||
/* Terminal Colors */
|
/* Terminal Colors */
|
||||||
--bolt-terminal-background: var(--bolt-elements-terminals-background);
|
--bolt-terminal-background: var(--bolt-elements-terminals-background);
|
||||||
@ -193,6 +193,11 @@
|
|||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
--header-height: 54px;
|
--header-height: 54px;
|
||||||
|
--chat-max-width: 37rem;
|
||||||
|
--chat-min-width: 640px;
|
||||||
|
--workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);
|
||||||
|
--workbench-inner-width: var(--workbench-width);
|
||||||
|
--workbench-left: calc(100% - var(--workbench-width));
|
||||||
|
|
||||||
/* Toasts */
|
/* Toasts */
|
||||||
--toastify-color-progress-success: var(--bolt-elements-icon-success);
|
--toastify-color-progress-success: var(--bolt-elements-icon-success);
|
||||||
|
@ -8,6 +8,14 @@ $zIndexMax: 999;
|
|||||||
z-index: $zIndexMax - 2;
|
z-index: $zIndexMax - 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.z-prompt {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-workbench {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.z-max {
|
.z-max {
|
||||||
z-index: $zIndexMax;
|
z-index: $zIndexMax;
|
||||||
}
|
}
|
||||||
|
@ -99,9 +99,10 @@ const COLOR_PRIMITIVES = {
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
'transition-theme':
|
'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||||
'transition-[background-color,border-color,color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]',
|
'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
|
||||||
kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
|
kdb: 'bg-bolt-elements-code-background text-bolt-elements-code-text py-1 px-1.5 rounded-md',
|
||||||
|
'max-w-chat': 'max-w-[var(--chat-max-width)]',
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
|
Loading…
Reference in New Issue
Block a user