mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-07-28 09:43:06 +08:00
feat: update ChatView layout
This commit is contained in:
@ -1,17 +1,24 @@
|
||||
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 { UserRole } from "../../types";
|
||||
import { generateUUID } from "../../utils";
|
||||
import Icon from "../Icon";
|
||||
|
||||
const Textarea = () => {
|
||||
const MessageTextarea = () => {
|
||||
const userStore = useUserStore();
|
||||
const chatStore = useChatStore();
|
||||
const messageStore = useMessageStore();
|
||||
const [value, setValue] = useState<string>("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
@ -49,11 +56,20 @@ const Textarea = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-auto border-t relative">
|
||||
<textarea ref={textareaRef} className="w-full h-full outline-none pt-2 px-2 resize-none" onChange={handleChange} rows={1} />
|
||||
<Icon.Send className="absolute bottom-2 right-2" onClick={handleSend} />
|
||||
<div className="w-full h-auto border rounded-md mb-4 px-2 py-1 relative shadow bg-white">
|
||||
<TextareaAutosize
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Textarea;
|
||||
export default MessageTextarea;
|
@ -1,4 +1,7 @@
|
||||
import { marked } from "marked";
|
||||
import { useUserStore } from "../../store";
|
||||
import { Message } from "../../types";
|
||||
import "highlight.js/styles/github.css";
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
@ -6,8 +9,22 @@ interface Props {
|
||||
|
||||
const Message = (props: Props) => {
|
||||
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;
|
||||
|
@ -4,6 +4,7 @@ import { User } from "../../types";
|
||||
const Sidebar = () => {
|
||||
const userStore = useUserStore();
|
||||
const chatStore = useChatStore();
|
||||
const currentChatUserId = chatStore.currentChat.userId;
|
||||
|
||||
const handleAssistantClick = (user: User) => {
|
||||
for (const chat of chatStore.chatList) {
|
||||
@ -16,13 +17,19 @@ const Sidebar = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full border-r p-2">
|
||||
<h2>Assistant list</h2>
|
||||
<div>
|
||||
<div className="w-52 lg:w-64 h-full transition-all shrink-0 border-r p-2 sticky top-0">
|
||||
<h2 className="pt-2 pb-4 w-full text-center">Assistant list</h2>
|
||||
<div className="w-full mt-2 flex flex-col justify-start items-start">
|
||||
{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}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useChatStore, useMessageStore, useUserStore } from "../../store";
|
||||
import MessageView from "./MessageView";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Textarea from "./Textarea";
|
||||
import MessageTextarea from "./MessageTextarea";
|
||||
|
||||
const ChatView = () => {
|
||||
const chatStore = useChatStore();
|
||||
@ -12,14 +12,16 @@ const ChatView = () => {
|
||||
const messageList = messageStore.messageList.filter((message) => message.chatId === currentChat?.id);
|
||||
|
||||
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 />
|
||||
<main className="w-full">
|
||||
<p className="w-full text-center py-2 border-b">{chatTitle}</p>
|
||||
<div className="py-2">
|
||||
{messageList.length === 0 ? <p>no message</p> : messageList.map((message) => <MessageView key={message.id} message={message} />)}
|
||||
<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="sticky top-0 w-full text-center py-4 border-b bg-white">{chatTitle}</p>
|
||||
<div className="p-2 w-full h-auto grow max-w-3xl py-1 px-4 mx-auto">
|
||||
{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>
|
||||
<Textarea />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
@ -12,15 +12,18 @@
|
||||
"csstype": "^3.1.1",
|
||||
"highlight.js": "^11.7.0",
|
||||
"lucide-react": "^0.125.0",
|
||||
"marked": "^4.2.12",
|
||||
"next": "^13.2.4",
|
||||
"openai": "^3.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"uuid": "^9.0.0",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/uuid": "^9.0.1",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AppProps } from "next/app";
|
||||
import React from "react";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import "../styles/global.css";
|
||||
import "../styles/tailwind.css";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
|
@ -12,7 +12,7 @@ const ChatPage: NextPage = () => {
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className="w-full min-h-screen flex flex-col items-center justify-center">
|
||||
<main className="w-full h-screen">
|
||||
<ChatView />
|
||||
</main>
|
||||
</div>
|
||||
|
70
pnpm-lock.yaml
generated
70
pnpm-lock.yaml
generated
@ -1,6 +1,7 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@types/marked': ^4.0.8
|
||||
'@types/node': ^18.11.18
|
||||
'@types/react': ^18.0.26
|
||||
'@types/uuid': ^9.0.1
|
||||
@ -12,12 +13,14 @@ specifiers:
|
||||
eslint-config-next: 12.2.3
|
||||
highlight.js: ^11.7.0
|
||||
lucide-react: ^0.125.0
|
||||
marked: ^4.2.12
|
||||
next: ^13.2.4
|
||||
openai: ^3.0.0
|
||||
postcss: ^8.4.20
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
react-hot-toast: ^2.4.0
|
||||
react-textarea-autosize: ^8.4.0
|
||||
tailwindcss: ^3.2.4
|
||||
typescript: ^4.9.4
|
||||
uuid: ^9.0.0
|
||||
@ -29,15 +32,18 @@ dependencies:
|
||||
csstype: 3.1.1
|
||||
highlight.js: 11.7.0
|
||||
lucide-react: 0.125.0_react@18.2.0
|
||||
marked: 4.2.12
|
||||
next: 13.2.4_biqbaboplfbrettd7655fr4n2y
|
||||
openai: 3.2.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-hot-toast: 2.4.0_owo25xnefcwdq3zjgtohz6dbju
|
||||
react-textarea-autosize: 8.4.0_pmekkgnqduwlme35zpnqhenc34
|
||||
uuid: 9.0.0
|
||||
zustand: 4.3.6_react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
'@types/marked': 4.0.8
|
||||
'@types/node': 18.15.3
|
||||
'@types/react': 18.0.28
|
||||
'@types/uuid': 9.0.1
|
||||
@ -55,7 +61,6 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.13.11
|
||||
dev: true
|
||||
|
||||
/@eslint/eslintrc/1.4.1:
|
||||
resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
|
||||
@ -251,13 +256,16 @@ packages:
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
dev: true
|
||||
|
||||
/@types/marked/4.0.8:
|
||||
resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==}
|
||||
dev: true
|
||||
|
||||
/@types/node/18.15.3:
|
||||
resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==}
|
||||
dev: true
|
||||
|
||||
/@types/prop-types/15.7.5:
|
||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
dev: true
|
||||
|
||||
/@types/react/18.0.28:
|
||||
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
|
||||
@ -265,11 +273,9 @@ packages:
|
||||
'@types/prop-types': 15.7.5
|
||||
'@types/scheduler': 0.16.2
|
||||
csstype: 3.1.1
|
||||
dev: true
|
||||
|
||||
/@types/scheduler/0.16.2:
|
||||
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||
dev: true
|
||||
|
||||
/@types/uuid/9.0.1:
|
||||
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
|
||||
@ -1666,6 +1672,12 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/marked/4.2.12:
|
||||
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
|
||||
engines: {node: '>= 12'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/merge2/1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -2054,6 +2066,20 @@ packages:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -2076,7 +2102,6 @@ packages:
|
||||
|
||||
/regenerator-runtime/0.13.11:
|
||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||
dev: true
|
||||
|
||||
/regexp.prototype.flags/1.4.3:
|
||||
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
|
||||
@ -2404,6 +2429,41 @@ packages:
|
||||
punycode: 2.3.0
|
||||
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:
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
|
@ -9,6 +9,13 @@ const assistantList: User[] = [
|
||||
avatar: "",
|
||||
role: UserRole.Assistant,
|
||||
},
|
||||
{
|
||||
id: "assistant-dba",
|
||||
name: "Great DBA Bot",
|
||||
description: "",
|
||||
avatar: "",
|
||||
role: UserRole.Assistant,
|
||||
},
|
||||
];
|
||||
|
||||
const localUser: User = {
|
||||
|
@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
15
styles/tailwind.css
Normal file
15
styles/tailwind.css
Normal 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;
|
||||
}
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
import { ChatCompletionResponseMessage, Configuration, OpenAIApi } from "openai";
|
||||
|
||||
export interface ChatCompletionResponse {
|
||||
message: ChatCompletionResponseMessage;
|
||||
}
|
||||
import { Configuration, OpenAIApi } from "openai";
|
||||
|
||||
const configuration = new Configuration({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
|
Reference in New Issue
Block a user