feat: update ChatView layout

This commit is contained in:
Steven
2023-03-16 21:29:19 +08:00
parent 5ff430fe34
commit 4ea6ce143f
12 changed files with 154 additions and 34 deletions

View File

@ -1,17 +1,24 @@
import axios from "axios"; import axios from "axios";
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useChatStore, useMessageStore, useUserStore } from "../../store"; import { useChatStore, useMessageStore, useUserStore } from "../../store";
import { UserRole } from "../../types"; import { UserRole } from "../../types";
import { generateUUID } from "../../utils"; import { generateUUID } from "../../utils";
import Icon from "../Icon"; import Icon from "../Icon";
const Textarea = () => { const MessageTextarea = () => {
const userStore = useUserStore(); const userStore = useUserStore();
const chatStore = useChatStore(); const chatStore = useChatStore();
const messageStore = useMessageStore(); const messageStore = useMessageStore();
const [value, setValue] = useState<string>(""); const [value, setValue] = useState<string>("");
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus();
}
}, []);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value); setValue(e.target.value);
}; };
@ -49,11 +56,20 @@ const Textarea = () => {
}; };
return ( return (
<div className="w-full h-auto border-t relative"> <div className="w-full h-auto border rounded-md mb-4 px-2 py-1 relative shadow bg-white">
<textarea ref={textareaRef} className="w-full h-full outline-none pt-2 px-2 resize-none" onChange={handleChange} rows={1} /> <TextareaAutosize
<Icon.Send className="absolute bottom-2 right-2" onClick={handleSend} /> ref={textareaRef}
className="w-full h-full outline-none border-none bg-transparent pt-1 mt-1 px-2 resize-none hide-scrollbar"
rows={1}
minRows={1}
maxRows={5}
onChange={handleChange}
/>
<div className="absolute bottom-2 right-2 w-8 p-1 cursor-pointer rounded-md hover:shadow hover:bg-gray-100" onClick={handleSend}>
<Icon.Send className="w-full h-auto text-blue-800" />
</div>
</div> </div>
); );
}; };
export default Textarea; export default MessageTextarea;

View File

@ -1,4 +1,7 @@
import { marked } from "marked";
import { useUserStore } from "../../store";
import { Message } from "../../types"; import { Message } from "../../types";
import "highlight.js/styles/github.css";
interface Props { interface Props {
message: Message; message: Message;
@ -6,8 +9,22 @@ interface Props {
const Message = (props: Props) => { const Message = (props: Props) => {
const message = props.message; const message = props.message;
const userStore = useUserStore();
const currentUser = userStore.currentUser;
const isCurrentUser = message.creatorId === currentUser.id;
return <div>{message.content}</div>; return (
<div className={`w-full flex flex-row justify-start items-start my-4 ${isCurrentUser ? "justify-end pl-8 sm:pl-16" : "pr-8 sm:pr-16"}`}>
{isCurrentUser ? (
<div className="w-auto max-w-full bg-white px-3 py-2 rounded-lg rounded-tr-none shadow">{message.content}</div>
) : (
<div
className="w-auto max-w-full bg-white px-3 py-2 rounded-lg rounded-tl-none shadow"
dangerouslySetInnerHTML={{ __html: marked.parse(message.content) }}
></div>
)}
</div>
);
}; };
export default Message; export default Message;

View File

@ -4,6 +4,7 @@ import { User } from "../../types";
const Sidebar = () => { const Sidebar = () => {
const userStore = useUserStore(); const userStore = useUserStore();
const chatStore = useChatStore(); const chatStore = useChatStore();
const currentChatUserId = chatStore.currentChat.userId;
const handleAssistantClick = (user: User) => { const handleAssistantClick = (user: User) => {
for (const chat of chatStore.chatList) { for (const chat of chatStore.chatList) {
@ -16,13 +17,19 @@ const Sidebar = () => {
}; };
return ( return (
<div className="w-full border-r p-2"> <div className="w-52 lg:w-64 h-full transition-all shrink-0 border-r p-2 sticky top-0">
<h2>Assistant list</h2> <h2 className="pt-2 pb-4 w-full text-center">Assistant list</h2>
<div> <div className="w-full mt-2 flex flex-col justify-start items-start">
{userStore.assistantList.map((assistant) => ( {userStore.assistantList.map((assistant) => (
<p onClick={() => handleAssistantClick(assistant)} key={assistant.id}> <div
className={`w-full py-2 px-3 rounded-md mb-2 cursor-pointer hover:opacity-80 hover:bg-gray-100 ${
currentChatUserId === assistant.id && "shadow bg-gray-100 font-medium"
}`}
onClick={() => handleAssistantClick(assistant)}
key={assistant.id}
>
{assistant.name} {assistant.name}
</p> </div>
))} ))}
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { useChatStore, useMessageStore, useUserStore } from "../../store"; import { useChatStore, useMessageStore, useUserStore } from "../../store";
import MessageView from "./MessageView"; import MessageView from "./MessageView";
import Sidebar from "./Sidebar"; import Sidebar from "./Sidebar";
import Textarea from "./Textarea"; import MessageTextarea from "./MessageTextarea";
const ChatView = () => { const ChatView = () => {
const chatStore = useChatStore(); const chatStore = useChatStore();
@ -12,14 +12,16 @@ const ChatView = () => {
const messageList = messageStore.messageList.filter((message) => message.chatId === currentChat?.id); const messageList = messageStore.messageList.filter((message) => message.chatId === currentChat?.id);
return ( return (
<div className="w-full max-w-full lg:max-w-3xl border rounded-md grid grid-cols-[192px_1fr]"> <div className="relative w-full max-w-full h-full rounded-md flex flex-row justify-start items-start">
<Sidebar /> <Sidebar />
<main className="w-full"> <main className="relative grow w-auto h-full max-h-full flex flex-col justify-start items-start overflow-y-auto bg-gray-100">
<p className="w-full text-center py-2 border-b">{chatTitle}</p> <p className="sticky top-0 w-full text-center py-4 border-b bg-white">{chatTitle}</p>
<div className="py-2"> <div className="p-2 w-full h-auto grow max-w-3xl py-1 px-4 mx-auto">
{messageList.length === 0 ? <p>no message</p> : messageList.map((message) => <MessageView key={message.id} message={message} />)} {messageList.length === 0 ? <></> : messageList.map((message) => <MessageView key={message.id} message={message} />)}
</div>
<div className="sticky bottom-0 w-full max-w-3xl py-1 px-4 mx-auto">
<MessageTextarea />
</div> </div>
<Textarea />
</main> </main>
</div> </div>
); );

View File

@ -12,15 +12,18 @@
"csstype": "^3.1.1", "csstype": "^3.1.1",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"lucide-react": "^0.125.0", "lucide-react": "^0.125.0",
"marked": "^4.2.12",
"next": "^13.2.4", "next": "^13.2.4",
"openai": "^3.0.0", "openai": "^3.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-textarea-autosize": "^8.4.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"zustand": "^4.3.6" "zustand": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@types/marked": "^4.0.8",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",

View File

@ -1,7 +1,7 @@
import { AppProps } from "next/app"; import { AppProps } from "next/app";
import React from "react"; import React from "react";
import { Analytics } from "@vercel/analytics/react"; import { Analytics } from "@vercel/analytics/react";
import "../styles/global.css"; import "../styles/tailwind.css";
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (

View File

@ -12,7 +12,7 @@ const ChatPage: NextPage = () => {
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<main className="w-full min-h-screen flex flex-col items-center justify-center"> <main className="w-full h-screen">
<ChatView /> <ChatView />
</main> </main>
</div> </div>

70
pnpm-lock.yaml generated
View File

@ -1,6 +1,7 @@
lockfileVersion: 5.4 lockfileVersion: 5.4
specifiers: specifiers:
'@types/marked': ^4.0.8
'@types/node': ^18.11.18 '@types/node': ^18.11.18
'@types/react': ^18.0.26 '@types/react': ^18.0.26
'@types/uuid': ^9.0.1 '@types/uuid': ^9.0.1
@ -12,12 +13,14 @@ specifiers:
eslint-config-next: 12.2.3 eslint-config-next: 12.2.3
highlight.js: ^11.7.0 highlight.js: ^11.7.0
lucide-react: ^0.125.0 lucide-react: ^0.125.0
marked: ^4.2.12
next: ^13.2.4 next: ^13.2.4
openai: ^3.0.0 openai: ^3.0.0
postcss: ^8.4.20 postcss: ^8.4.20
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
react-hot-toast: ^2.4.0 react-hot-toast: ^2.4.0
react-textarea-autosize: ^8.4.0
tailwindcss: ^3.2.4 tailwindcss: ^3.2.4
typescript: ^4.9.4 typescript: ^4.9.4
uuid: ^9.0.0 uuid: ^9.0.0
@ -29,15 +32,18 @@ dependencies:
csstype: 3.1.1 csstype: 3.1.1
highlight.js: 11.7.0 highlight.js: 11.7.0
lucide-react: 0.125.0_react@18.2.0 lucide-react: 0.125.0_react@18.2.0
marked: 4.2.12
next: 13.2.4_biqbaboplfbrettd7655fr4n2y next: 13.2.4_biqbaboplfbrettd7655fr4n2y
openai: 3.2.1 openai: 3.2.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-hot-toast: 2.4.0_owo25xnefcwdq3zjgtohz6dbju react-hot-toast: 2.4.0_owo25xnefcwdq3zjgtohz6dbju
react-textarea-autosize: 8.4.0_pmekkgnqduwlme35zpnqhenc34
uuid: 9.0.0 uuid: 9.0.0
zustand: 4.3.6_react@18.2.0 zustand: 4.3.6_react@18.2.0
devDependencies: devDependencies:
'@types/marked': 4.0.8
'@types/node': 18.15.3 '@types/node': 18.15.3
'@types/react': 18.0.28 '@types/react': 18.0.28
'@types/uuid': 9.0.1 '@types/uuid': 9.0.1
@ -55,7 +61,6 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
regenerator-runtime: 0.13.11 regenerator-runtime: 0.13.11
dev: true
/@eslint/eslintrc/1.4.1: /@eslint/eslintrc/1.4.1:
resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
@ -251,13 +256,16 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true dev: true
/@types/marked/4.0.8:
resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==}
dev: true
/@types/node/18.15.3: /@types/node/18.15.3:
resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==} resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==}
dev: true dev: true
/@types/prop-types/15.7.5: /@types/prop-types/15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
dev: true
/@types/react/18.0.28: /@types/react/18.0.28:
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==} resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
@ -265,11 +273,9 @@ packages:
'@types/prop-types': 15.7.5 '@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2 '@types/scheduler': 0.16.2
csstype: 3.1.1 csstype: 3.1.1
dev: true
/@types/scheduler/0.16.2: /@types/scheduler/0.16.2:
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
dev: true
/@types/uuid/9.0.1: /@types/uuid/9.0.1:
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
@ -1666,6 +1672,12 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/marked/4.2.12:
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
engines: {node: '>= 12'}
hasBin: true
dev: false
/merge2/1.4.1: /merge2/1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -2054,6 +2066,20 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true dev: true
/react-textarea-autosize/8.4.0_pmekkgnqduwlme35zpnqhenc34:
resolution: {integrity: sha512-YrTFaEHLgJsi8sJVYHBzYn+mkP3prGkmP2DKb/tm0t7CLJY5t1Rxix8070LAKb0wby7bl/lf2EeHkuMihMZMwQ==}
engines: {node: '>=10'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.21.0
react: 18.2.0
use-composed-ref: 1.3.0_react@18.2.0
use-latest: 1.2.1_pmekkgnqduwlme35zpnqhenc34
transitivePeerDependencies:
- '@types/react'
dev: false
/react/18.2.0: /react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2076,7 +2102,6 @@ packages:
/regenerator-runtime/0.13.11: /regenerator-runtime/0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: true
/regexp.prototype.flags/1.4.3: /regexp.prototype.flags/1.4.3:
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
@ -2404,6 +2429,41 @@ packages:
punycode: 2.3.0 punycode: 2.3.0
dev: true dev: true
/use-composed-ref/1.3.0_react@18.2.0:
resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/use-isomorphic-layout-effect/1.1.2_pmekkgnqduwlme35zpnqhenc34:
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
react: 18.2.0
dev: false
/use-latest/1.2.1_pmekkgnqduwlme35zpnqhenc34:
resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==}
peerDependencies:
'@types/react': '*'
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
react: 18.2.0
use-isomorphic-layout-effect: 1.1.2_pmekkgnqduwlme35zpnqhenc34
dev: false
/use-sync-external-store/1.2.0_react@18.2.0: /use-sync-external-store/1.2.0_react@18.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies: peerDependencies:

View File

@ -9,6 +9,13 @@ const assistantList: User[] = [
avatar: "", avatar: "",
role: UserRole.Assistant, role: UserRole.Assistant,
}, },
{
id: "assistant-dba",
name: "Great DBA Bot",
description: "",
avatar: "",
role: UserRole.Assistant,
},
]; ];
const localUser: User = { const localUser: User = {

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

15
styles/tailwind.css Normal file
View File

@ -0,0 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Chrome, Safari and Opera */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
}

View File

@ -1,8 +1,4 @@
import { ChatCompletionResponseMessage, Configuration, OpenAIApi } from "openai"; import { Configuration, OpenAIApi } from "openai";
export interface ChatCompletionResponse {
message: ChatCompletionResponseMessage;
}
const configuration = new Configuration({ const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY,