Skip to content

Commit ab9d59a

Browse files
committed
feat: refactor layout and introduce workspace panel and fix some bugs
1 parent 5fa2ee5 commit ab9d59a

16 files changed

+294
-208
lines changed

packages/bolt/app/components/Header.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IconButton } from './ui/IconButton';
22

33
export function Header() {
44
return (
5-
<header className="flex items-center bg-white p-4 border-b border-gray-200">
5+
<header className="flex items-center bg-white p-4 border-b border-gray-200 h-[var(--header-height)]">
66
<div className="flex items-center gap-2">
77
<div className="text-2xl font-semibold text-accent">Bolt</div>
88
</div>

packages/bolt/app/components/chat/Artifact.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ import { workspaceStore } from '~/lib/stores/workspace';
33

44
interface ArtifactProps {
55
messageId: string;
6-
onClick?: () => void;
76
}
87

9-
export function Artifact({ messageId, onClick }: ArtifactProps) {
8+
export function Artifact({ messageId }: ArtifactProps) {
109
const artifacts = useStore(workspaceStore.artifacts);
1110

1211
const artifact = artifacts[messageId];
1312

1413
return (
15-
<button className="flex border rounded-lg overflow-hidden items-stretch bg-gray-50/25 w-full" onClick={onClick}>
16-
<div className="border-r flex items-center px-6 bg-gray-50">
14+
<button
15+
className="flex border rounded-lg overflow-hidden items-stretch bg-gray-50/25 w-full"
16+
onClick={() => {
17+
const showWorkspace = workspaceStore.showWorkspace.get();
18+
workspaceStore.showWorkspace.set(!showWorkspace);
19+
}}
20+
>
21+
<div className="border-r flex items-center px-6 bg-gray-100/50">
1722
{!artifact?.closed ? (
1823
<div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
1924
) : (

packages/bolt/app/components/chat/BaseChat.tsx

+92-75
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
import type { Message } from 'ai';
12
import type { LegacyRef } from 'react';
23
import React from 'react';
34
import { ClientOnly } from 'remix-utils/client-only';
45
import { IconButton } from '~/components/ui/IconButton';
6+
import { Workspace } from '~/components/workspace/Workspace.client';
57
import { classNames } from '~/utils/classNames';
8+
import { Messages } from './Messages.client';
69
import { SendButton } from './SendButton.client';
710

811
interface BaseChatProps {
912
textareaRef?: LegacyRef<HTMLTextAreaElement> | undefined;
1013
messagesSlot?: React.ReactNode;
1114
workspaceSlot?: React.ReactNode;
1215
chatStarted?: boolean;
16+
isStreaming?: boolean;
17+
messages?: Message[];
1318
enhancingPrompt?: boolean;
1419
promptEnhanced?: boolean;
1520
input?: string;
@@ -27,10 +32,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
2732
{
2833
textareaRef,
2934
chatStarted = false,
35+
isStreaming = false,
3036
enhancingPrompt = false,
3137
promptEnhanced = false,
32-
messagesSlot,
33-
workspaceSlot,
38+
messages,
3439
input = '',
3540
sendMessage,
3641
handleInputChange,
@@ -41,14 +46,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
4146
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
4247

4348
return (
44-
<div ref={ref} className="h-full flex w-full overflow-scroll px-6">
45-
<div className="flex flex-col items-center w-full h-full">
46-
<div id="chat" className="w-full">
49+
<div ref={ref} className="relative flex h-full w-full overflow-hidden ">
50+
<div className="flex overflow-scroll w-full h-full">
51+
<div id="chat" className="flex flex-col w-full h-full px-6">
4752
{!chatStarted && (
48-
<div id="intro" className="mt-[20vh] mb-14 max-w-2xl mx-auto">
53+
<div id="intro" className="mt-[20vh] mb-14 max-w-3xl mx-auto">
4954
<h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
5055
<p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
51-
<div className="grid max-md:grid-cols-[repeat(2,1fr)] md:grid-cols-[repeat(2,minmax(200px,1fr))] gap-4">
56+
<div className="grid max-md:grid-cols-[repeat(1,1fr)] md:grid-cols-[repeat(2,minmax(300px,1fr))] gap-4">
5257
{EXAMPLES.map((suggestion, index) => (
5358
<button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
5459
{suggestion.text}
@@ -57,83 +62,95 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
5762
</div>
5863
</div>
5964
)}
60-
{messagesSlot}
61-
</div>
62-
<div
63-
className={classNames('w-full md:max-w-[720px] mx-auto', {
64-
'fixed bg-bolt-elements-app-backgroundColor bottom-0': chatStarted,
65-
})}
66-
>
6765
<div
68-
className={classNames(
69-
'relative shadow-sm border border-gray-200 md:mb-6 bg-white rounded-lg overflow-hidden',
70-
{
71-
'max-md:rounded-none max-md:border-x-none': chatStarted,
72-
},
73-
)}
66+
className={classNames('pt-10', {
67+
'h-full flex flex-col': chatStarted,
68+
})}
7469
>
75-
<textarea
76-
ref={textareaRef}
77-
onKeyDown={(event) => {
78-
if (event.key === 'Enter') {
79-
if (event.shiftKey) {
80-
return;
81-
}
70+
<ClientOnly>
71+
{() => {
72+
return chatStarted ? (
73+
<Messages
74+
className="flex flex-col w-full flex-1 max-w-3xl px-4 pb-10 mx-auto z-1"
75+
messages={messages}
76+
isStreaming={isStreaming}
77+
/>
78+
) : null;
79+
}}
80+
</ClientOnly>
81+
<div
82+
className={classNames('relative w-full max-w-3xl md:mx-auto z-2', {
83+
'sticky bottom-0 bg-bolt-elements-app-backgroundColor': chatStarted,
84+
})}
85+
>
86+
<div
87+
className={classNames('shadow-sm mb-6 border border-gray-200 bg-white rounded-lg overflow-hidden')}
88+
>
89+
<textarea
90+
ref={textareaRef}
91+
onKeyDown={(event) => {
92+
if (event.key === 'Enter') {
93+
if (event.shiftKey) {
94+
return;
95+
}
8296

83-
event.preventDefault();
97+
event.preventDefault();
8498

85-
sendMessage?.();
86-
}
87-
}}
88-
value={input}
89-
onChange={(event) => {
90-
handleInputChange?.(event);
91-
}}
92-
className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none`}
93-
style={{
94-
minHeight: TEXTAREA_MIN_HEIGHT,
95-
maxHeight: TEXTAREA_MAX_HEIGHT,
96-
}}
97-
placeholder="How can Bolt help you today?"
98-
translate="no"
99-
/>
100-
<ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
101-
<div className="flex justify-between text-sm p-4 pt-2">
102-
<div className="flex gap-1 items-center">
103-
<IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
104-
<IconButton icon="i-ph:plus-circle-duotone" />
105-
<IconButton
106-
disabled={input.length === 0 || enhancingPrompt}
107-
className={classNames({
108-
'opacity-100!': enhancingPrompt,
109-
'text-accent! pr-1.5 enabled:hover:bg-accent/12!': promptEnhanced,
110-
})}
111-
onClick={() => enhancePrompt?.()}
112-
>
113-
{enhancingPrompt ? (
114-
<>
115-
<div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
116-
<div className="ml-1.5">Enhancing prompt...</div>
117-
</>
118-
) : (
119-
<>
120-
<div className="i-blitz:stars text-xl"></div>
121-
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
122-
</>
123-
)}
124-
</IconButton>
125-
</div>
126-
{input.length > 3 ? (
127-
<div className="text-xs">
128-
Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
129-
<kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
99+
sendMessage?.();
100+
}
101+
}}
102+
value={input}
103+
onChange={(event) => {
104+
handleInputChange?.(event);
105+
}}
106+
className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none`}
107+
style={{
108+
minHeight: TEXTAREA_MIN_HEIGHT,
109+
maxHeight: TEXTAREA_MAX_HEIGHT,
110+
}}
111+
placeholder="How can Bolt help you today?"
112+
translate="no"
113+
/>
114+
<ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
115+
<div className="flex justify-between text-sm p-4 pt-2">
116+
<div className="flex gap-1 items-center">
117+
<IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
118+
<IconButton icon="i-ph:plus-circle-duotone" />
119+
<IconButton icon="i-ph:pencil-simple-duotone" />
120+
<IconButton
121+
disabled={input.length === 0 || enhancingPrompt}
122+
className={classNames({
123+
'opacity-100!': enhancingPrompt,
124+
'text-accent! pr-1.5 enabled:hover:bg-accent/12!': promptEnhanced,
125+
})}
126+
onClick={() => enhancePrompt?.()}
127+
>
128+
{enhancingPrompt ? (
129+
<>
130+
<div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
131+
<div className="ml-1.5">Enhancing prompt...</div>
132+
</>
133+
) : (
134+
<>
135+
<div className="i-blitz:stars text-xl"></div>
136+
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
137+
</>
138+
)}
139+
</IconButton>
140+
</div>
141+
{input.length > 3 ? (
142+
<div className="text-xs">
143+
Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
144+
<kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
145+
</div>
146+
) : null}
130147
</div>
131-
) : null}
148+
</div>
132149
</div>
133150
</div>
134151
</div>
152+
<ClientOnly>{() => <Workspace chatStarted={chatStarted} />}</ClientOnly>
135153
</div>
136-
{workspaceSlot}
137154
</div>
138155
);
139156
},

packages/bolt/app/components/chat/Chat.client.tsx

+14-28
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useChat } from 'ai/react';
2-
import { cubicBezier, useAnimate } from 'framer-motion';
2+
import { useAnimate } from 'framer-motion';
33
import { useEffect, useRef, useState } from 'react';
44
import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
5+
import { cubicEasingFn } from '~/utils/easings';
56
import { createScopedLogger } from '~/utils/logger';
67
import { BaseChat } from './BaseChat';
7-
import { Messages } from './Messages';
88

99
const logger = createScopedLogger('Chat');
10-
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
1110

1211
export function Chat() {
1312
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -61,10 +60,7 @@ export function Chat() {
6160
return;
6261
}
6362

64-
await Promise.all([
65-
animate('#chat', { height: '100%' }, { duration: 0.3, ease: customEasingFn }),
66-
animate('#intro', { opacity: 0, display: 'none' }, { duration: 0.15, ease: customEasingFn }),
67-
]);
63+
await animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn });
6864

6965
setChatStarted(true);
7066
};
@@ -87,31 +83,21 @@ export function Chat() {
8783
textareaRef={textareaRef}
8884
input={input}
8985
chatStarted={chatStarted}
86+
isStreaming={isLoading}
9087
enhancingPrompt={enhancingPrompt}
9188
promptEnhanced={promptEnhanced}
9289
sendMessage={sendMessage}
9390
handleInputChange={handleInputChange}
94-
messagesSlot={
95-
chatStarted ? (
96-
<Messages
97-
classNames={{
98-
root: 'h-full pt-10',
99-
messagesContainer: 'max-w-2xl mx-auto max-md:pb-[calc(140px+1.5rem)] md:pb-[calc(140px+3rem)]',
100-
}}
101-
messages={messages.map((message, i) => {
102-
if (message.role === 'user') {
103-
return message;
104-
}
105-
106-
return {
107-
...message,
108-
content: parsedMessages[i] || '',
109-
};
110-
})}
111-
isLoading={isLoading}
112-
/>
113-
) : null
114-
}
91+
messages={messages.map((message, i) => {
92+
if (message.role === 'user') {
93+
return message;
94+
}
95+
96+
return {
97+
...message,
98+
content: parsedMessages[i] || '',
99+
};
100+
})}
115101
enhancePrompt={() => {
116102
enhancePrompt(input, (input) => {
117103
setInput(input);

packages/bolt/app/components/chat/CodeBlock.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const CodeBlock = memo(({ code, language, theme }: CodeBlockProps) => {
6464
>
6565
<button
6666
className={classNames(
67-
'flex items-center p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',
67+
'flex items-center bg-transparent p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',
6868
{
6969
'before:opacity-0': !copied,
7070
'before:opacity-100': copied,

packages/bolt/app/components/chat/Markdown.module.scss

+9-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ $color-blockquote-border: #dfe2e5;
9595
:is(ul, ol) {
9696
padding-left: 2em;
9797
margin-top: 0;
98-
margin-bottom: 16px;
98+
margin-bottom: 24px;
9999
}
100100

101101
ul {
@@ -106,6 +106,14 @@ $color-blockquote-border: #dfe2e5;
106106
list-style-type: decimal;
107107
}
108108

109+
li + li {
110+
margin-top: 8px;
111+
}
112+
113+
li > *:not(:last-child) {
114+
margin-bottom: 16px;
115+
}
116+
109117
img {
110118
max-width: 100%;
111119
box-sizing: border-box;

0 commit comments

Comments
 (0)