diff --git a/app/404.tsx b/app/404.tsx new file mode 100644 index 00000000..0cd25afd --- /dev/null +++ b/app/404.tsx @@ -0,0 +1,12 @@ +import Link from "next/link"; + +export default function FourOhFour() { + return ( + <> +

404 - Page Not Found

+ + Go back home + + + ); +} diff --git a/app/500.tsx b/app/500.tsx new file mode 100644 index 00000000..c518c57b --- /dev/null +++ b/app/500.tsx @@ -0,0 +1,12 @@ +import Link from "next/link"; + +export default function FourOhFour() { + return ( + <> +

500 - Page Not Found

+ + Go back home + + + ); +} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index df43a3fe..a6b73c9d 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,5 +1,6 @@ import { OpenAIApi, Configuration } from "openai"; import { apiKey } from "./config"; +import { ChatRequest } from "./typing"; // set up openai api client const config = new Configuration({ @@ -7,17 +8,12 @@ const config = new Configuration({ }); const openai = new OpenAIApi(config); -export async function GET(req: Request) { +export async function POST(req: Request) { try { + const requestBody = (await req.json()) as ChatRequest; const completion = await openai.createChatCompletion( { - messages: [ - { - role: "user", - content: "hello", - }, - ], - model: "gpt-3.5-turbo", + ...requestBody, }, { proxy: { diff --git a/app/api/hello/route.ts b/app/api/hello/route.ts deleted file mode 100644 index d1cc6ee2..00000000 --- a/app/api/hello/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET(request: Request) { - return new Response('Hello, Next.js!') -} diff --git a/app/components/button.tsx b/app/components/button.tsx index e45f8352..8aea3ea2 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -15,6 +15,7 @@ export function IconButton(props: { styles["icon-button"] + ` ${props.bordered && styles.border} ${props.className ?? ""}` } + onClick={props.onClick} >
{props.icon}
{props.text && ( diff --git a/app/components/home.module.css b/app/components/home.module.css index 06dcf965..06acbf86 100644 --- a/app/components/home.module.css +++ b/app/components/home.module.css @@ -1,7 +1,7 @@ .container { max-width: 1080px; - max-height: 780px; min-width: 600px; + min-height: 480px; width: 90vw; height: 90vh; background-color: var(--white); @@ -19,7 +19,7 @@ background-color: var(--second); display: flex; flex-direction: column; - box-shadow:inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05); + box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05); } .sidebar-header { @@ -40,7 +40,7 @@ } .sidebar-sub-title { - font-size: 12px; + font-size: 12px; font-weight: 400px; } @@ -59,9 +59,23 @@ border-radius: 10px; margin-bottom: 10px; box-shadow: var(--card-shadow); - transition: all .3s ease; + transition: all 0.3s ease; cursor: pointer; user-select: none; + border: 2px solid transparent; + position: relative; + overflow: hidden; +} + +@keyframes slide-in { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0px); + } } .chat-item:hover { @@ -69,12 +83,34 @@ } .chat-item-selected { - border: 2px solid var(--primary); + border-color: var(--primary); } .chat-item-title { font-size: 14px; font-weight: bolder; + display: block; + width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-item-delete { + position: absolute; + top: 10px; + right: -20px; + transition: all ease 0.3s; + opacity: 0; +} + +.chat-item:hover > .chat-item-delete { + opacity: 0.5; + right: 10px; +} + +.chat-item:hover > .chat-item-delete:hover { + opacity: 1; } .chat-item-info { @@ -85,8 +121,11 @@ margin-top: 8px; } -.chat-item-count {} -.chat-item-date {} +.chat-item-count { +} + +.chat-item-date { +} .sidebar-tail { display: flex; @@ -97,6 +136,7 @@ .sidebar-actions { display: inline-flex; } + .sidebar-action:last-child { margin-left: 15px; } @@ -116,17 +156,26 @@ justify-content: space-between; align-items: center; } + .chat-header-title { font-size: 20px; font-weight: bolder; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } + .chat-header-sub-title { font-size: 14px; margin-top: 5px; } + .chat-actions { display: inline-flex; } + .chat-action-button { margin-left: 10px; } @@ -137,9 +186,10 @@ padding: 20px; margin-bottom: 100px; } + .chat-message { display: flex; - flex-direction: row; + flex-direction: row; } .chat-message-user { @@ -148,42 +198,68 @@ } .chat-message-container { - width: 60%; + max-width: 60%; display: flex; flex-direction: column; align-items: flex-start; + animation: slide-in ease 0.3s; } .chat-message-user > .chat-message-container { align-items: flex-end; } -.chat-message-avtar {} +.chat-message-avatar { + margin-top: 20px; +} + +.chat-message-status { + font-size: 12px; + color: #aaa; + line-height: 1.5; + margin-top: 5px; +} + +.user-avtar { + height: 30px; + width: 30px; + display: flex; + align-items: center; + justify-content: center; + border: var(--border-in-light); + box-shadow: var(--card-shadow); + border-radius: 10px; +} + .chat-message-item { + margin-top: 5px; border-radius: 10px; background-color: rgba(0, 0, 0, 0.05); padding: 10px; font-size: 14px; - margin-top: 5px; user-select: text; + word-break: break-all; } .chat-message-user > .chat-message-container > .chat-message-item { background-color: var(--second); } -.chat-message-actions{ +.chat-message-actions { display: flex; flex-direction: row-reverse; width: 100%; - padding: 5px 10px; + padding-top: 5px; box-sizing: border-box; } -.chat-message-action-date{ + +.chat-message-action-date { font-size: 12px; color: #aaa; } -.chat-message-action-button{} + +.chat-message-action-button { +} .chat-input-panel { position: absolute; @@ -199,7 +275,9 @@ flex: 1; } -.chat-input-panel-multi {} +.chat-input-panel-multi { +} + .chat-input { height: 100%; width: 100%; @@ -217,11 +295,11 @@ border: 1px solid var(--primary); } -.chat-input-send{ +.chat-input-send { background-color: var(--primary); color: white; position: absolute; right: 30px; bottom: 10px; -} \ No newline at end of file +} diff --git a/app/components/home.tsx b/app/components/home.tsx index 18172acc..c09de3d6 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -1,5 +1,8 @@ "use client"; +import { useState, useRef, useLayoutEffect, useEffect } from "react"; +import ReactMarkdown from "react-markdown"; + import { IconButton } from "./button"; import styles from "./home.module.css"; @@ -10,11 +13,23 @@ import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; import ExportIcon from "../icons/export.svg"; import BotIcon from "../icons/bot.svg"; -import UserIcon from "../icons/user.svg"; import AddIcon from "../icons/add.svg"; +import DeleteIcon from "../icons/delete.svg"; +import LoadingIcon from "../icons/three-dots.svg"; + +import { Message, useChatStore } from "../store"; + +export function Avatar(props: { role: Message["role"] }) { + if (props.role === "assistant") { + return ; + } + + return
🤣
; +} export function ChatItem(props: { onClick?: () => void; + onDelete?: () => void; title: string; count: number; time: string; @@ -25,58 +40,106 @@ export function ChatItem(props: { className={`${styles["chat-item"]} ${ props.selected && styles["chat-item-selected"] }`} + onClick={props.onClick} >
{props.title}
{props.count} 条对话
{props.time}
+
+ +
); } export function ChatList() { - const listData = new Array(5).fill({ - title: "这是一个标题", - count: 10, - time: new Date().toLocaleString(), - }); - - const selectedIndex = 0; + const [sessions, selectedIndex, selectSession, removeSession] = useChatStore( + (state) => [ + state.sessions, + state.currentSessionIndex, + state.selectSession, + state.removeSession, + ] + ); return (
- {listData.map((item, i) => ( - + {sessions.map((item, i) => ( + selectSession(i)} + onDelete={() => removeSession(i)} + /> ))}
); } export function Chat() { - const messages = [ - { - role: "user", - content: "这是一条消息", - date: new Date().toLocaleString(), - }, - { - role: "bot", - content: "这是一条回复".repeat(10), - date: new Date().toLocaleString(), - }, - ]; + type RenderMessage = Message & { preview?: boolean }; - const title = "这是一个标题"; - const count = 10; + const session = useChatStore((state) => state.currentSession()); + const [userInput, setUserInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const onUserInput = useChatStore((state) => state.onUserInput); + const onUserSubmit = () => { + if (userInput.length <= 0) return; + setIsLoading(true); + onUserInput(userInput).then(() => setIsLoading(false)); + setUserInput(""); + }; + const onInputKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" && (e.shiftKey || e.ctrlKey || e.metaKey)) { + onUserSubmit(); + e.preventDefault(); + } + }; + const latestMessageRef = useRef(null); + + const messages = (session.messages as RenderMessage[]) + .concat( + isLoading + ? [ + { + role: "assistant", + content: "……", + date: new Date().toLocaleString(), + preview: true, + }, + ] + : [] + ) + .concat( + userInput.length > 0 + ? [ + { + role: "user", + content: userInput, + date: new Date().toLocaleString(), + preview: true, + }, + ] + : [] + ); + + useEffect(() => { + latestMessageRef.current?.scrollIntoView(false); + }); return (
-
{title}
+
{session.topic}
- 与 ChatGPT 的 {count} 条对话 + 与 ChatGPT 的 {session.messages.length} 条对话
@@ -101,16 +164,25 @@ export function Chat() { } >
-
- {message.role === "user" ? : } +
+
+ {message.preview && ( +
正在输入…
+ )}
- {message.content} + {message.preview && !isUser ? ( + + ) : ( +
+ {message.content} +
+ )}
- {!isUser && ( + {!isUser && !message.preview && (
- {message.date} + {message.date.toLocaleString()}
)} @@ -118,19 +190,26 @@ export function Chat() {
); })} + + - +