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 +1 -1
- packages/bolt/app/components/chat/Artifact.tsx +9 -4
- packages/bolt/app/components/chat/BaseChat.tsx +92 -75
- packages/bolt/app/components/chat/Chat.client.tsx +14 -28
- packages/bolt/app/components/chat/CodeBlock.tsx +1 -1
- packages/bolt/app/components/chat/Markdown.module.scss +9 -1
- packages/bolt/app/components/chat/Markdown.tsx +40 -36
- packages/bolt/app/components/chat/{Messages.tsx → Messages.client.tsx} +32 -25
- packages/bolt/app/components/ui/IconButton.tsx +4 -2
- packages/bolt/app/components/workspace/Workspace.client.tsx +55 -0
- packages/bolt/app/components/workspace/WorkspacePanel.tsx +0 -3
- packages/bolt/app/routes/api.enhancer.ts +1 -1
- packages/bolt/app/styles/index.scss +1 -1
- packages/bolt/app/styles/variables.scss +2 -0
- packages/bolt/app/utils/easings.ts +3 -0
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
|
10 |
const artifacts = useStore(workspaceStore.artifacts);
|
11 |
|
12 |
const artifact = artifacts[messageId];
|
13 |
|
14 |
return (
|
15 |
-
<button
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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
|
45 |
-
<div className="flex
|
46 |
-
<div id="chat" className="w-full">
|
47 |
{!chatStarted && (
|
48 |
-
<div id="intro" className="mt-[20vh] mb-14 max-w-
|
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(
|
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 |
-
'
|
70 |
-
|
71 |
-
'max-md:rounded-none max-md:border-x-none': chatStarted,
|
72 |
-
},
|
73 |
-
)}
|
74 |
>
|
75 |
-
<
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
83 |
-
|
84 |
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
130 |
</div>
|
131 |
-
|
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 {
|
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
|
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 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
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:
|
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 |
-
|
20 |
-
|
21 |
-
className
|
22 |
-
|
23 |
-
|
24 |
-
if (className?.includes('__boltArtifact__')) {
|
25 |
-
const messageId = node?.properties.dataMessageId as string;
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
}
|
30 |
-
|
31 |
-
return <Artifact messageId={messageId} />;
|
32 |
}
|
33 |
|
34 |
-
return
|
35 |
-
|
36 |
-
{children}
|
37 |
-
</div>
|
38 |
-
);
|
39 |
-
},
|
40 |
-
pre: (props) => {
|
41 |
-
const { children, node, ...rest } = props;
|
42 |
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
-
|
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 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
|
57 |
-
return <
|
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 |
-
|
10 |
-
|
11 |
messages?: Message[];
|
12 |
}
|
13 |
|
14 |
export function Messages(props: MessagesProps) {
|
15 |
-
const { id,
|
16 |
-
|
17 |
-
const containerRef = useRef<HTMLDivElement>(null);
|
18 |
|
19 |
return (
|
20 |
-
<div id={id}
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
27 |
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
<div
|
30 |
-
|
31 |
-
|
32 |
-
'
|
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':
|
40 |
-
'bg-accent text-xl':
|
41 |
},
|
42 |
)}
|
43 |
>
|
44 |
-
<div className={
|
45 |
</div>
|
46 |
{isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
47 |
</div>
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
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
|
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:
|
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);
|