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 {
margin-left: 5px;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

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

View File

@ -3,7 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.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 CopyIcon from "../icons/copy.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 AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import {
Message,
@ -19,6 +21,7 @@ import {
BOT_HELLO,
ROLES,
createMessage,
useAccessStore,
} from "../store";
import {
@ -485,11 +488,17 @@ export function Chat(props: {
const context: RenderMessage[] = session.context.slice();
const accessStore = useAccessStore();
if (
context.length === 0 &&
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
@ -584,6 +593,17 @@ export function Chat(props: {
}}
/>
</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>
<PromptToast

View File

@ -10,7 +10,7 @@
background-color: var(--white);
min-width: 600px;
min-height: 480px;
max-width: 900px;
max-width: 1200px;
display: flex;
overflow: hidden;
@ -48,6 +48,27 @@
display: flex;
flex-direction: column;
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 {
@ -177,10 +198,11 @@
margin-top: 8px;
}
.chat-item-count {
}
.chat-item-count,
.chat-item-date {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-tail {
@ -436,6 +458,7 @@
.export-content {
white-space: break-spaces;
padding: 10px !important;
}
.loading-content {

View File

@ -2,7 +2,13 @@
require("../polyfill");
import { useState, useEffect } from "react";
import {
useState,
useEffect,
useRef,
useCallback,
MouseEventHandler,
} from "react";
import { IconButton } from "./button";
import styles from "./home.module.scss";
@ -24,6 +30,7 @@ import { Chat } from "./chat";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { ErrorBoundary } from "./error";
import { useDebounce } from "use-debounce";
export function Loading(props: { noLogo?: boolean }) {
return (
@ -75,6 +82,49 @@ function useSwitchTheme() {
}, [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 [hasHydrated, setHasHydrated] = useState<boolean>(false);
@ -101,6 +151,9 @@ function _Home() {
const [openSettings, setOpenSettings] = useState(false);
const config = useChatStore((state) => state.config);
// drag side bar
const { onDragMouseDown } = useDragSideBar();
useSwitchTheme();
if (loading) {
@ -174,6 +227,11 @@ function _Home() {
/>
</div>
</div>
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
></div>
</div>
<div className={styles["window-content"]}>

View File

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

View File

@ -60,13 +60,17 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
}
return (
<div className={styles["password-input"]}>
<div className={styles["password-input-container"]}>
<IconButton
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={styles["password-eye"]}
/>
<input {...props} type={visible ? "text" : "password"} />
<input
{...props}
type={visible ? "text" : "password"}
className={styles["password-input"]}
/>
</div>
);
}
@ -120,8 +124,7 @@ export function Settings(props: { closeSettings: () => void }) {
const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0;
const showUsage = !!accessStore.token || !!accessStore.accessCode;
const showUsage = accessStore.isAuthorized();
useEffect(() => {
checkUpdate();
showUsage && checkUsage();
@ -342,37 +345,7 @@ export function Settings(props: { closeSettings: () => void }) {
></input>
</SettingItem>
</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>
{enabledAccessControl ? (
<SettingItem
@ -469,6 +442,38 @@ export function Settings(props: { closeSettings: () => void }) {
</SettingItem>
</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>
<SettingItem title={Locale.Settings.Model}>
<select

View File

@ -127,6 +127,7 @@
width: 100vw;
display: flex;
justify-content: center;
pointer-events: none;
.toast-content {
max-width: 80vw;
@ -141,6 +142,7 @@
margin-bottom: 20px;
display: flex;
align-items: center;
pointer-events: all;
.toast-action {
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 = {
WIP: "该功能仍在开发中……",
Error: {
Unauthorized: "现在是未授权状态,请在设置页输入访问密码。",
Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
@ -90,7 +90,7 @@ const cn = {
},
SendKey: "发送键",
Theme: "主题",
TightBorder: "紧凑边框",
TightBorder: "无边框模式",
SendPreviewBubble: "发送预览气泡",
Prompt: {
Disable: {

View File

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

View File

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