This commit is contained in:
GH Action - Upstream Sync
2023-04-06 00:51:42 +00:00
37 changed files with 833 additions and 245 deletions

View File

@@ -1,14 +1,13 @@
import { useState, useRef, useEffect, useLayoutEffect } from "react";
import DeleteIcon from "../icons/delete.svg";
import styles from "./home.module.scss";
import {
Message,
SubmitKey,
useChatStore,
ChatSession,
BOT_HELLO,
} from "../store";
DragDropContext,
Droppable,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { useChatStore } from "../store";
import Locale from "../locales";
import { isMobileScreen } from "../utils";
@@ -20,54 +19,92 @@ export function ChatItem(props: {
count: number;
time: string;
selected: boolean;
id: number;
index: number;
}) {
return (
<div
className={`${styles["chat-item"]} ${
props.selected && styles["chat-item-selected"]
}`}
onClick={props.onClick}
>
<div className={styles["chat-item-title"]}>{props.title}</div>
<div className={styles["chat-item-info"]}>
<div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)}
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`${styles["chat-item"]} ${
props.selected && styles["chat-item-selected"]
}`}
onClick={props.onClick}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={styles["chat-item-title"]}>{props.title}</div>
<div className={styles["chat-item-info"]}>
<div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)}
</div>
<div className={styles["chat-item-date"]}>{props.time}</div>
</div>
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
<DeleteIcon />
</div>
</div>
<div className={styles["chat-item-date"]}>{props.time}</div>
</div>
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
<DeleteIcon />
</div>
</div>
)}
</Draggable>
);
}
export function ChatList() {
const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
(state) => [
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
useChatStore((state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.removeSession,
],
);
state.moveSession,
]);
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
moveSession(source.index, destination.index);
};
return (
<div className={styles["chat-list"]}>
{sessions.map((item, i) => (
<ChatItem
title={item.topic}
time={item.lastUpdate}
count={item.messages.length}
key={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={() =>
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
removeSession(i)
}
/>
))}
</div>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chat-list">
{(provided) => (
<div
className={styles["chat-list"]}
ref={provided.innerRef}
{...provided.droppableProps}
>
{sessions.map((item, i) => (
<ChatItem
title={item.topic}
time={item.lastUpdate}
count={item.messages.length}
key={item.id}
id={item.id}
index={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={() =>
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
removeSession(i)
}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}

View File

@@ -63,6 +63,14 @@
font-size: 12px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.memory-prompt-action {
display: flex;
align-items: center;
}
}
.memory-prompt-content {

View File

@@ -12,7 +12,14 @@ import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import { Message, SubmitKey, useChatStore, BOT_HELLO, ROLES } from "../store";
import {
Message,
SubmitKey,
useChatStore,
BOT_HELLO,
ROLES,
createMessage,
} from "../store";
import {
copyToClipboard,
@@ -32,11 +39,14 @@ import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
import { Modal, showModal, showToast } from "./ui-lib";
import { Input, Modal, showModal, showToast } from "./ui-lib";
const Markdown = dynamic(async () => memo((await import("./markdown")).Markdown), {
loading: () => <LoadingIcon />,
});
const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown),
{
loading: () => <LoadingIcon />,
},
);
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
loading: () => <LoadingIcon />,
@@ -141,6 +151,16 @@ function PromptToast(props: {
title={Locale.Context.Edit}
onClose={() => props.setShowModal(false)}
actions={[
<IconButton
key="reset"
icon={<CopyIcon />}
bordered
text={Locale.Memory.Reset}
onClick={() =>
confirm(Locale.Memory.ResetConfirm) &&
chatStore.resetSession()
}
/>,
<IconButton
key="copy"
icon={<CopyIcon />}
@@ -151,7 +171,6 @@ function PromptToast(props: {
]}
>
<>
{" "}
<div className={chatStyle["context-prompt"]}>
{context.map((c, i) => (
<div className={chatStyle["context-prompt-row"]} key={i}>
@@ -171,17 +190,18 @@ function PromptToast(props: {
</option>
))}
</select>
<input
<Input
value={c.content}
type="text"
className={chatStyle["context-content"]}
onChange={(e) =>
rows={1}
onInput={(e) =>
updateContextPrompt(i, {
...c,
content: e.target.value as any,
content: e.currentTarget.value as any,
})
}
></input>
/>
<IconButton
icon={<DeleteIcon />}
className={chatStyle["context-delete-button"]}
@@ -209,8 +229,24 @@ function PromptToast(props: {
</div>
<div className={chatStyle["memory-prompt"]}>
<div className={chatStyle["memory-prompt-title"]}>
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
{session.messages.length})
<span>
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
{session.messages.length})
</span>
<label className={chatStyle["memory-prompt-action"]}>
{Locale.Memory.Send}
<input
type="checkbox"
checked={session.sendMemory}
onChange={() =>
chatStore.updateCurrentSession(
(session) =>
(session.sendMemory = !session.sendMemory),
)
}
></input>
</label>
</div>
<div className={chatStyle["memory-prompt-content"]}>
{session.memoryPrompt || Locale.Memory.EmptyContent}
@@ -378,8 +414,8 @@ export function Chat(props: {
};
// stop response
const onUserStop = (messageIndex: number) => {
ControllerPool.stop(sessionIndex, messageIndex);
const onUserStop = (messageId: number) => {
ControllerPool.stop(sessionIndex, messageId);
};
// check if should send message
@@ -409,6 +445,9 @@ export function Chat(props: {
chatStore
.onUserInput(messages[i].content)
.then(() => setIsLoading(false));
chatStore.updateCurrentSession((session) =>
session.messages.splice(i, 2),
);
inputRef.current?.focus();
return;
}
@@ -433,9 +472,10 @@ export function Chat(props: {
isLoading
? [
{
role: "assistant",
content: "……",
date: new Date().toLocaleString(),
...createMessage({
role: "assistant",
content: "……",
}),
preview: true,
},
]
@@ -445,9 +485,10 @@ export function Chat(props: {
userInput.length > 0 && config.sendPreviewBubble
? [
{
role: "user",
content: userInput,
date: new Date().toLocaleString(),
...createMessage({
role: "user",
content: userInput,
}),
preview: true,
},
]
@@ -460,6 +501,7 @@ export function Chat(props: {
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
inputRef.current?.focus();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
@@ -563,7 +605,7 @@ export function Chat(props: {
{message.streaming ? (
<div
className={styles["chat-message-top-action"]}
onClick={() => onUserStop(i)}
onClick={() => onUserStop(message.id ?? i)}
>
{Locale.Chat.Actions.Stop}
</div>

View File

@@ -125,7 +125,7 @@
border-radius: 10px;
margin-bottom: 10px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
transition: background-color 0.3s ease;
cursor: pointer;
user-select: none;
border: 2px solid transparent;

View File

@@ -19,7 +19,6 @@ import CloseIcon from "../icons/close.svg";
import { useChatStore } from "../store";
import { isMobileScreen } from "../utils";
import Locale from "../locales";
import { ChatList } from "./chat-list";
import { Chat } from "./chat";
import dynamic from "next/dynamic";
@@ -39,6 +38,10 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />,
});
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => <Loading noLogo />,
});
function useSwitchTheme() {
const config = useChatStore((state) => state.config);

View File

@@ -96,26 +96,18 @@ export function Settings(props: { closeSettings: () => void }) {
const [usage, setUsage] = useState<{
used?: number;
subscription?: number;
}>();
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
setLoadingUsage(true);
requestUsage()
.then((res) =>
setUsage({
used: res,
}),
)
.then((res) => setUsage(res))
.finally(() => {
setLoadingUsage(false);
});
}
useEffect(() => {
checkUpdate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const accessStore = useAccessStore();
const enabledAccessControl = useMemo(
() => accessStore.enabledAccessControl(),
@@ -127,12 +119,13 @@ export function Settings(props: { closeSettings: () => void }) {
const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0;
const showUsage = accessStore.token !== "";
const showUsage = !!accessStore.token || !!accessStore.accessCode;
useEffect(() => {
if (showUsage) {
checkUsage();
}
}, [showUsage]);
checkUpdate();
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ErrorBoundary>
@@ -392,7 +385,10 @@ export function Settings(props: { closeSettings: () => void }) {
showUsage
? loadingUsage
? Locale.Settings.Usage.IsChecking
: Locale.Settings.Usage.SubTitle(usage?.used ?? "[?]")
: Locale.Settings.Usage.SubTitle(
usage?.used ?? "[?]",
usage?.subscription ?? "[?]",
)
: Locale.Settings.Usage.NoAccess
}
>

View File

@@ -141,6 +141,16 @@
}
}
.input {
border: var(--border-in-light);
border-radius: 10px;
padding: 10px;
font-family: inherit;
background-color: var(--white);
color: var(--black);
resize: none;
}
@media only screen and (max-width: 600px) {
.modal-container {
width: 90vw;

View File

@@ -2,6 +2,7 @@ import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import { createRoot } from "react-dom/client";
import React from "react";
export function Popover(props: {
children: JSX.Element;
@@ -140,3 +141,17 @@ export function showToast(content: string, delay = 3000) {
root.render(<Toast content={content} />);
}
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
autoHeight?: boolean;
rows?: number;
};
export function Input(props: InputProps) {
return (
<textarea
{...props}
className={`${styles["input"]} ${props.className}`}
></textarea>
);
}