add more mobile ui (#18)
This commit is contained in:
parent
263c5c33ae
commit
7e6651dea7
|
@ -23,6 +23,7 @@ Expect frequent improvements.
|
||||||
- [ ] Mobile view
|
- [ ] Mobile view
|
||||||
- [ ] Saving via data export
|
- [ ] Saving via data export
|
||||||
- [ ] Folders
|
- [ ] Folders
|
||||||
|
- [ ] Change default prompt
|
||||||
|
|
||||||
**Recent updates:**
|
**Recent updates:**
|
||||||
|
|
||||||
|
|
|
@ -27,22 +27,22 @@ export const Chat: FC<Props> = ({ model, messages, messageIsStreaming, loading,
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-col dark:bg-[#343541]">
|
<div className="flex-1 overflow-scroll dark:bg-[#343541]">
|
||||||
<div className="flex-1 overflow-auto">
|
<div>
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-center pt-8 overflow-auto">
|
<div className="flex justify-center pt-8">
|
||||||
<ModelSelect
|
<ModelSelect
|
||||||
model={model}
|
model={model}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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) => (
|
{messages.map((message, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
|
@ -51,18 +51,21 @@ export const Chat: FC<Props> = ({ model, messages, messageIsStreaming, loading,
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{loading && <ChatLoader />}
|
{loading && <ChatLoader />}
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
|
<div
|
||||||
|
className="bg-white dark:bg-[#343541] h-24 sm:h-32"
|
||||||
|
ref={messagesEndRef}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-[100px] w-[340px] sm:w-[400px] md:w-[500px] lg:w-[700px] xl:w-[800px] mx-auto">
|
<ChatInput
|
||||||
<ChatInput
|
messageIsStreaming={messageIsStreaming}
|
||||||
messageIsStreaming={messageIsStreaming}
|
onSend={onSend}
|
||||||
onSend={onSend}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,14 +32,27 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming }) => {
|
||||||
alert("Please enter a message");
|
alert("Please enter a message");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSend({ role: "user", content });
|
onSend({ role: "user", content });
|
||||||
setContent("");
|
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>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (!isTyping && e.key === "Enter" && !e.shiftKey) {
|
if (!isTyping) {
|
||||||
e.preventDefault();
|
if (e.key === "Enter" && !e.shiftKey && !isMobile()) {
|
||||||
handleSend();
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,32 +64,31 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming }) => {
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<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">
|
||||||
<div className="absolute bottom-[-80px] w-full">
|
<textarea
|
||||||
<textarea
|
ref={textareaRef}
|
||||||
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"
|
||||||
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={{
|
||||||
style={{
|
resize: "none",
|
||||||
resize: "none",
|
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
maxHeight: "400px",
|
||||||
maxHeight: "400px",
|
overflow: "auto"
|
||||||
overflow: "auto"
|
}}
|
||||||
}}
|
placeholder="Type a message..."
|
||||||
placeholder="Type a message..."
|
value={content}
|
||||||
value={content}
|
rows={1}
|
||||||
rows={1}
|
onCompositionStart={() => setIsTyping(true)}
|
||||||
onCompositionStart={() => setIsTyping(true)}
|
onCompositionEnd={() => setIsTyping(false)}
|
||||||
onCompositionEnd={() => setIsTyping(false)}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyDown={handleKeyDown}
|
/>
|
||||||
/>
|
|
||||||
<button
|
<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"
|
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}
|
onClick={handleSend}
|
||||||
>
|
>
|
||||||
<IconSend size={18} />
|
<IconSend size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</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 }) => {
|
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onRenameConversation, onApiKeyChange }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-[#202123] min-w-[260px] max-w-[260px]">
|
<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] pl-2">
|
<div className="flex items-center h-[60px] sm:pl-2 px-2">
|
||||||
<button
|
<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}
|
onClick={onNewConversation}
|
||||||
>
|
>
|
||||||
<IconPlus
|
<IconPlus
|
||||||
|
@ -35,13 +35,13 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<IconArrowBarLeft
|
<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}
|
size={38}
|
||||||
onClick={onToggleSidebar}
|
onClick={onToggleSidebar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 justify-center overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Conversations
|
<Conversations
|
||||||
loading={loading}
|
loading={loading}
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
|
|
||||||
export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange }) => {
|
export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange }) => {
|
||||||
return (
|
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
|
<SidebarButton
|
||||||
text={lightMode === "light" ? "Dark mode" : "Light mode"}
|
text={lightMode === "light" ? "Dark mode" : "Light mode"}
|
||||||
icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />}
|
icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Chat } from "@/components/Chat/Chat";
|
import { Chat } from "@/components/Chat/Chat";
|
||||||
|
import { Navbar } from "@/components/Mobile/Navbar";
|
||||||
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
||||||
import { Conversation, Message, OpenAIModel } from "@/types";
|
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 Head from "next/head";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ export default function Home() {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
throw new Error(response.statusText);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = response.body;
|
const data = response.body;
|
||||||
|
@ -148,7 +149,7 @@ export default function Home() {
|
||||||
|
|
||||||
const newConversation: Conversation = {
|
const newConversation: Conversation = {
|
||||||
id: lastConversation ? lastConversation.id + 1 : 1,
|
id: lastConversation ? lastConversation.id + 1 : 1,
|
||||||
name: "New conversation",
|
name: `Conversation ${lastConversation ? lastConversation.id + 1 : 1}`,
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -174,8 +175,8 @@ export default function Home() {
|
||||||
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations));
|
||||||
|
|
||||||
if (updatedConversations.length > 0) {
|
if (updatedConversations.length > 0) {
|
||||||
setSelectedConversation(updatedConversations[0]);
|
setSelectedConversation(updatedConversations[updatedConversations.length - 1]);
|
||||||
localStorage.setItem("selectedConversation", JSON.stringify(updatedConversations[0]));
|
localStorage.setItem("selectedConversation", JSON.stringify(updatedConversations[updatedConversations.length - 1]));
|
||||||
} else {
|
} else {
|
||||||
setSelectedConversation({
|
setSelectedConversation({
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -202,6 +203,10 @@ export default function Home() {
|
||||||
setApiKey(apiKey);
|
setApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.innerWidth < 640) {
|
||||||
|
setShowSidebar(false);
|
||||||
|
}
|
||||||
|
|
||||||
const conversationHistory = localStorage.getItem("conversationHistory");
|
const conversationHistory = localStorage.getItem("conversationHistory");
|
||||||
|
|
||||||
if (conversationHistory) {
|
if (conversationHistory) {
|
||||||
|
@ -214,7 +219,7 @@ export default function Home() {
|
||||||
} else {
|
} else {
|
||||||
setSelectedConversation({
|
setSelectedConversation({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "",
|
name: "New conversation",
|
||||||
messages: []
|
messages: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -239,39 +244,54 @@ export default function Home() {
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
{selectedConversation && (
|
{selectedConversation && (
|
||||||
<div className={`flex h-screen text-white ${lightMode}`}>
|
<div className={`flex flex-col h-screen w-screen text-white ${lightMode}`}>
|
||||||
{showSidebar ? (
|
<div className="sm:hidden w-full fixed top-0">
|
||||||
<Sidebar
|
<Navbar
|
||||||
loading={messageIsStreaming}
|
|
||||||
conversations={conversations}
|
|
||||||
lightMode={lightMode}
|
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
||||||
apiKey={apiKey}
|
|
||||||
onToggleLightMode={handleLightMode}
|
|
||||||
onNewConversation={handleNewConversation}
|
onNewConversation={handleNewConversation}
|
||||||
onSelectConversation={handleSelectConversation}
|
|
||||||
onDeleteConversation={handleDeleteConversation}
|
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
|
||||||
onRenameConversation={handleRenameConversation}
|
|
||||||
onApiKeyChange={handleApiKeyChange}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<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)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Chat
|
<div className="flex h-full w-full pt-[48px] sm:pt-0">
|
||||||
messageIsStreaming={messageIsStreaming}
|
{showSidebar ? (
|
||||||
model={model}
|
<>
|
||||||
messages={selectedConversation.messages}
|
<Sidebar
|
||||||
loading={loading}
|
loading={messageIsStreaming}
|
||||||
lightMode={lightMode}
|
conversations={conversations}
|
||||||
onSend={handleSend}
|
lightMode={lightMode}
|
||||||
onSelect={setModel}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Reference in New Issue