forked from XiaoMo/ChatGPT-Next-Web
feat: add mobile support
This commit is contained in:
parent
76a6341c7b
commit
1fae774bb2
@ -20,14 +20,12 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
@include container();
|
@include container();
|
||||||
|
|
||||||
max-width: 1080px;
|
|
||||||
max-height: 864px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tight-container {
|
.tight-container {
|
||||||
--window-width: 100vw;
|
--window-width: 100vw;
|
||||||
--window-height: 100vh;
|
--window-height: 100vh;
|
||||||
|
--window-content-width: calc(var(--window-width) - var(--sidebar-width));
|
||||||
|
|
||||||
@include container();
|
@include container();
|
||||||
|
|
||||||
@ -44,6 +42,43 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
width: var(--window-content-width);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
min-width: unset;
|
||||||
|
min-height: unset;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
z-index: 999;
|
||||||
|
border-bottom-left-radius: 20px;
|
||||||
|
border-bottom-right-radius: 20px;
|
||||||
|
height: 80vh;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-show {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
@ -72,7 +107,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-list {
|
.chat-list {
|
||||||
width: 260px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item {
|
.chat-item {
|
||||||
@ -159,13 +193,8 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action:last-child {
|
.sidebar-action:not(:last-child) {
|
||||||
margin-left: 15px;
|
margin-right: 15px;
|
||||||
}
|
|
||||||
|
|
||||||
.window-content {
|
|
||||||
width: var(--window-content-width);
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
@ -193,7 +222,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-container {
|
.chat-message-container {
|
||||||
max-width: 80%;
|
max-width: var(--message-max-width);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -227,6 +256,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-item {
|
.chat-message-item {
|
||||||
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -255,9 +285,6 @@
|
|||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-action-button {
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
@ -272,15 +299,12 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-panel-multi {
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input {
|
.chat-input {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
@ -21,22 +21,13 @@ import BotIcon from "../icons/bot.svg";
|
|||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
|
import MenuIcon from "../icons/menu.svg";
|
||||||
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
|
||||||
import { Message, SubmitKey, useChatStore, Theme } from "../store";
|
import { Message, SubmitKey, useChatStore, Theme } from "../store";
|
||||||
import { Settings } from "./settings";
|
import { Settings } from "./settings";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
export const LazySettings = dynamic(
|
|
||||||
async () => await (await import("./settings")).Settings,
|
|
||||||
{
|
|
||||||
loading: () => (
|
|
||||||
<div className="">
|
|
||||||
<LoadingIcon />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function Markdown(props: { content: string }) {
|
export function Markdown(props: { content: string }) {
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown remarkPlugins={[RemarkMath]} rehypePlugins={[RehypeKatex]}>
|
<ReactMarkdown remarkPlugins={[RemarkMath]} rehypePlugins={[RehypeKatex]}>
|
||||||
@ -134,7 +125,7 @@ function useSubmitHandler() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat(props: { showSideBar?: () => void }) {
|
||||||
type RenderMessage = Message & { preview?: boolean };
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
const session = useChatStore((state) => state.currentSession());
|
const session = useChatStore((state) => state.currentSession());
|
||||||
@ -200,6 +191,14 @@ export function Chat() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-actions"]}>
|
<div className={styles["window-actions"]}>
|
||||||
|
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
||||||
|
<IconButton
|
||||||
|
icon={<MenuIcon />}
|
||||||
|
bordered
|
||||||
|
title="查看消息列表"
|
||||||
|
onClick={props?.showSideBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<BrainIcon />}
|
icon={<BrainIcon />}
|
||||||
@ -300,6 +299,7 @@ function useSwitchTheme() {
|
|||||||
export function Home() {
|
export function Home() {
|
||||||
const [createNewSession] = useChatStore((state) => [state.newSession]);
|
const [createNewSession] = useChatStore((state) => [state.newSession]);
|
||||||
const loading = !useChatStore?.persist?.hasHydrated();
|
const loading = !useChatStore?.persist?.hasHydrated();
|
||||||
|
const [showSideBar, setShowSideBar] = useState(true);
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
const [openSettings, setOpenSettings] = useState(false);
|
const [openSettings, setOpenSettings] = useState(false);
|
||||||
@ -322,7 +322,9 @@ export function Home() {
|
|||||||
config.tightBorder ? styles["tight-container"] : styles.container
|
config.tightBorder ? styles["tight-container"] : styles.container
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={styles.sidebar}>
|
<div
|
||||||
|
className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
|
||||||
|
>
|
||||||
<div className={styles["sidebar-header"]}>
|
<div className={styles["sidebar-header"]}>
|
||||||
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
||||||
<div className={styles["sidebar-sub-title"]}>
|
<div className={styles["sidebar-sub-title"]}>
|
||||||
@ -342,6 +344,12 @@ export function Home() {
|
|||||||
|
|
||||||
<div className={styles["sidebar-tail"]}>
|
<div className={styles["sidebar-tail"]}>
|
||||||
<div className={styles["sidebar-actions"]}>
|
<div className={styles["sidebar-actions"]}>
|
||||||
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
onClick={() => setShowSideBar(!showSideBar)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles["sidebar-action"]}>
|
<div className={styles["sidebar-action"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<SettingsIcon />}
|
icon={<SettingsIcon />}
|
||||||
@ -365,7 +373,11 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles["window-content"]}>
|
<div className={styles["window-content"]}>
|
||||||
{openSettings ? <LazySettings /> : <Chat key="chat" />}
|
{openSettings ? (
|
||||||
|
<Settings closeSettings={() => setOpenSettings(false)} />
|
||||||
|
) : (
|
||||||
|
<Chat key="chat" showSideBar={() => setShowSideBar(true)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -5,15 +5,15 @@ import EmojiPicker, { Emoji, Theme as EmojiTheme } from "emoji-picker-react";
|
|||||||
import styles from "./settings.module.scss";
|
import styles from "./settings.module.scss";
|
||||||
|
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
|
||||||
import { List, ListItem, Popover } from "./ui-lib";
|
import { List, ListItem, Popover } from "./ui-lib";
|
||||||
|
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import { SubmitKey, useChatStore, Theme } from "../store";
|
import { SubmitKey, useChatStore, Theme } from "../store";
|
||||||
import { Avatar } from "./home";
|
import { Avatar } from "./home";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings(props: { closeSettings: () => void }) {
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [config, updateConfig, resetConfig] = useChatStore((state) => [
|
const [config, updateConfig, resetConfig] = useChatStore((state) => [
|
||||||
state.config,
|
state.config,
|
||||||
@ -29,6 +29,14 @@ export function Settings() {
|
|||||||
<div className={styles["window-header-sub-title"]}>设置选项</div>
|
<div className={styles["window-header-sub-title"]}>设置选项</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-actions"]}>
|
<div className={styles["window-actions"]}>
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
onClick={props.closeSettings}
|
||||||
|
bordered
|
||||||
|
title="重置所有选项"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ResetIcon />}
|
icon={<ResetIcon />}
|
||||||
|
@ -12,9 +12,9 @@
|
|||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
white-space: nowrap;
|
||||||
-webkit-line-clamp: 2;
|
display: block;
|
||||||
-webkit-box-orient: vertical;
|
max-width: 50vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window-header-sub-title {
|
.window-header-sub-title {
|
||||||
|
@ -46,6 +46,17 @@
|
|||||||
--window-height: 90vh;
|
--window-height: 90vh;
|
||||||
--sidebar-width: 300px;
|
--sidebar-width: 300px;
|
||||||
--window-content-width: calc(var(--window-width) - var(--sidebar-width));
|
--window-content-width: calc(var(--window-width) - var(--sidebar-width));
|
||||||
|
--message-max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
:root {
|
||||||
|
--window-width: 100vw;
|
||||||
|
--window-height: 100vh;
|
||||||
|
--sidebar-width: 100vw;
|
||||||
|
--window-content-width: var(--window-width);
|
||||||
|
--message-max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
21
app/icons/close.svg
Normal file
21
app/icons/close.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
|
||||||
|
height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<defs>
|
||||||
|
<rect id="path_0" x="0" y="0" width="16" height="16" />
|
||||||
|
</defs>
|
||||||
|
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||||
|
<mask id="bg-mask-0" fill="white">
|
||||||
|
<use xlink:href="#path_0"></use>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#bg-mask-0)">
|
||||||
|
<path id="路径 1"
|
||||||
|
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||||
|
transform="translate(2.6666666666666665 2.6666666666666665) rotate(0 5.333333333333333 5.333333333333333)"
|
||||||
|
d="M0,0L10.67,10.67 " />
|
||||||
|
<path id="路径 2"
|
||||||
|
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||||
|
transform="translate(2.6666666666666665 2.6666666666666665) rotate(0 5.333333333333333 5.333333333333333)"
|
||||||
|
d="M0,10.67L10.67,0 " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 981 B |
25
app/icons/menu.svg
Normal file
25
app/icons/menu.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
|
||||||
|
height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<defs>
|
||||||
|
<rect id="path_0" x="0" y="0" width="16" height="16" />
|
||||||
|
</defs>
|
||||||
|
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||||
|
<mask id="bg-mask-0" fill="white">
|
||||||
|
<use xlink:href="#path_0"></use>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#bg-mask-0)">
|
||||||
|
<path id="路径 1"
|
||||||
|
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||||
|
transform="translate(2.649903333333333 3.983233333333333) rotate(0 5.333331666666666 0)"
|
||||||
|
d="M0,0L10.67,0 " />
|
||||||
|
<path id="路径 2"
|
||||||
|
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||||
|
transform="translate(2.649903333333333 7.983233333333333) rotate(0 5.333331666666666 0)"
|
||||||
|
d="M0,0L10.67,0 " />
|
||||||
|
<path id="路径 3"
|
||||||
|
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
|
||||||
|
transform="translate(2.649903333333333 11.983233333333333) rotate(0 5.333331666666666 0)"
|
||||||
|
d="M0,0L10.67,0 " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -12,7 +12,11 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="zh-Hans-CN">
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||||
|
/>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user