add more mobile ui (#18)
This commit is contained in:
parent
263c5c33ae
commit
7e6651dea7
|
@ -23,6 +23,7 @@ Expect frequent improvements.
|
|||
- [ ] Mobile view
|
||||
- [ ] Saving via data export
|
||||
- [ ] Folders
|
||||
- [ ] Change default prompt
|
||||
|
||||
**Recent updates:**
|
||||
|
||||
|
|
|
@ -27,22 +27,22 @@ export const Chat: FC<Props> = ({ model, messages, messageIsStreaming, loading,
|
|||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col dark:bg-[#343541]">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="flex-1 overflow-scroll dark:bg-[#343541]">
|
||||
<div>
|
||||
{messages.length === 0 ? (
|
||||
<>
|
||||
<div className="flex justify-center pt-8 overflow-auto">
|
||||
<div className="flex justify-center pt-8">
|
||||
<ModelSelect
|
||||
model={model}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-4xl text-center text-neutral-300 pt-[280px]">Chatbot UI Pro</div>
|
||||
<div className="text-4xl text-center text-neutral-600 dark:text-neutral-200 pt-[160px] sm:pt-[280px]">Chatbot UI</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center py-3 dark:bg-[#444654] dark:text-neutral-300 text-neutral-500 text-sm border border-b-neutral-300 dark:border-none">Model: {OpenAIModelNames[model]}</div>
|
||||
<div className="flex justify-center py-2 text-neutral-500 bg-neutral-100 dark:bg-[#444654] dark:text-neutral-200 text-sm border border-b-neutral-300 dark:border-none">Model: {OpenAIModelNames[model]}</div>
|
||||
|
||||
{messages.map((message, index) => (
|
||||
<ChatMessage
|
||||
|
@ -51,18 +51,21 @@ export const Chat: FC<Props> = ({ model, messages, messageIsStreaming, loading,
|
|||
lightMode={lightMode}
|
||||
/>
|
||||
))}
|
||||
|
||||
{loading && <ChatLoader />}
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
<div
|
||||
className="bg-white dark:bg-[#343541] h-24 sm:h-32"
|
||||
ref={messagesEndRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-[100px] w-[340px] sm:w-[400px] md:w-[500px] lg:w-[700px] xl:w-[800px] mx-auto">
|
||||
<ChatInput
|
||||
messageIsStreaming={messageIsStreaming}
|
||||
onSend={onSend}
|
||||
/>
|
||||
</div>
|
||||
<ChatInput
|
||||
messageIsStreaming={messageIsStreaming}
|
||||
onSend={onSend}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,14 +32,27 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming }) => {
|
|||
alert("Please enter a message");
|
||||
return;
|
||||
}
|
||||
|
||||
onSend({ role: "user", content });
|
||||
setContent("");
|
||||
|
||||
if (textareaRef && textareaRef.current) {
|
||||
textareaRef.current.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const isMobile = () => {
|
||||
const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent;
|
||||
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
|
||||
return mobileRegex.test(userAgent);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!isTyping && e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
if (!isTyping) {
|
||||
if (e.key === "Enter" && !e.shiftKey && !isMobile()) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -51,32 +64,31 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming }) => {
|
|||
}, [content]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute bottom-[-80px] w-full">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="rounded-lg pl-4 pr-8 py-3 w-full focus:outline-none max-h-[280px] dark:bg-[#40414F] dark:border-opacity-50 dark:border-neutral-800 dark:text-neutral-100 border border-neutral-300 shadow text-neutral-900"
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "400px",
|
||||
overflow: "auto"
|
||||
}}
|
||||
placeholder="Type a message..."
|
||||
value={content}
|
||||
rows={1}
|
||||
onCompositionStart={() => setIsTyping(true)}
|
||||
onCompositionEnd={() => setIsTyping(false)}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
className="absolute right-2 bottom-[14px] text-neutral-400 p-2 hover:dark:bg-neutral-800 hover:bg-neutral-400 hover:text-white rounded-md"
|
||||
onClick={handleSend}
|
||||
>
|
||||
<IconSend size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="fixed sm:absolute bottom-4 sm:bottom-8 w-full sm:w-1/2 px-2 left-0 sm:left-[280px] lg:left-[200px] right-0 ml-auto mr-auto">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="rounded-lg pl-4 pr-8 py-3 w-full focus:outline-none max-h-[280px] dark:bg-[#40414F] dark:border-opacity-50 dark:border-neutral-800 dark:text-neutral-100 border border-neutral-300 shadow text-neutral-900"
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "400px",
|
||||
overflow: "auto"
|
||||
}}
|
||||
placeholder="Type a message..."
|
||||
value={content}
|
||||
rows={1}
|
||||
onCompositionStart={() => setIsTyping(true)}
|
||||
onCompositionEnd={() => setIsTyping(false)}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="absolute right-5 bottom-[18px] focus:outline-none text-neutral-800 hover:text-neutral-900 dark:text-neutral-100 dark:hover:text-neutral-200 dark:bg-opacity-50 hover:bg-neutral-200 p-1 rounded-sm"
|
||||
onClick={handleSend}
|
||||
>
|
||||
<IconSend size={18} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { Conversation } from "@/types";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
|
||||
interface Props {
|
||||
selectedConversation: Conversation;
|
||||
onNewConversation: () => void;
|
||||
}
|
||||
|
||||
export const Navbar: FC<Props> = ({ selectedConversation, onNewConversation }) => {
|
||||
return (
|
||||
<div className="flex justify-between bg-[#202123] py-3 px-4 w-full">
|
||||
<div className="mr-4"></div>
|
||||
|
||||
<div className="max-w-[240px] whitespace-nowrap overflow-hidden text-ellipsis">{selectedConversation.name}</div>
|
||||
|
||||
<IconPlus
|
||||
className="cursor-pointer hover:text-neutral-400"
|
||||
onClick={onNewConversation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -21,10 +21,10 @@ interface Props {
|
|||
|
||||
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onRenameConversation, onApiKeyChange }) => {
|
||||
return (
|
||||
<div className="flex flex-col bg-[#202123] min-w-[260px] max-w-[260px]">
|
||||
<div className="flex items-center h-[60px] pl-2">
|
||||
<div className={`flex flex-col bg-[#202123] min-w-full sm:min-w-[260px] sm:max-w-[260px] z-10`}>
|
||||
<div className="flex items-center h-[60px] sm:pl-2 px-2">
|
||||
<button
|
||||
className="flex items-center w-[200px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700"
|
||||
className="flex items-center w-full sm:w-[200px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700"
|
||||
onClick={onNewConversation}
|
||||
>
|
||||
<IconPlus
|
||||
|
@ -35,13 +35,13 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
|||
</button>
|
||||
|
||||
<IconArrowBarLeft
|
||||
className="ml-1 p-1 text-neutral-300 cursor-pointer hover:text-neutral-400"
|
||||
className="ml-1 p-1 text-neutral-300 cursor-pointer hover:text-neutral-400 hidden sm:flex"
|
||||
size={38}
|
||||
onClick={onToggleSidebar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 justify-center overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Conversations
|
||||
loading={loading}
|
||||
conversations={conversations}
|
||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
|
||||
export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center border-t border-neutral-500 px-2 py-4 text-sm space-y-4">
|
||||
<div className="flex flex-col items-center border-t border-neutral-500 px-2 py-4 text-sm space-y-2">
|
||||
<SidebarButton
|
||||
text={lightMode === "light" ? "Dark mode" : "Light mode"}
|
||||
icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Chat } from "@/components/Chat/Chat";
|
||||
import { Navbar } from "@/components/Mobile/Navbar";
|
||||
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
||||
import { Conversation, Message, OpenAIModel } from "@/types";
|
||||
import { IconArrowBarRight } from "@tabler/icons-react";
|
||||
import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react";
|
||||
import Head from "next/head";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
@ -40,7 +41,7 @@ export default function Home() {
|
|||
|
||||
if (!response.ok) {
|
||||
setLoading(false);
|
||||
throw new Error(response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = response.body;
|
||||
|
@ -148,7 +149,7 @@ export default function Home() {
|
|||
|
||||
const newConversation: Conversation = {
|
||||
id: lastConversation ? lastConversation.id + 1 : 1,
|
||||
name: "New conversation",
|
||||
name: `Conversation ${lastConversation ? lastConversation.id + 1 : 1}`,
|
||||
messages: []
|
||||
};
|
||||
|
||||
|
@ -174,8 +175,8 @@ export default function Home() {
|
|||
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
||||
|
||||
if (updatedConversations.length > 0) {
|
||||
setSelectedConversation(updatedConversations[0]);
|
||||
localStorage.setItem("selectedConversation", JSON.stringify(updatedConversations[0]));
|
||||
setSelectedConversation(updatedConversations[updatedConversations.length - 1]);
|
||||
localStorage.setItem("selectedConversation", JSON.stringify(updatedConversations[updatedConversations.length - 1]));
|
||||
} else {
|
||||
setSelectedConversation({
|
||||
id: 1,
|
||||
|
@ -202,6 +203,10 @@ export default function Home() {
|
|||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
if (window.innerWidth < 640) {
|
||||
setShowSidebar(false);
|
||||
}
|
||||
|
||||
const conversationHistory = localStorage.getItem("conversationHistory");
|
||||
|
||||
if (conversationHistory) {
|
||||
|
@ -214,7 +219,7 @@ export default function Home() {
|
|||
} else {
|
||||
setSelectedConversation({
|
||||
id: 1,
|
||||
name: "",
|
||||
name: "New conversation",
|
||||
messages: []
|
||||
});
|
||||
}
|
||||
|
@ -239,39 +244,54 @@ export default function Home() {
|
|||
</Head>
|
||||
|
||||
{selectedConversation && (
|
||||
<div className={`flex h-screen text-white ${lightMode}`}>
|
||||
{showSidebar ? (
|
||||
<Sidebar
|
||||
loading={messageIsStreaming}
|
||||
conversations={conversations}
|
||||
lightMode={lightMode}
|
||||
<div className={`flex flex-col h-screen w-screen text-white ${lightMode}`}>
|
||||
<div className="sm:hidden w-full fixed top-0">
|
||||
<Navbar
|
||||
selectedConversation={selectedConversation}
|
||||
apiKey={apiKey}
|
||||
onToggleLightMode={handleLightMode}
|
||||
onNewConversation={handleNewConversation}
|
||||
onSelectConversation={handleSelectConversation}
|
||||
onDeleteConversation={handleDeleteConversation}
|
||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||
onRenameConversation={handleRenameConversation}
|
||||
onApiKeyChange={handleApiKeyChange}
|
||||
/>
|
||||
) : (
|
||||
<IconArrowBarRight
|
||||
className="absolute top-1 left-4 text-black dark:text-white cursor-pointer hover:text-gray-400 dark:hover:text-gray-300"
|
||||
size={32}
|
||||
onClick={() => setShowSidebar(!showSidebar)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Chat
|
||||
messageIsStreaming={messageIsStreaming}
|
||||
model={model}
|
||||
messages={selectedConversation.messages}
|
||||
loading={loading}
|
||||
lightMode={lightMode}
|
||||
onSend={handleSend}
|
||||
onSelect={setModel}
|
||||
/>
|
||||
<div className="flex h-full w-full pt-[48px] sm:pt-0">
|
||||
{showSidebar ? (
|
||||
<>
|
||||
<Sidebar
|
||||
loading={messageIsStreaming}
|
||||
conversations={conversations}
|
||||
lightMode={lightMode}
|
||||
selectedConversation={selectedConversation}
|
||||
apiKey={apiKey}
|
||||
onToggleLightMode={handleLightMode}
|
||||
onNewConversation={handleNewConversation}
|
||||
onSelectConversation={handleSelectConversation}
|
||||
onDeleteConversation={handleDeleteConversation}
|
||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||
onRenameConversation={handleRenameConversation}
|
||||
onApiKeyChange={handleApiKeyChange}
|
||||
/>
|
||||
|
||||
<IconArrowBarLeft
|
||||
className="fixed top-2.5 left-4 sm:top-1 sm:left-4 sm:text-neutral-700 dark:text-white cursor-pointer hover:text-gray-400 dark:hover:text-gray-300 h-7 w-7 sm:h-8 sm:w-8 sm:hidden"
|
||||
onClick={() => setShowSidebar(!showSidebar)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<IconArrowBarRight
|
||||
className="fixed top-2.5 left-4 sm:top-1.5 sm:left-4 sm:text-neutral-700 dark:text-white cursor-pointer hover:text-gray-400 dark:hover:text-gray-300 h-7 w-7 sm:h-8 sm:w-8"
|
||||
onClick={() => setShowSidebar(!showSidebar)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Chat
|
||||
messageIsStreaming={messageIsStreaming}
|
||||
model={model}
|
||||
messages={selectedConversation.messages}
|
||||
loading={loading}
|
||||
lightMode={lightMode}
|
||||
onSend={handleSend}
|
||||
onSelect={setModel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
Loading…
Reference in New Issue