Merge pull request #659 from Yidadaa/bugfix-0409

fix: many UI bugs and resizable side bar
This commit is contained in:
Yifei Zhang 2023-04-10 01:04:38 +08:00 committed by GitHub
commit 601e72b56c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 292 additions and 61 deletions

View File

@ -49,4 +49,7 @@
.icon-button-text { .icon-button-text {
margin-left: 5px; margin-left: 5px;
font-size: 12px; font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }

View File

@ -96,7 +96,7 @@ export function ChatList() {
index={i} index={i}
selected={i === selectedIndex} selected={i === selectedIndex}
onClick={() => selectSession(i)} onClick={() => selectSession(i)}
onDelete={chatStore.deleteSession} onDelete={() => chatStore.deleteSession(i)}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}

View File

@ -3,7 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
import ExportIcon from "../icons/export.svg"; import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg"; import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg"; import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg"; import DownloadIcon from "../icons/download.svg";
@ -11,6 +11,8 @@ import LoadingIcon from "../icons/three-dots.svg";
import BotIcon from "../icons/bot.svg"; 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 MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import { import {
Message, Message,
@ -19,6 +21,7 @@ import {
BOT_HELLO, BOT_HELLO,
ROLES, ROLES,
createMessage, createMessage,
useAccessStore,
} from "../store"; } from "../store";
import { import {
@ -485,11 +488,17 @@ export function Chat(props: {
const context: RenderMessage[] = session.context.slice(); const context: RenderMessage[] = session.context.slice();
const accessStore = useAccessStore();
if ( if (
context.length === 0 && context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content session.messages.at(0)?.content !== BOT_HELLO.content
) { ) {
context.push(BOT_HELLO); const copiedHello = Object.assign({}, BOT_HELLO);
if (!accessStore.isAuthorized()) {
copiedHello.content = Locale.Error.Unauthorized;
}
context.push(copiedHello);
} }
// preview messages // preview messages
@ -584,6 +593,17 @@ export function Chat(props: {
}} }}
/> />
</div> </div>
<div className={styles["window-action-button"]}>
<IconButton
icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {
chatStore.updateConfig(
(config) => (config.tightBorder = !config.tightBorder),
);
}}
/>
</div>
</div> </div>
<PromptToast <PromptToast

View File

@ -10,7 +10,7 @@
background-color: var(--white); background-color: var(--white);
min-width: 600px; min-width: 600px;
min-height: 480px; min-height: 480px;
max-width: 900px; max-width: 1200px;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
@ -48,6 +48,27 @@
display: flex; display: flex;
flex-direction: column; 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);
position: relative;
transition: width ease 0.1s;
}
.sidebar-drag {
$width: 10px;
position: absolute;
top: 0;
right: 0;
height: 100%;
width: $width;
background-color: var(--black);
cursor: ew-resize;
opacity: 0;
transition: all ease 0.3s;
&:hover,
&:active {
opacity: 0.2;
}
} }
.window-content { .window-content {
@ -177,10 +198,11 @@
margin-top: 8px; margin-top: 8px;
} }
.chat-item-count { .chat-item-count,
}
.chat-item-date { .chat-item-date {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.sidebar-tail { .sidebar-tail {
@ -436,6 +458,7 @@
.export-content { .export-content {
white-space: break-spaces; white-space: break-spaces;
padding: 10px !important;
} }
.loading-content { .loading-content {

View File

@ -2,7 +2,13 @@
require("../polyfill"); require("../polyfill");
import { useState, useEffect } from "react"; import {
useState,
useEffect,
useRef,
useCallback,
MouseEventHandler,
} from "react";
import { IconButton } from "./button"; import { IconButton } from "./button";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
@ -24,6 +30,7 @@ import { Chat } from "./chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { REPO_URL } from "../constant"; import { REPO_URL } from "../constant";
import { ErrorBoundary } from "./error"; import { ErrorBoundary } from "./error";
import { useDebounce } from "use-debounce";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
@ -75,6 +82,49 @@ function useSwitchTheme() {
}, [config.theme]); }, [config.theme]);
} }
function useDragSideBar() {
const limit = (x: number) => Math.min(500, Math.max(220, x));
const chatStore = useChatStore();
const startX = useRef(0);
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 100) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = chatStore.config.sidebarWidth ?? 300;
window.removeEventListener("mousemove", handleMouseMove.current);
window.removeEventListener("mouseup", handleMouseUp.current);
});
const onDragMouseDown = (e: MouseEvent) => {
startX.current = e.clientX;
window.addEventListener("mousemove", handleMouseMove.current);
window.addEventListener("mouseup", handleMouseUp.current);
};
useEffect(() => {
document.documentElement.style.setProperty(
"--sidebar-width",
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
);
}, [chatStore.config.sidebarWidth]);
return {
onDragMouseDown,
};
}
const useHasHydrated = () => { const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false); const [hasHydrated, setHasHydrated] = useState<boolean>(false);
@ -101,6 +151,9 @@ function _Home() {
const [openSettings, setOpenSettings] = useState(false); const [openSettings, setOpenSettings] = useState(false);
const config = useChatStore((state) => state.config); const config = useChatStore((state) => state.config);
// drag side bar
const { onDragMouseDown } = useDragSideBar();
useSwitchTheme(); useSwitchTheme();
if (loading) { if (loading) {
@ -174,6 +227,11 @@ function _Home() {
/> />
</div> </div>
</div> </div>
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
></div>
</div> </div>
<div className={styles["window-content"]}> <div className={styles["window-content"]}>

View File

@ -19,11 +19,16 @@
cursor: pointer; cursor: pointer;
} }
.password-input { .password-input-container {
max-width: 50%;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
.password-eye { .password-eye {
margin-right: 4px; margin-right: 4px;
} }
.password-input {
min-width: 80%;
}
} }

View File

@ -60,13 +60,17 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
} }
return ( return (
<div className={styles["password-input"]}> <div className={styles["password-input-container"]}>
<IconButton <IconButton
icon={visible ? <EyeIcon /> : <EyeOffIcon />} icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility} onClick={changeVisibility}
className={styles["password-eye"]} className={styles["password-eye"]}
/> />
<input {...props} type={visible ? "text" : "password"} /> <input
{...props}
type={visible ? "text" : "password"}
className={styles["password-input"]}
/>
</div> </div>
); );
} }
@ -120,8 +124,7 @@ export function Settings(props: { closeSettings: () => void }) {
const builtinCount = SearchService.count.builtin; const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0; const customCount = promptStore.prompts.size ?? 0;
const showUsage = !!accessStore.token || !!accessStore.accessCode; const showUsage = accessStore.isAuthorized();
useEffect(() => { useEffect(() => {
checkUpdate(); checkUpdate();
showUsage && checkUsage(); showUsage && checkUsage();
@ -342,37 +345,7 @@ export function Settings(props: { closeSettings: () => void }) {
></input> ></input>
</SettingItem> </SettingItem>
</List> </List>
<List>
<SettingItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
/>
</SettingItem>
</List>
<List> <List>
{enabledAccessControl ? ( {enabledAccessControl ? (
<SettingItem <SettingItem
@ -469,6 +442,38 @@ export function Settings(props: { closeSettings: () => void }) {
</SettingItem> </SettingItem>
</List> </List>
<List>
<SettingItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
/>
</SettingItem>
</List>
<List> <List>
<SettingItem title={Locale.Settings.Model}> <SettingItem title={Locale.Settings.Model}>
<select <select

View File

@ -127,6 +127,7 @@
width: 100vw; width: 100vw;
display: flex; display: flex;
justify-content: center; justify-content: center;
pointer-events: none;
.toast-content { .toast-content {
max-width: 80vw; max-width: 80vw;
@ -141,6 +142,7 @@
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
pointer-events: all;
.toast-action { .toast-action {
padding-left: 20px; padding-left: 20px;

41
app/icons/max.svg Normal file
View File

@ -0,0 +1,41 @@
<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 2) rotate(0 1.6666666666666665 1.6499166666666665)"
d="M0,0L3.33,3.3 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
d="M0,3.3L3.33,0 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
d="M3.3,3.3L0,0 " />
<path id="路径 4"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
d="M3.3,0L0,3.3 " />
<path id="路径 5"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(11 2) rotate(0 1.5 1.5)" d="M0,0L3,0L3,3 " />
<path id="路径 6"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(11 11) rotate(0 1.5 1.5)" d="M3,0L3,3L0,3 " />
<path id="路径 7"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 11) rotate(0 1.5 1.5)" d="M3,3L0,3L0,0 " />
<path id="路径 8"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2) rotate(0 1.5 1.5)" d="M0,3L0,0L3,0 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

45
app/icons/min.svg Normal file
View File

@ -0,0 +1,45 @@
<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 2) rotate(0 1.6666666666666665 1.6499166666666665)"
d="M0,0L3.33,3.3 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
d="M0,3.3L3.33,0 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
d="M3.3,3.3L0,0 " />
<path id="路径 4"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
d="M3.3,0L0,3.3 " />
<path id="路径 5"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2.333333333333333) rotate(0 1.5 1.5)"
d="M0,0L0,3L3,3 " />
<path id="路径 6"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2.333333333333333 2.333333333333333) rotate(0 1.5 1.5)"
d="M3,0L3,3L0,3 " />
<path id="路径 7"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2.333333333333333 10.666666666666666) rotate(0 1.5 1.5)"
d="M3,3L3,0L0,0 " />
<path id="路径 8"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 10.666666666666666) rotate(0 1.4832500000000004 1.5)"
d="M0,3L0,0L2.97,0 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

17
app/icons/share.svg Normal file
View File

@ -0,0 +1,17 @@
<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 1.3333333333333333) rotate(0 6.333333333333333 6.5)"
d="M6.67,3.67C1.67,3.67 0,7.33 0,13C0,13 2,8 6.67,8L6.67,11.67L12.67,6L6.67,0L6.67,3.67Z " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 740 B

View File

@ -3,7 +3,7 @@ import { SubmitKey } from "../store/app";
const cn = { const cn = {
WIP: "该功能仍在开发中……", WIP: "该功能仍在开发中……",
Error: { Error: {
Unauthorized: "现在是未授权状态,请在设置页输入访问密码。", Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`, ChatItemCount: (count: number) => `${count} 条对话`,
@ -90,7 +90,7 @@ const cn = {
}, },
SendKey: "发送键", SendKey: "发送键",
Theme: "主题", Theme: "主题",
TightBorder: "紧凑边框", TightBorder: "无边框模式",
SendPreviewBubble: "发送预览气泡", SendPreviewBubble: "发送预览气泡",
Prompt: { Prompt: {
Disable: { Disable: {

View File

@ -9,6 +9,7 @@ export interface AccessControlStore {
updateToken: (_: string) => void; updateToken: (_: string) => void;
updateCode: (_: string) => void; updateCode: (_: string) => void;
enabledAccessControl: () => boolean; enabledAccessControl: () => boolean;
isAuthorized: () => boolean;
} }
export const ACCESS_KEY = "access-control"; export const ACCESS_KEY = "access-control";
@ -27,10 +28,13 @@ export const useAccessStore = create<AccessControlStore>()(
updateToken(token: string) { updateToken(token: string) {
set((state) => ({ token })); set((state) => ({ token }));
}, },
isAuthorized() {
return !!get().token || !!get().accessCode;
},
}), }),
{ {
name: ACCESS_KEY, name: ACCESS_KEY,
version: 1, version: 1,
} },
) ),
); );

View File

@ -53,6 +53,7 @@ export interface ChatConfig {
theme: Theme; theme: Theme;
tightBorder: boolean; tightBorder: boolean;
sendPreviewBubble: boolean; sendPreviewBubble: boolean;
sidebarWidth: number;
disablePromptHint: boolean; disablePromptHint: boolean;
@ -141,6 +142,7 @@ const DEFAULT_CONFIG: ChatConfig = {
theme: Theme.Auto as Theme, theme: Theme.Auto as Theme,
tightBorder: false, tightBorder: false,
sendPreviewBubble: true, sendPreviewBubble: true,
sidebarWidth: 300,
disablePromptHint: false, disablePromptHint: false,
@ -205,7 +207,7 @@ interface ChatStore {
moveSession: (from: number, to: number) => void; moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void; selectSession: (index: number) => void;
newSession: () => void; newSession: () => void;
deleteSession: () => void; deleteSession: (index?: number) => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
onNewMessage: (message: Message) => void; onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
@ -326,24 +328,30 @@ export const useChatStore = create<ChatStore>()(
})); }));
}, },
deleteSession() { deleteSession(i?: number) {
const deletedSession = get().currentSession(); const deletedSession = get().currentSession();
const index = get().currentSessionIndex; const index = i ?? get().currentSessionIndex;
const isLastSession = get().sessions.length === 1; const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) { if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index); get().removeSession(index);
showToast(Locale.Home.DeleteToast, { showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert, text: Locale.Home.Revert,
onClick() { onClick() {
set((state) => ({ set((state) => ({
sessions: state.sessions sessions: state.sessions
.slice(0, index) .slice(0, index)
.concat([deletedSession]) .concat([deletedSession])
.concat(state.sessions.slice(index + Number(isLastSession))), .concat(
state.sessions.slice(index + Number(isLastSession)),
),
})); }));
}, },
}); },
5000,
);
} }
}, },