fix(ui): mobile friendly

This commit is contained in:
Qwikode 2024-11-21 15:12:33 +02:00
parent 95776af641
commit f644066189
7 changed files with 124 additions and 87 deletions

View File

@ -10,11 +10,7 @@ interface APIKeyManagerProps {
labelForGetApiKey?: string; labelForGetApiKey?: string;
} }
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
provider,
apiKey,
setApiKey,
}) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [tempKey, setTempKey] = useState(apiKey); const [tempKey, setTempKey] = useState(apiKey);
@ -24,15 +20,29 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
}; };
return ( return (
<div className="flex items-center gap-2 mt-2 mb-2"> <div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span> <div>
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
{!isEditing && (
<div className="flex items-center mb-4">
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
</span>
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
<div className="i-ph:pencil-simple" />
</IconButton>
</div>
)}
</div>
{isEditing ? ( {isEditing ? (
<> <div className="flex items-center gap-3 mt-2">
<input <input
type="password" type="password"
value={tempKey} value={tempKey}
placeholder="Your API Key"
onChange={(e) => setTempKey(e.target.value)} onChange={(e) => setTempKey(e.target.value)}
className="flex-1 p-1 text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus" className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
/> />
<IconButton onClick={handleSave} title="Save API Key"> <IconButton onClick={handleSave} title="Save API Key">
<div className="i-ph:check" /> <div className="i-ph:check" />
@ -40,20 +50,15 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
<IconButton onClick={() => setIsEditing(false)} title="Cancel"> <IconButton onClick={() => setIsEditing(false)} title="Cancel">
<div className="i-ph:x" /> <div className="i-ph:x" />
</IconButton> </IconButton>
</> </div>
) : ( ) : (
<> <>
<span className="flex-1 text-sm text-bolt-elements-textPrimary"> {provider?.getApiKeyLink && (
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'} <IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
</span> <span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key"> <div className={provider?.icon || 'i-ph:key'} />
<div className="i-ph:pencil-simple" /> </IconButton>
</IconButton> )}
{provider?.getApiKeyLink && <IconButton onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
<span className="mr-2">{provider?.labelForGetApiKey || 'Get API Key'}</span>
<div className={provider?.icon || "i-ph:key"} />
</IconButton>}
</> </>
)} )}
</div> </div>

View File

@ -27,9 +27,9 @@ const EXAMPLE_PROMPTS = [
const providerList = PROVIDER_LIST; const providerList = PROVIDER_LIST;
const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => { const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
return ( return (
<div className="mb-2 flex gap-2"> <div className="mb-2 flex gap-2 flex-col sm:flex-row">
<select <select
value={provider?.name} value={provider?.name}
onChange={(e) => { onChange={(e) => {
@ -49,8 +49,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
key={provider?.name} key={provider?.name}
value={model} value={model}
onChange={(e) => setModel(e.target.value)} onChange={(e) => setModel(e.target.value)}
style={{ maxWidth: '70%' }} className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%] "
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
> >
{[...modelList] {[...modelList]
.filter((e) => e.provider == provider?.name && e.name) .filter((e) => e.provider == provider?.name && e.name)
@ -157,25 +156,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
ref={ref} ref={ref}
className={classNames( className={classNames(
styles.BaseChat, styles.BaseChat,
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1', 'relative flex flex-col lg:flex-row h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
)} )}
data-chat-visible={showChat} data-chat-visible={showChat}
> >
<ClientOnly>{() => <Menu />}</ClientOnly> <ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex overflow-y-auto w-full h-full"> <div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
<div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}> <div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
{!chatStarted && ( {!chatStarted && (
<div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center"> <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
<h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in"> <h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
Where ideas begin Where ideas begin
</h1> </h1>
<p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200"> <p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
Bring ideas to life in seconds or get help on existing projects. Bring ideas to life in seconds or get help on existing projects.
</p> </p>
</div> </div>
)} )}
<div <div
className={classNames('pt-6 px-6', { className={classNames('pt-6 px-2 sm:px-6', {
'h-full flex flex-col': chatStarted, 'h-full flex flex-col': chatStarted,
})} })}
> >
@ -184,7 +183,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-chat px-4 pb-6 mx-auto z-1" className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
messages={messages} messages={messages}
isStreaming={isStreaming} isStreaming={isStreaming}
/> />
@ -193,10 +192,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</ClientOnly> </ClientOnly>
<div <div
className={classNames( className={classNames(
'bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt', ' bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
{ {
'sticky bottom-0': chatStarted 'sticky bottom-2': chatStarted,
})} },
)}
> >
<ModelSelector <ModelSelector
key={provider?.name + ':' + modelList.length} key={provider?.name + ':' + modelList.length}
@ -206,7 +206,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
provider={provider} provider={provider}
setProvider={setProvider} setProvider={setProvider}
providerList={PROVIDER_LIST} providerList={PROVIDER_LIST}
apiKeys={apiKeys}
/> />
{provider && ( {provider && (
<APIKeyManager <APIKeyManager
provider={provider} provider={provider}
@ -214,6 +216,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setApiKey={(key) => updateApiKey(provider.name, key)} setApiKey={(key) => updateApiKey(provider.name, key)}
/> />
)} )}
<div <div
className={classNames( className={classNames(
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all', 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
@ -221,7 +224,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
> >
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`} className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-0 focus:border-none focus:shadow-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
if (event.shiftKey) { if (event.shiftKey) {
@ -294,7 +297,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
) : null} ) : null}
</div> </div>
</div> </div>
<div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
</div> </div>
</div> </div>
{!chatStarted && ( {!chatStarted && (

View File

@ -4,7 +4,7 @@ import { classNames } from '~/utils/classNames';
import { AssistantMessage } from './AssistantMessage'; import { AssistantMessage } from './AssistantMessage';
import { UserMessage } from './UserMessage'; import { UserMessage } from './UserMessage';
import * as Tooltip from '@radix-ui/react-tooltip'; import * as Tooltip from '@radix-ui/react-tooltip';
import { useLocation, useNavigate } from '@remix-run/react'; import { useLocation } from '@remix-run/react';
import { db, chatId } from '~/lib/persistence/useChatHistory'; import { db, chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db'; import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -19,7 +19,6 @@ interface MessagesProps {
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => { export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
const { id, isStreaming = false, messages = [] } = props; const { id, isStreaming = false, messages = [] } = props;
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const handleRewind = (messageId: string) => { const handleRewind = (messageId: string) => {
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
@ -69,53 +68,57 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
<div className="grid grid-col-1 w-full"> <div className="grid grid-col-1 w-full">
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />} {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
</div> </div>
{!isUserMessage && (<div className="flex gap-2"> {!isUserMessage && (
<Tooltip.Root> <div className="flex gap-2 flex-col lg:flex-row">
<Tooltip.Trigger asChild> <Tooltip.Root>
{messageId && (<button <Tooltip.Trigger asChild>
onClick={() => handleRewind(messageId)} {messageId && (
key='i-ph:arrow-u-up-left' <button
className={classNames( onClick={() => handleRewind(messageId)}
'i-ph:arrow-u-up-left', key="i-ph:arrow-u-up-left"
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors' className={classNames(
'i-ph:arrow-u-up-left',
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
)}
/>
)} )}
/>)} </Tooltip.Trigger>
</Tooltip.Trigger> <Tooltip.Portal>
<Tooltip.Portal> <Tooltip.Content
<Tooltip.Content className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg" sideOffset={5}
sideOffset={5} style={{ zIndex: 1000 }}
style={{zIndex: 1000}} >
> Revert to this message
Revert to this message <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" /> </Tooltip.Content>
</Tooltip.Content> </Tooltip.Portal>
</Tooltip.Portal> </Tooltip.Root>
</Tooltip.Root>
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button
onClick={() => handleFork(messageId)} onClick={() => handleFork(messageId)}
key='i-ph:git-fork' key="i-ph:git-fork"
className={classNames( className={classNames(
'i-ph:git-fork', 'i-ph:git-fork',
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors' 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
)} )}
/> />
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content <Tooltip.Content
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg" className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
sideOffset={5} sideOffset={5}
style={{zIndex: 1000}} style={{ zIndex: 1000 }}
> >
Fork chat from this message Fork chat from this message
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" /> <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>
</Tooltip.Root> </Tooltip.Root>
</div>)} </div>
)}
</div> </div>
); );
}) })

View File

@ -1,4 +1,5 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import useViewport from '~/lib/hooks';
import { chatStore } from '~/lib/stores/chat'; import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench'; import { workbenchStore } from '~/lib/stores/workbench';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
@ -9,6 +10,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
const showWorkbench = useStore(workbenchStore.showWorkbench); const showWorkbench = useStore(workbenchStore.showWorkbench);
const { showChat } = useStore(chatStore); const { showChat } = useStore(chatStore);
const isSmallViewport = useViewport(1024);
const canHideChat = showWorkbench || !showChat; const canHideChat = showWorkbench || !showChat;
return ( return (
@ -16,7 +19,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden"> <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
<Button <Button
active={showChat} active={showChat}
disabled={!canHideChat} disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's needed
onClick={() => { onClick={() => {
if (canHideChat) { if (canHideChat) {
chatStore.setKey('showChat', !showChat); chatStore.setKey('showChat', !showChat);

View File

@ -16,6 +16,7 @@ import { cubicEasingFn } from '~/utils/easings';
import { renderLogger } from '~/utils/logger'; import { renderLogger } from '~/utils/logger';
import { EditorPanel } from './EditorPanel'; import { EditorPanel } from './EditorPanel';
import { Preview } from './Preview'; import { Preview } from './Preview';
import useViewport from '~/lib/hooks';
interface WorkspaceProps { interface WorkspaceProps {
chatStarted?: boolean; chatStarted?: boolean;
@ -65,6 +66,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
const files = useStore(workbenchStore.files); const files = useStore(workbenchStore.files);
const selectedView = useStore(workbenchStore.currentView); const selectedView = useStore(workbenchStore.currentView);
const isSmallViewport = useViewport(1024);
const setSelectedView = (view: WorkbenchViewType) => { const setSelectedView = (view: WorkbenchViewType) => {
workbenchStore.currentView.set(view); workbenchStore.currentView.set(view);
}; };
@ -128,12 +131,14 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
className={classNames( className={classNames(
'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', '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',
{ {
'w-full': isSmallViewport,
'left-0': showWorkbench && isSmallViewport,
'left-[var(--workbench-left)]': showWorkbench, 'left-[var(--workbench-left)]': showWorkbench,
'left-[100%]': !showWorkbench, 'left-[100%]': !showWorkbench,
}, },
)} )}
> >
<div className="absolute inset-0 px-6"> <div className="absolute inset-0 px-2 lg:px-6">
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden"> <div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor"> <div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} /> <Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />

View File

@ -2,3 +2,4 @@ export * from './useMessageParser';
export * from './usePromptEnhancer'; export * from './usePromptEnhancer';
export * from './useShortcuts'; export * from './useShortcuts';
export * from './useSnapScroll'; export * from './useSnapScroll';
export { default } from './useViewport';

View File

@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
const useViewport = (threshold = 1024) => {
const [isSmallViewport, setIsSmallViewport] = useState(window.innerWidth < threshold);
useEffect(() => {
const handleResize = () => setIsSmallViewport(window.innerWidth < threshold);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [threshold]);
return isSmallViewport;
};
export default useViewport;