Dominic Elm commited on
Commit
ab9d59a
·
1 Parent(s): 5fa2ee5

feat: refactor layout and introduce workspace panel and fix some bugs

Browse files
packages/bolt/app/components/Header.tsx CHANGED
@@ -2,7 +2,7 @@ import { IconButton } from './ui/IconButton';
2
 
3
  export function Header() {
4
  return (
5
- <header className="flex items-center bg-white p-4 border-b border-gray-200">
6
  <div className="flex items-center gap-2">
7
  <div className="text-2xl font-semibold text-accent">Bolt</div>
8
  </div>
 
2
 
3
  export function Header() {
4
  return (
5
+ <header className="flex items-center bg-white p-4 border-b border-gray-200 h-[var(--header-height)]">
6
  <div className="flex items-center gap-2">
7
  <div className="text-2xl font-semibold text-accent">Bolt</div>
8
  </div>
packages/bolt/app/components/chat/Artifact.tsx CHANGED
@@ -3,17 +3,22 @@ import { workspaceStore } from '~/lib/stores/workspace';
3
 
4
  interface ArtifactProps {
5
  messageId: string;
6
- onClick?: () => void;
7
  }
8
 
9
- export function Artifact({ messageId, onClick }: ArtifactProps) {
10
  const artifacts = useStore(workspaceStore.artifacts);
11
 
12
  const artifact = artifacts[messageId];
13
 
14
  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">
 
 
 
 
 
 
17
  {!artifact?.closed ? (
18
  <div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
19
  ) : (
 
3
 
4
  interface ArtifactProps {
5
  messageId: string;
 
6
  }
7
 
8
+ export function Artifact({ messageId }: ArtifactProps) {
9
  const artifacts = useStore(workspaceStore.artifacts);
10
 
11
  const artifact = artifacts[messageId];
12
 
13
  return (
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">
22
  {!artifact?.closed ? (
23
  <div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
24
  ) : (
packages/bolt/app/components/chat/BaseChat.tsx CHANGED
@@ -1,8 +1,11 @@
 
1
  import type { LegacyRef } from 'react';
2
  import React from 'react';
3
  import { ClientOnly } from 'remix-utils/client-only';
4
  import { IconButton } from '~/components/ui/IconButton';
 
5
  import { classNames } from '~/utils/classNames';
 
6
  import { SendButton } from './SendButton.client';
7
 
8
  interface BaseChatProps {
@@ -10,6 +13,8 @@ interface BaseChatProps {
10
  messagesSlot?: React.ReactNode;
11
  workspaceSlot?: React.ReactNode;
12
  chatStarted?: boolean;
 
 
13
  enhancingPrompt?: boolean;
14
  promptEnhanced?: boolean;
15
  input?: string;
@@ -27,10 +32,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
27
  {
28
  textareaRef,
29
  chatStarted = false,
 
30
  enhancingPrompt = false,
31
  promptEnhanced = false,
32
- messagesSlot,
33
- workspaceSlot,
34
  input = '',
35
  sendMessage,
36
  handleInputChange,
@@ -41,14 +46,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
41
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
42
 
43
  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">
47
  {!chatStarted && (
48
- <div id="intro" className="mt-[20vh] mb-14 max-w-2xl mx-auto">
49
  <h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
50
  <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">
52
  {EXAMPLES.map((suggestion, index) => (
53
  <button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
54
  {suggestion.text}
@@ -57,83 +62,95 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
57
  </div>
58
  </div>
59
  )}
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
- >
67
  <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
- )}
74
  >
75
- <textarea
76
- ref={textareaRef}
77
- onKeyDown={(event) => {
78
- if (event.key === 'Enter') {
79
- if (event.shiftKey) {
80
- return;
81
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- event.preventDefault();
84
 
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
 
 
 
130
  </div>
131
- ) : null}
132
  </div>
133
  </div>
134
  </div>
 
135
  </div>
136
- {workspaceSlot}
137
  </div>
138
  );
139
  },
 
1
+ import type { Message } from 'ai';
2
  import type { LegacyRef } from 'react';
3
  import React from 'react';
4
  import { ClientOnly } from 'remix-utils/client-only';
5
  import { IconButton } from '~/components/ui/IconButton';
6
+ import { Workspace } from '~/components/workspace/Workspace.client';
7
  import { classNames } from '~/utils/classNames';
8
+ import { Messages } from './Messages.client';
9
  import { SendButton } from './SendButton.client';
10
 
11
  interface BaseChatProps {
 
13
  messagesSlot?: React.ReactNode;
14
  workspaceSlot?: React.ReactNode;
15
  chatStarted?: boolean;
16
+ isStreaming?: boolean;
17
+ messages?: Message[];
18
  enhancingPrompt?: boolean;
19
  promptEnhanced?: boolean;
20
  input?: string;
 
32
  {
33
  textareaRef,
34
  chatStarted = false,
35
+ isStreaming = false,
36
  enhancingPrompt = false,
37
  promptEnhanced = false,
38
+ messages,
 
39
  input = '',
40
  sendMessage,
41
  handleInputChange,
 
46
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
47
 
48
  return (
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">
52
  {!chatStarted && (
53
+ <div id="intro" className="mt-[20vh] mb-14 max-w-3xl mx-auto">
54
  <h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
55
  <p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
56
+ <div className="grid max-md:grid-cols-[repeat(1,1fr)] md:grid-cols-[repeat(2,minmax(300px,1fr))] gap-4">
57
  {EXAMPLES.map((suggestion, index) => (
58
  <button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
59
  {suggestion.text}
 
62
  </div>
63
  </div>
64
  )}
 
 
 
 
 
 
 
65
  <div
66
+ className={classNames('pt-10', {
67
+ 'h-full flex flex-col': chatStarted,
68
+ })}
 
 
 
69
  >
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
+ }
96
 
97
+ event.preventDefault();
98
 
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}
147
  </div>
148
+ </div>
149
  </div>
150
  </div>
151
  </div>
152
+ <ClientOnly>{() => <Workspace chatStarted={chatStarted} />}</ClientOnly>
153
  </div>
 
154
  </div>
155
  );
156
  },
packages/bolt/app/components/chat/Chat.client.tsx CHANGED
@@ -1,13 +1,12 @@
1
  import { useChat } from 'ai/react';
2
- import { cubicBezier, useAnimate } from 'framer-motion';
3
  import { useEffect, useRef, useState } from 'react';
4
  import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
 
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { BaseChat } from './BaseChat';
7
- import { Messages } from './Messages';
8
 
9
  const logger = createScopedLogger('Chat');
10
- const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
11
 
12
  export function Chat() {
13
  const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -61,10 +60,7 @@ export function Chat() {
61
  return;
62
  }
63
 
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
- ]);
68
 
69
  setChatStarted(true);
70
  };
@@ -87,31 +83,21 @@ export function Chat() {
87
  textareaRef={textareaRef}
88
  input={input}
89
  chatStarted={chatStarted}
 
90
  enhancingPrompt={enhancingPrompt}
91
  promptEnhanced={promptEnhanced}
92
  sendMessage={sendMessage}
93
  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
- }
115
  enhancePrompt={() => {
116
  enhancePrompt(input, (input) => {
117
  setInput(input);
 
1
  import { useChat } from 'ai/react';
2
+ import { useAnimate } from 'framer-motion';
3
  import { useEffect, useRef, useState } from 'react';
4
  import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
5
+ import { cubicEasingFn } from '~/utils/easings';
6
  import { createScopedLogger } from '~/utils/logger';
7
  import { BaseChat } from './BaseChat';
 
8
 
9
  const logger = createScopedLogger('Chat');
 
10
 
11
  export function Chat() {
12
  const textareaRef = useRef<HTMLTextAreaElement>(null);
 
60
  return;
61
  }
62
 
63
+ await animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn });
 
 
 
64
 
65
  setChatStarted(true);
66
  };
 
83
  textareaRef={textareaRef}
84
  input={input}
85
  chatStarted={chatStarted}
86
+ isStreaming={isLoading}
87
  enhancingPrompt={enhancingPrompt}
88
  promptEnhanced={promptEnhanced}
89
  sendMessage={sendMessage}
90
  handleInputChange={handleInputChange}
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
+ })}
 
 
 
 
 
 
 
 
 
 
 
101
  enhancePrompt={() => {
102
  enhancePrompt(input, (input) => {
103
  setInput(input);
packages/bolt/app/components/chat/CodeBlock.tsx CHANGED
@@ -64,7 +64,7 @@ export const CodeBlock = memo(({ code, language, theme }: CodeBlockProps) => {
64
  >
65
  <button
66
  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',
68
  {
69
  'before:opacity-0': !copied,
70
  'before:opacity-100': copied,
 
64
  >
65
  <button
66
  className={classNames(
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',
68
  {
69
  'before:opacity-0': !copied,
70
  'before:opacity-100': copied,
packages/bolt/app/components/chat/Markdown.module.scss CHANGED
@@ -95,7 +95,7 @@ $color-blockquote-border: #dfe2e5;
95
  :is(ul, ol) {
96
  padding-left: 2em;
97
  margin-top: 0;
98
- margin-bottom: 16px;
99
  }
100
 
101
  ul {
@@ -106,6 +106,14 @@ $color-blockquote-border: #dfe2e5;
106
  list-style-type: decimal;
107
  }
108
 
 
 
 
 
 
 
 
 
109
  img {
110
  max-width: 100%;
111
  box-sizing: border-box;
 
95
  :is(ul, ol) {
96
  padding-left: 2em;
97
  margin-top: 0;
98
+ margin-bottom: 24px;
99
  }
100
 
101
  ul {
 
106
  list-style-type: decimal;
107
  }
108
 
109
+ li + li {
110
+ margin-top: 8px;
111
+ }
112
+
113
+ li > *:not(:last-child) {
114
+ margin-bottom: 16px;
115
+ }
116
+
117
  img {
118
  max-width: 100%;
119
  box-sizing: border-box;
packages/bolt/app/components/chat/Markdown.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { memo } from 'react';
2
- import ReactMarkdown from 'react-markdown';
3
  import type { BundledLanguage } from 'shiki';
4
  import { createScopedLogger } from '~/utils/logger';
5
  import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
@@ -16,47 +16,51 @@ interface MarkdownProps {
16
  export const Markdown = memo(({ children }: MarkdownProps) => {
17
  logger.trace('Render');
18
 
19
- return (
20
- <ReactMarkdown
21
- className={styles.MarkdownContent}
22
- components={{
23
- div: ({ className, children, node, ...props }) => {
24
- if (className?.includes('__boltArtifact__')) {
25
- const messageId = node?.properties.dataMessageId as string;
26
 
27
- if (!messageId) {
28
- logger.warn(`Invalud message id ${messageId}`);
29
- }
30
-
31
- return <Artifact messageId={messageId} />;
32
  }
33
 
34
- return (
35
- <div className={className} {...props}>
36
- {children}
37
- </div>
38
- );
39
- },
40
- pre: (props) => {
41
- const { children, node, ...rest } = props;
42
 
43
- const [firstChild] = node?.children ?? [];
 
 
 
 
 
 
 
44
 
45
- if (
46
- firstChild &&
47
- firstChild.type === 'element' &&
48
- firstChild.tagName === 'code' &&
49
- firstChild.children[0].type === 'text'
50
- ) {
51
- const { className, ...rest } = firstChild.properties;
52
- const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
53
 
54
- return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
55
- }
 
 
 
 
 
 
56
 
57
- return <pre {...rest}>{children}</pre>;
58
- },
59
- }}
 
 
 
 
 
 
 
 
 
60
  remarkPlugins={remarkPlugins}
61
  rehypePlugins={rehypePlugins}
62
  >
 
1
+ import { memo, useMemo } from 'react';
2
+ import ReactMarkdown, { type Components } from 'react-markdown';
3
  import type { BundledLanguage } from 'shiki';
4
  import { createScopedLogger } from '~/utils/logger';
5
  import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
 
16
  export const Markdown = memo(({ children }: MarkdownProps) => {
17
  logger.trace('Render');
18
 
19
+ const components = useMemo<Components>(() => {
20
+ return {
21
+ div: ({ className, children, node, ...props }) => {
22
+ if (className?.includes('__boltArtifact__')) {
23
+ const messageId = node?.properties.dataMessageId as string;
 
 
24
 
25
+ if (!messageId) {
26
+ logger.warn(`Invalud message id ${messageId}`);
 
 
 
27
  }
28
 
29
+ return <Artifact messageId={messageId} />;
30
+ }
 
 
 
 
 
 
31
 
32
+ return (
33
+ <div className={className} {...props}>
34
+ {children}
35
+ </div>
36
+ );
37
+ },
38
+ pre: (props) => {
39
+ const { children, node, ...rest } = props;
40
 
41
+ const [firstChild] = node?.children ?? [];
 
 
 
 
 
 
 
42
 
43
+ if (
44
+ firstChild &&
45
+ firstChild.type === 'element' &&
46
+ firstChild.tagName === 'code' &&
47
+ firstChild.children[0].type === 'text'
48
+ ) {
49
+ const { className, ...rest } = firstChild.properties;
50
+ const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
51
 
52
+ return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
53
+ }
54
+
55
+ return <pre {...rest}>{children}</pre>;
56
+ },
57
+ };
58
+ }, []);
59
+
60
+ return (
61
+ <ReactMarkdown
62
+ className={styles.MarkdownContent}
63
+ components={components}
64
  remarkPlugins={remarkPlugins}
65
  rehypePlugins={rehypePlugins}
66
  >
packages/bolt/app/components/chat/{Messages.tsx → Messages.client.tsx} RENAMED
@@ -1,55 +1,62 @@
1
  import type { Message } from 'ai';
2
- import { useRef } from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
6
 
7
  interface MessagesProps {
8
  id?: string;
9
- classNames?: { root?: string; messagesContainer?: string };
10
- isLoading?: boolean;
11
  messages?: Message[];
12
  }
13
 
14
  export function Messages(props: MessagesProps) {
15
- const { id, isLoading, messages = [] } = props;
16
-
17
- const containerRef = useRef<HTMLDivElement>(null);
18
 
19
  return (
20
- <div id={id} ref={containerRef} className={props.classNames?.root}>
21
- <div className={classNames('flex flex-col', props.classNames?.messagesContainer)}>
22
- {messages.length > 0
23
- ? messages.map((message, i) => {
24
- const { role, content } = message;
25
- const isUser = role === 'user';
26
- const isFirst = i === 0;
 
 
27
 
28
- return (
 
 
 
 
 
 
 
 
29
  <div
30
- key={message.id}
31
- className={classNames('flex gap-4 border rounded-md p-6 bg-white/80 backdrop-blur-sm', {
32
- 'mt-4': !isFirst,
33
  })}
34
  >
35
  <div
36
  className={classNames(
37
  'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
38
  {
39
- 'bg-gray-100': role === 'user',
40
- 'bg-accent text-xl': role === 'assistant',
41
  },
42
  )}
43
  >
44
- <div className={role === 'user' ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
45
  </div>
46
  {isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
47
  </div>
48
- );
49
- })
50
- : null}
51
- {isLoading && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
52
- </div>
53
  </div>
54
  );
55
  }
 
1
  import type { Message } from 'ai';
 
2
  import { classNames } from '~/utils/classNames';
3
  import { AssistantMessage } from './AssistantMessage';
4
  import { UserMessage } from './UserMessage';
5
 
6
  interface MessagesProps {
7
  id?: string;
8
+ className?: string;
9
+ isStreaming?: boolean;
10
  messages?: Message[];
11
  }
12
 
13
  export function Messages(props: MessagesProps) {
14
+ const { id, isStreaming = false, messages = [] } = props;
 
 
15
 
16
  return (
17
+ <div id={id} className={props.className}>
18
+ {messages.length > 0
19
+ ? messages.map((message, i) => {
20
+ const { role, content } = message;
21
+ const isUser = role === 'user';
22
+ const isFirst = i === 0;
23
+ const isLast = i === messages.length - 1;
24
+ const isUserMessage = message.role === 'user';
25
+ const isAssistantMessage = message.role === 'assistant';
26
 
27
+ return (
28
+ <div
29
+ key={message.id}
30
+ className={classNames('relative overflow-hidden rounded-md p-[1px]', {
31
+ 'mt-4': !isFirst,
32
+ 'bg-gray-200': isUserMessage || !isStreaming || (isStreaming && isAssistantMessage && !isLast),
33
+ 'bg-gradient-to-b from-gray-200 to-transparent': isStreaming && isAssistantMessage && isLast,
34
+ })}
35
+ >
36
  <div
37
+ className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.375rem-1px)]', {
38
+ 'bg-white': isUserMessage || !isStreaming || (isStreaming && !isLast),
39
+ 'bg-gradient-to-b from-white from-30% to-transparent': isStreaming && isLast,
40
  })}
41
  >
42
  <div
43
  className={classNames(
44
  'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
45
  {
46
+ 'bg-gray-100': isUserMessage,
47
+ 'bg-accent text-xl': isAssistantMessage,
48
  },
49
  )}
50
  >
51
+ <div className={isUserMessage ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
52
  </div>
53
  {isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
54
  </div>
55
+ </div>
56
+ );
57
+ })
58
+ : null}
59
+ {isStreaming && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
60
  </div>
61
  );
62
  }
packages/bolt/app/components/ui/IconButton.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { memo } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
 
4
- type IconSize = 'sm' | 'md' | 'xl';
5
 
6
  interface BaseIconButtonProps {
7
  size?: IconSize;
@@ -64,7 +64,9 @@ function getIconSize(size: IconSize) {
64
  return 'text-sm';
65
  } else if (size === 'md') {
66
  return 'text-md';
67
- } else {
68
  return 'text-xl';
 
 
69
  }
70
  }
 
1
  import { memo } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
 
4
+ type IconSize = 'sm' | 'md' | 'xl' | 'xxl';
5
 
6
  interface BaseIconButtonProps {
7
  size?: IconSize;
 
64
  return 'text-sm';
65
  } else if (size === 'md') {
66
  return 'text-md';
67
+ } else if (size === 'xl') {
68
  return 'text-xl';
69
+ } else {
70
+ return 'text-2xl';
71
  }
72
  }
packages/bolt/app/components/workspace/Workspace.client.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useStore } from '@nanostores/react';
2
+ import { AnimatePresence, motion, type Variants } from 'framer-motion';
3
+ import { IconButton } from '~/components/ui/IconButton';
4
+ import { cubicEasingFn } from '~/utils/easings';
5
+ import { workspaceStore } from '../../lib/stores/workspace';
6
+
7
+ interface WorkspaceProps {
8
+ chatStarted?: boolean;
9
+ }
10
+
11
+ const workspaceVariants = {
12
+ closed: {
13
+ width: 0,
14
+ transition: {
15
+ duration: 0.2,
16
+ ease: cubicEasingFn,
17
+ },
18
+ },
19
+ open: {
20
+ width: '100%',
21
+ transition: {
22
+ duration: 0.5,
23
+ type: 'spring',
24
+ },
25
+ },
26
+ } satisfies Variants;
27
+
28
+ export function Workspace({ chatStarted }: WorkspaceProps) {
29
+ const showWorkspace = useStore(workspaceStore.showWorkspace);
30
+
31
+ return (
32
+ chatStarted && (
33
+ <AnimatePresence>
34
+ {showWorkspace && (
35
+ <motion.div initial="closed" animate="open" exit="closed" variants={workspaceVariants}>
36
+ <div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[50vw] mr-4 z-0">
37
+ <div className="bg-white border border-gray-200 shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8">
38
+ <header className="px-3 py-2 border-b border-gray-200">
39
+ <IconButton
40
+ icon="i-ph:x-circle"
41
+ className="ml-auto"
42
+ size="xxl"
43
+ onClick={() => {
44
+ workspaceStore.showWorkspace.set(false);
45
+ }}
46
+ />
47
+ </header>
48
+ </div>
49
+ </div>
50
+ </motion.div>
51
+ )}
52
+ </AnimatePresence>
53
+ )
54
+ );
55
+ }
packages/bolt/app/components/workspace/WorkspacePanel.tsx DELETED
@@ -1,3 +0,0 @@
1
- export function Workspace() {
2
- return <div>WORKSPACE PANEL</div>;
3
- }
 
 
 
 
packages/bolt/app/routes/api.enhancer.ts CHANGED
@@ -19,7 +19,7 @@ export async function action({ context, request }: ActionFunctionArgs) {
19
  {
20
  role: 'user',
21
  content: stripIndents`
22
- I want you to improve the following prompt.
23
 
24
  IMPORTANT: Only respond with the improved prompt and nothing else!
25
 
 
19
  {
20
  role: 'user',
21
  content: stripIndents`
22
+ I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
23
 
24
  IMPORTANT: Only respond with the improved prompt and nothing else!
25
 
packages/bolt/app/styles/index.scss CHANGED
@@ -14,7 +14,7 @@ body {
14
  mask: linear-gradient(-25deg, transparent 60%, white);
15
  pointer-events: none;
16
  position: fixed;
17
- top: 0;
18
  transform-style: flat;
19
  width: 100vw;
20
  z-index: -1;
 
14
  mask: linear-gradient(-25deg, transparent 60%, white);
15
  pointer-events: none;
16
  position: fixed;
17
+ top: -8px;
18
  transform-style: flat;
19
  width: 100vw;
20
  z-index: -1;
packages/bolt/app/styles/variables.scss CHANGED
@@ -16,6 +16,8 @@
16
  * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
17
  */
18
  :root {
 
 
19
  /* App */
20
  --bolt-elements-app-backgroundColor: var(--bolt-background-primary);
21
  }
 
16
  * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
17
  */
18
  :root {
19
+ --header-height: 65px;
20
+
21
  /* App */
22
  --bolt-elements-app-backgroundColor: var(--bolt-background-primary);
23
  }
packages/bolt/app/utils/easings.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import { cubicBezier } from 'framer-motion';
2
+
3
+ export const cubicEasingFn = cubicBezier(0.4, 0, 0.2, 1);