Dominic Elm commited on
Commit
5bbcdcc
·
unverified ·
1 Parent(s): 41f3f20

feat(ui): style sidebar and landing page (#27)

Browse files
packages/bolt/app/components/chat/BaseChat.tsx CHANGED
@@ -1,15 +1,15 @@
1
  import type { Message } from 'ai';
2
- import React, { type LegacyRef, type RefCallback } from 'react';
3
  import { ClientOnly } from 'remix-utils/client-only';
 
4
  import { IconButton } from '~/components/ui/IconButton';
5
  import { Workbench } from '~/components/workbench/Workbench.client';
6
  import { classNames } from '~/utils/classNames';
7
  import { Messages } from './Messages.client';
8
- import { Menu } from '~/components/sidemenu/Menu.client';
9
  import { SendButton } from './SendButton.client';
10
 
11
  interface BaseChatProps {
12
- textareaRef?: LegacyRef<HTMLTextAreaElement> | undefined;
13
  messageRef?: RefCallback<HTMLDivElement> | undefined;
14
  scrollRef?: RefCallback<HTMLDivElement> | undefined;
15
  chatStarted?: boolean;
@@ -19,12 +19,18 @@ interface BaseChatProps {
19
  promptEnhanced?: boolean;
20
  input?: string;
21
  handleStop?: () => void;
22
- sendMessage?: (event: React.UIEvent) => void;
23
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
24
  enhancePrompt?: () => void;
25
  }
26
 
27
- const EXAMPLES = [{ text: 'Example' }, { text: 'Example' }, { text: 'Example' }, { text: 'Example' }];
 
 
 
 
 
 
28
 
29
  const TEXTAREA_MIN_HEIGHT = 72;
30
 
@@ -53,22 +59,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
53
  <div ref={ref} className="relative flex h-full w-full overflow-hidden ">
54
  <ClientOnly>{() => <Menu />}</ClientOnly>
55
  <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
56
- <div id="chat" className="flex flex-col w-full h-full px-6">
57
  {!chatStarted && (
58
- <div id="intro" className="mt-[20vh] mb-14 max-w-3xl mx-auto">
59
- <h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
60
- <p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
61
- <div className="grid max-md:grid-cols-[repeat(1,1fr)] md:grid-cols-[repeat(2,minmax(300px,1fr))] gap-4">
62
- {EXAMPLES.map((suggestion, index) => (
63
- <button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
64
- {suggestion.text}
65
- </button>
66
- ))}
67
- </div>
68
  </div>
69
  )}
70
  <div
71
- className={classNames('pt-10', {
72
  'h-full flex flex-col': chatStarted,
73
  })}
74
  >
@@ -77,7 +76,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
77
  return chatStarted ? (
78
  <Messages
79
  ref={messageRef}
80
- className="flex flex-col w-full flex-1 max-w-3xl px-4 pb-10 mx-auto z-1"
81
  messages={messages}
82
  isStreaming={isStreaming}
83
  />
@@ -85,7 +84,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
85
  }}
86
  </ClientOnly>
87
  <div
88
- className={classNames('relative w-full max-w-3xl md:mx-auto z-2', {
89
  'sticky bottom-0': chatStarted,
90
  })}
91
  >
@@ -172,6 +171,26 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
172
  <div className="bg-white pb-6">{/* Ghost Element */}</div>
173
  </div>
174
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
  <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
177
  </div>
 
1
  import type { Message } from 'ai';
2
+ import React, { type RefCallback } from 'react';
3
  import { ClientOnly } from 'remix-utils/client-only';
4
+ import { Menu } from '~/components/sidebar/Menu.client';
5
  import { IconButton } from '~/components/ui/IconButton';
6
  import { Workbench } from '~/components/workbench/Workbench.client';
7
  import { classNames } from '~/utils/classNames';
8
  import { Messages } from './Messages.client';
 
9
  import { SendButton } from './SendButton.client';
10
 
11
  interface BaseChatProps {
12
+ textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
13
  messageRef?: RefCallback<HTMLDivElement> | undefined;
14
  scrollRef?: RefCallback<HTMLDivElement> | undefined;
15
  chatStarted?: boolean;
 
19
  promptEnhanced?: boolean;
20
  input?: string;
21
  handleStop?: () => void;
22
+ sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
23
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
24
  enhancePrompt?: () => void;
25
  }
26
 
27
+ const EXAMPLE_PROMPTS = [
28
+ { text: 'Build a todo app in React using Tailwind' },
29
+ { text: 'Build a simple blog using Astro' },
30
+ { text: 'Create a cookie consent form using Material UI' },
31
+ { text: 'Make a space invaders game' },
32
+ { text: 'How do I can center a div?' },
33
+ ];
34
 
35
  const TEXTAREA_MIN_HEIGHT = 72;
36
 
 
59
  <div ref={ref} className="relative flex h-full w-full overflow-hidden ">
60
  <ClientOnly>{() => <Menu />}</ClientOnly>
61
  <div ref={scrollRef} className="flex overflow-scroll w-full h-full">
62
+ <div className="flex flex-col w-full h-full px-6">
63
  {!chatStarted && (
64
+ <div id="intro" className="mt-[26vh] max-w-2xl mx-auto">
65
+ <h1 className="text-5xl text-center font-bold text-slate-800 mb-2">Where ideas begin</h1>
66
+ <p className="mb-4 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
 
 
 
 
 
 
 
67
  </div>
68
  )}
69
  <div
70
+ className={classNames('pt-6', {
71
  'h-full flex flex-col': chatStarted,
72
  })}
73
  >
 
76
  return chatStarted ? (
77
  <Messages
78
  ref={messageRef}
79
+ className="flex flex-col w-full flex-1 max-w-2xl px-4 pb-10 mx-auto z-1"
80
  messages={messages}
81
  isStreaming={isStreaming}
82
  />
 
84
  }}
85
  </ClientOnly>
86
  <div
87
+ className={classNames('relative w-full max-w-2xl md:mx-auto z-2', {
88
  'sticky bottom-0': chatStarted,
89
  })}
90
  >
 
171
  <div className="bg-white pb-6">{/* Ghost Element */}</div>
172
  </div>
173
  </div>
174
+ {!chatStarted && (
175
+ <div id="examples" className="relative w-full max-w-2xl mx-auto text-center mt-8 flex justify-center">
176
+ <div className="flex flex-col items-center space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
177
+ {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
178
+ return (
179
+ <button
180
+ key={index}
181
+ onClick={(event) => {
182
+ sendMessage?.(event, examplePrompt.text);
183
+ }}
184
+ className="group flex items-center gap-2 bg-transparent text-gray-500 hover:text-gray-1000"
185
+ >
186
+ {examplePrompt.text}
187
+ <div className="i-ph:arrow-bend-down-left" />
188
+ </button>
189
+ );
190
+ })}
191
+ </div>
192
+ </div>
193
+ )}
194
  </div>
195
  <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
196
  </div>
packages/bolt/app/components/chat/Chat.client.tsx CHANGED
@@ -44,7 +44,7 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
44
 
45
  const [animationScope, animate] = useAnimate();
46
 
47
- const { messages, isLoading, input, handleInputChange, setInput, handleSubmit, stop, append } = useChat({
48
  api: '/api/chat',
49
  onError: (error) => {
50
  logger.error('Request failed\n\n', error);
@@ -61,6 +61,10 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
61
 
62
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
63
 
 
 
 
 
64
  useEffect(() => {
65
  parseMessages(messages, isLoading);
66
 
@@ -101,13 +105,20 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
101
  return;
102
  }
103
 
104
- await animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn });
 
 
 
 
 
105
 
106
  setChatStarted(true);
107
  };
108
 
109
- const sendMessage = async (event: React.UIEvent) => {
110
- if (input.length === 0 || isLoading) {
 
 
111
  return;
112
  }
113
 
@@ -136,9 +147,7 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
136
  * manually reset the input and we'd have to manually pass in file attachments. However, those
137
  * aren't relevant here.
138
  */
139
- append({ role: 'user', content: `${diff}\n\n${input}` });
140
-
141
- setInput('');
142
 
143
  /**
144
  * After sending a new message we reset all modifications since the model
@@ -146,9 +155,11 @@ export function ChatImpl({ initialMessages, storeMessageHistory }: ChatProps) {
146
  */
147
  workbenchStore.resetAllFileModifications();
148
  } else {
149
- handleSubmit(event);
150
  }
151
 
 
 
152
  resetEnhancer();
153
 
154
  textareaRef.current?.blur();
 
44
 
45
  const [animationScope, animate] = useAnimate();
46
 
47
+ const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
48
  api: '/api/chat',
49
  onError: (error) => {
50
  logger.error('Request failed\n\n', error);
 
61
 
62
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
63
 
64
+ useEffect(() => {
65
+ chatStore.setKey('started', initialMessages.length > 0);
66
+ }, []);
67
+
68
  useEffect(() => {
69
  parseMessages(messages, isLoading);
70
 
 
105
  return;
106
  }
107
 
108
+ await Promise.all([
109
+ animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
110
+ animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
111
+ ]);
112
+
113
+ chatStore.setKey('started', true);
114
 
115
  setChatStarted(true);
116
  };
117
 
118
+ const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
119
+ const _input = messageInput || input;
120
+
121
+ if (_input.length === 0 || isLoading) {
122
  return;
123
  }
124
 
 
147
  * manually reset the input and we'd have to manually pass in file attachments. However, those
148
  * aren't relevant here.
149
  */
150
+ append({ role: 'user', content: `${diff}\n\n${_input}` });
 
 
151
 
152
  /**
153
  * After sending a new message we reset all modifications since the model
 
155
  */
156
  workbenchStore.resetAllFileModifications();
157
  } else {
158
+ append({ role: 'user', content: _input });
159
  }
160
 
161
+ setInput('');
162
+
163
  resetEnhancer();
164
 
165
  textareaRef.current?.blur();
packages/bolt/app/components/header/Header.tsx CHANGED
@@ -1,20 +1,26 @@
 
1
  import { ClientOnly } from 'remix-utils/client-only';
2
- import { IconButton } from '~/components/ui/IconButton';
 
3
  import { OpenStackBlitz } from './OpenStackBlitz.client';
4
 
5
  export function Header() {
 
 
6
  return (
7
- <header className="flex items-center bg-white p-4 border-b border-gray-200 h-[var(--header-height)]">
 
 
 
 
 
8
  <div className="flex items-center gap-2">
9
  <a href="/" className="text-2xl font-semibold text-accent">
10
- Bolt
11
  </a>
12
  </div>
13
  <div className="ml-auto flex gap-2">
14
  <ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
15
- <a href="/logout">
16
- <IconButton icon="i-ph:sign-out" />
17
- </a>
18
  </div>
19
  </header>
20
  );
 
1
+ import { useStore } from '@nanostores/react';
2
  import { ClientOnly } from 'remix-utils/client-only';
3
+ import { chatStore } from '~/lib/stores/chat';
4
+ import { classNames } from '~/utils/classNames';
5
  import { OpenStackBlitz } from './OpenStackBlitz.client';
6
 
7
  export function Header() {
8
+ const chat = useStore(chatStore);
9
+
10
  return (
11
+ <header
12
+ className={classNames('flex items-center bg-white p-5 border-b h-[var(--header-height)]', {
13
+ 'border-transparent': !chat.started,
14
+ 'border-gray-200': chat.started,
15
+ })}
16
+ >
17
  <div className="flex items-center gap-2">
18
  <a href="/" className="text-2xl font-semibold text-accent">
19
+ <img src="/logo_text.svg" width="60px" alt="Bolt Logo" />
20
  </a>
21
  </div>
22
  <div className="ml-auto flex gap-2">
23
  <ClientOnly>{() => <OpenStackBlitz />}</ClientOnly>
 
 
 
24
  </div>
25
  </header>
26
  );
packages/bolt/app/components/{sidemenu → sidebar}/HistoryItem.tsx RENAMED
@@ -1,39 +1,23 @@
 
1
  import { toast } from 'react-toastify';
2
- import { useCallback, useEffect, useState, useRef } from 'react';
3
- import { motion, type Variants } from 'framer-motion';
4
- import { cubicEasingFn } from '~/utils/easings';
5
- import { IconButton } from '~/components/ui/IconButton';
6
  import { db, deleteId, type ChatHistory } from '~/lib/persistence';
7
-
8
- const iconVariants = {
9
- closed: {
10
- transform: 'translate(40px,0)',
11
- opacity: 0,
12
- transition: {
13
- duration: 0.2,
14
- ease: cubicEasingFn,
15
- },
16
- },
17
- open: {
18
- transform: 'translate(0,0)',
19
- opacity: 1,
20
- transition: {
21
- duration: 0.2,
22
- ease: cubicEasingFn,
23
- },
24
- },
25
- } satisfies Variants;
26
 
27
  export function HistoryItem({ item, loadEntries }: { item: ChatHistory; loadEntries: () => void }) {
28
  const [requestingDelete, setRequestingDelete] = useState(false);
29
  const [hovering, setHovering] = useState(false);
30
  const hoverRef = useRef<HTMLDivElement>(null);
31
 
32
- const deleteItem = useCallback(() => {
 
 
33
  if (db) {
34
  deleteId(db, item.id)
35
  .then(() => loadEntries())
36
- .catch((error) => toast.error(error.message));
 
 
 
37
  }
38
  }, []);
39
 
@@ -67,22 +51,30 @@ export function HistoryItem({ item, loadEntries }: { item: ChatHistory; loadEntr
67
  }, []);
68
 
69
  return (
70
- <div className="ml-3 mr-1">
71
- <div
72
- ref={hoverRef}
73
- className="rounded-md hover:bg-gray-200 p-1 overflow-hidden flex justify-between items-center"
74
- >
75
- <a href={`/chat/${item.urlId}`} className="truncate block pr-1">
76
- {item.description}
77
- </a>
78
- <motion.div initial="closed" animate={hovering ? 'open' : 'closed'} variants={iconVariants}>
79
- {requestingDelete ? (
80
- <IconButton icon="i-ph:check" onClick={deleteItem} />
81
- ) : (
82
- <IconButton icon="i-ph:trash" onClick={() => setRequestingDelete(true)} />
 
 
 
 
 
 
 
 
83
  )}
84
- </motion.div>
85
- </div>
86
  </div>
87
  );
88
  }
 
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
  import { toast } from 'react-toastify';
 
 
 
 
3
  import { db, deleteId, type ChatHistory } from '~/lib/persistence';
4
+ import { logger } from '~/utils/logger';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  export function HistoryItem({ item, loadEntries }: { item: ChatHistory; loadEntries: () => void }) {
7
  const [requestingDelete, setRequestingDelete] = useState(false);
8
  const [hovering, setHovering] = useState(false);
9
  const hoverRef = useRef<HTMLDivElement>(null);
10
 
11
+ const deleteItem = useCallback((event: React.UIEvent) => {
12
+ event.preventDefault();
13
+
14
  if (db) {
15
  deleteId(db, item.id)
16
  .then(() => loadEntries())
17
+ .catch((error) => {
18
+ toast.error('Failed to delete conversation');
19
+ logger.error(error);
20
+ });
21
  }
22
  }, []);
23
 
 
51
  }, []);
52
 
53
  return (
54
+ <div
55
+ ref={hoverRef}
56
+ className="group rounded-md hover:bg-gray-100 overflow-hidden flex justify-between items-center px-2 py-1"
57
+ >
58
+ <a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
59
+ {item.description}
60
+ <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-white group-hover:from-gray-100 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
61
+ {hovering && (
62
+ <div className="flex items-center p-1">
63
+ {requestingDelete ? (
64
+ <button className="i-ph:check text-gray-600 hover:text-gray-1000 scale-110" onClick={deleteItem} />
65
+ ) : (
66
+ <button
67
+ className="i-ph:trash text-gray-600 hover:text-gray-1000 scale-110"
68
+ onClick={(event) => {
69
+ event.preventDefault();
70
+ setRequestingDelete(true);
71
+ }}
72
+ />
73
+ )}
74
+ </div>
75
  )}
76
+ </div>
77
+ </a>
78
  </div>
79
  );
80
  }
packages/bolt/app/components/{sidemenu → sidebar}/Menu.client.tsx RENAMED
@@ -1,20 +1,23 @@
1
- import { Fragment, useEffect, useState, useRef, useCallback } from 'react';
2
- import { toast } from 'react-toastify';
3
  import { motion, type Variants } from 'framer-motion';
4
- import { cubicEasingFn } from '~/utils/easings';
 
 
5
  import { db, getAll, type ChatHistory } from '~/lib/persistence';
 
6
  import { HistoryItem } from './HistoryItem';
7
  import { binDates } from './date-binning';
8
 
9
  const menuVariants = {
10
  closed: {
11
- left: '-400px',
 
12
  transition: {
13
  duration: 0.2,
14
  ease: cubicEasingFn,
15
  },
16
  },
17
  open: {
 
18
  left: 0,
19
  transition: {
20
  duration: 0.2,
@@ -69,31 +72,43 @@ export function Menu() {
69
  initial="closed"
70
  animate={open ? 'open' : 'closed'}
71
  variants={menuVariants}
72
- className="flex flex-col side-menu fixed left-[-400px] top-0 w-[400px] h-full bg-white border-r border-gray-200 z-max"
73
  >
74
- <div className="flex items-center border-b border-gray-200 bg-white p-4 h-[var(--header-height)]">
75
- <a href="/" className="text-2xl font-semibold">
76
- Bolt
77
- </a>
78
- </div>
79
- <div className="m-4 ml-2 mt-7">
80
- <a href="/" className="text-white rounded-md bg-accent-600 p-2 font-semibold">
81
- Start new chat
82
- </a>
83
  </div>
84
- <div className="font-semibold m-2 ml-4">Your Chats</div>
85
- <div className="overflow-auto pb-2">
86
- {list.length === 0 && <div className="ml-4 text-gray">No previous conversations</div>}
87
- {binDates(list).map(({ category, items }) => (
88
- <Fragment key={category}>
89
- <div className="ml-4 text-gray m-2">{category}</div>
90
- {items.map((item) => (
91
- <HistoryItem key={item.id} item={item} loadEntries={loadEntries} />
92
- ))}
93
- </Fragment>
94
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  </div>
96
- <div className="border-t border-gray-200 h-[var(--header-height)]" />
97
  </motion.div>
98
  );
99
  }
 
 
 
1
  import { motion, type Variants } from 'framer-motion';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { toast } from 'react-toastify';
4
+ import { IconButton } from '~/components/ui/IconButton';
5
  import { db, getAll, type ChatHistory } from '~/lib/persistence';
6
+ import { cubicEasingFn } from '~/utils/easings';
7
  import { HistoryItem } from './HistoryItem';
8
  import { binDates } from './date-binning';
9
 
10
  const menuVariants = {
11
  closed: {
12
+ opacity: 0,
13
+ left: '-150px',
14
  transition: {
15
  duration: 0.2,
16
  ease: cubicEasingFn,
17
  },
18
  },
19
  open: {
20
+ opacity: 1,
21
  left: 0,
22
  transition: {
23
  duration: 0.2,
 
72
  initial="closed"
73
  animate={open ? 'open' : 'closed'}
74
  variants={menuVariants}
75
+ className="flex flex-col side-menu fixed top-0 w-[350px] h-full bg-white border-r rounded-r-3xl border-gray-200 z-max shadow-xl text-sm"
76
  >
77
+ <div className="flex items-center p-4 h-[var(--header-height)]">
78
+ <img src="/logo_text.svg" width="60px" alt="Bolt Logo" />
 
 
 
 
 
 
 
79
  </div>
80
+ <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
81
+ <div className="p-4">
82
+ <a
83
+ href="/"
84
+ className="flex gap-2 items-center text-accent-600 rounded-md bg-accent-600/12 hover:bg-accent-600/15 p-2 font-medium"
85
+ >
86
+ <span className="inline-block i-blitz:chat scale-110" />
87
+ Start new chat
88
+ </a>
89
+ </div>
90
+ <div className="font-semibold pl-6 pr-5 my-2">Your Chats</div>
91
+ <div className="flex-1 overflow-scroll pl-4 pr-5 pb-5">
92
+ {list.length === 0 && <div className="pl-2 text-gray">No previous conversations</div>}
93
+ {binDates(list).map(({ category, items }) => (
94
+ <div key={category} className="mt-4 first:mt-0 space-y-1">
95
+ <div className="text-gray sticky top-0 z-20 bg-white pl-2 pt-2 pb-1">{category}</div>
96
+ {items.map((item) => (
97
+ <HistoryItem key={item.id} item={item} loadEntries={loadEntries} />
98
+ ))}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ <div className="flex items-center border-t p-4">
103
+ <a href="/logout">
104
+ <IconButton className="p-1.5 gap-1.5">
105
+ <>
106
+ Logout <span className="i-ph:sign-out text-lg" />
107
+ </>
108
+ </IconButton>
109
+ </a>
110
+ </div>
111
  </div>
 
112
  </motion.div>
113
  );
114
  }
packages/bolt/app/components/{sidemenu → sidebar}/date-binning.ts RENAMED
@@ -1,5 +1,5 @@
 
1
  import type { ChatHistory } from '~/lib/persistence';
2
- import { format, isToday, isYesterday, isThisWeek, isThisYear, subDays, isAfter } from 'date-fns';
3
 
4
  type Bin = { category: string; items: ChatHistory[] };
5
 
@@ -19,6 +19,7 @@ export function binDates(_list: ChatHistory[]) {
19
  };
20
 
21
  binLookup[category] = bin;
 
22
  bins.push(bin);
23
  } else {
24
  binLookup[category].items.push(item);
@@ -38,7 +39,8 @@ function dateCategory(date: Date) {
38
  }
39
 
40
  if (isThisWeek(date)) {
41
- return format(date, 'eeee'); // e.g. "Monday"
 
42
  }
43
 
44
  const thirtyDaysAgo = subDays(new Date(), 30);
@@ -48,8 +50,10 @@ function dateCategory(date: Date) {
48
  }
49
 
50
  if (isThisYear(date)) {
51
- return format(date, 'MMMM'); // e.g., "July"
 
52
  }
53
 
54
- return format(date, 'MMMM yyyy'); // e.g. "July 2023"
 
55
  }
 
1
+ import { format, isAfter, isThisWeek, isThisYear, isToday, isYesterday, subDays } from 'date-fns';
2
  import type { ChatHistory } from '~/lib/persistence';
 
3
 
4
  type Bin = { category: string; items: ChatHistory[] };
5
 
 
19
  };
20
 
21
  binLookup[category] = bin;
22
+
23
  bins.push(bin);
24
  } else {
25
  binLookup[category].items.push(item);
 
39
  }
40
 
41
  if (isThisWeek(date)) {
42
+ // e.g., "Monday"
43
+ return format(date, 'eeee');
44
  }
45
 
46
  const thirtyDaysAgo = subDays(new Date(), 30);
 
50
  }
51
 
52
  if (isThisYear(date)) {
53
+ // e.g., "July"
54
+ return format(date, 'MMMM');
55
  }
56
 
57
+ // e.g., "July 2023"
58
+ return format(date, 'MMMM yyyy');
59
  }
packages/bolt/app/lib/stores/chat.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { map } from 'nanostores';
2
 
3
  export const chatStore = map({
 
4
  aborted: false,
5
  });
 
1
  import { map } from 'nanostores';
2
 
3
  export const chatStore = map({
4
+ started: false,
5
  aborted: false,
6
  });
packages/bolt/app/styles/index.scss CHANGED
@@ -4,30 +4,6 @@
4
  @import './components/terminal.scss';
5
  @import './components/resize-handle.scss';
6
 
7
- body {
8
- --at-apply: bg-bolt-elements-app-backgroundColor;
9
-
10
- font-family: 'Inter', sans-serif;
11
-
12
- &:before {
13
- --line: color-mix(in lch, canvasText, transparent 93%);
14
- --size: 50px;
15
-
16
- content: '';
17
- height: 100vh;
18
- mask: linear-gradient(-25deg, transparent 60%, white);
19
- pointer-events: none;
20
- position: fixed;
21
- top: -8px;
22
- transform-style: flat;
23
- width: 100vw;
24
- z-index: -1;
25
- background:
26
- linear-gradient(90deg, var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size),
27
- linear-gradient(var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size);
28
- }
29
- }
30
-
31
  html,
32
  body {
33
  height: 100%;
 
4
  @import './components/terminal.scss';
5
  @import './components/resize-handle.scss';
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  html,
8
  body {
9
  height: 100%;
packages/bolt/icons/chat.svg ADDED
packages/bolt/public/logo.svg ADDED
packages/bolt/public/logo_text.svg ADDED