mirror of https://github.com/usememos/memos
parent
69225b507b
commit
32e2f1d339
@ -1,243 +0,0 @@
|
|||||||
import { Button, FormControl, Input, Modal, ModalClose, ModalDialog, Stack, Textarea, Typography } from "@mui/joy";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import * as api from "@/helpers/api";
|
|
||||||
import useLoading from "@/hooks/useLoading";
|
|
||||||
import { marked } from "@/labs/marked";
|
|
||||||
import { useMessageStore } from "@/store/zustand/message";
|
|
||||||
import { defaultMessageGroup, MessageGroup, useMessageGroupStore } from "@/store/zustand/message-group";
|
|
||||||
import Icon from "./Icon";
|
|
||||||
import { generateDialog } from "./Dialog";
|
|
||||||
import showSettingDialog from "./SettingDialog";
|
|
||||||
import Selector from "./kit/Selector";
|
|
||||||
|
|
||||||
type Props = DialogProps;
|
|
||||||
|
|
||||||
const AskAIDialog: React.FC<Props> = (props: Props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { destroy, hide } = props;
|
|
||||||
const fetchingState = useLoading(false);
|
|
||||||
const [messageGroup, setMessageGroup] = useState<MessageGroup>(defaultMessageGroup);
|
|
||||||
const messageStore = useMessageStore(messageGroup)();
|
|
||||||
const [isEnabled, setIsEnabled] = useState<boolean>(true);
|
|
||||||
const [isInIME, setIsInIME] = useState(false);
|
|
||||||
const [question, setQuestion] = useState<string>("");
|
|
||||||
const messageList = messageStore.messageList;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api.checkOpenAIEnabled().then(({ data }) => {
|
|
||||||
const { data: enabled } = data;
|
|
||||||
setIsEnabled(enabled);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleGotoSystemSetting = () => {
|
|
||||||
showSettingDialog("system");
|
|
||||||
destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuestionTextareaChange = async (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setQuestion(event.currentTarget.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
||||||
if (event.key === "Enter" && !event.shiftKey && !isInIME) {
|
|
||||||
event.preventDefault();
|
|
||||||
handleSendQuestionButtonClick().then();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendQuestionButtonClick = async () => {
|
|
||||||
if (!question) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchingState.setLoading();
|
|
||||||
setQuestion("");
|
|
||||||
messageStore.addMessage({
|
|
||||||
role: "user",
|
|
||||||
content: question,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await fetchChatCompletion();
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error(error.response.data.error);
|
|
||||||
}
|
|
||||||
fetchingState.setFinish();
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchChatCompletion = async () => {
|
|
||||||
const messageList = messageStore.getState().messageList;
|
|
||||||
const {
|
|
||||||
data: { data: answer },
|
|
||||||
} = await api.postChatCompletion(messageList);
|
|
||||||
messageStore.addMessage({
|
|
||||||
role: "assistant",
|
|
||||||
content: answer.replace(/^\n\n/, ""),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMessageGroupSelect = (value: string) => {
|
|
||||||
const messageGroup = messageGroupList.find((group) => group.messageStorageId === value);
|
|
||||||
if (messageGroup) {
|
|
||||||
setMessageGroup(messageGroup);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [isAddMessageGroupDialogOpen, setIsAddMessageGroupDialogOpen] = useState<boolean>(false);
|
|
||||||
const [groupName, setGroupName] = useState<string>("");
|
|
||||||
|
|
||||||
const messageGroupStore = useMessageGroupStore();
|
|
||||||
const messageGroupList = messageGroupStore.groupList;
|
|
||||||
|
|
||||||
const handleOpenDialog = () => {
|
|
||||||
setIsAddMessageGroupDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveDialog = () => {
|
|
||||||
setMessageGroup(messageGroupStore.removeGroup(messageGroup));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseDialog = () => {
|
|
||||||
setIsAddMessageGroupDialogOpen(false);
|
|
||||||
setGroupName("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddMessageGroupDlgConfirm = () => {
|
|
||||||
const newMessageGroup: MessageGroup = {
|
|
||||||
name: groupName,
|
|
||||||
messageStorageId: "message-storage-" + groupName,
|
|
||||||
};
|
|
||||||
messageGroupStore.addGroup(newMessageGroup);
|
|
||||||
setMessageGroup(newMessageGroup);
|
|
||||||
handleCloseDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
handleCloseDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="dialog-header-container">
|
|
||||||
<p className="title-text flex flex-row items-center">
|
|
||||||
<Icon.Bot className="mr-1 w-5 h-auto opacity-80" />
|
|
||||||
<span className="mr-4">{t("ask-ai.title")}</span>
|
|
||||||
<span className="flex flex-row justify-start items-center">
|
|
||||||
<Selector
|
|
||||||
className="w-32"
|
|
||||||
dataSource={messageGroupList.map((item) => ({ text: item.name, value: item.messageStorageId }))}
|
|
||||||
value={messageGroup.messageStorageId}
|
|
||||||
handleValueChanged={handleMessageGroupSelect}
|
|
||||||
/>
|
|
||||||
<button className="btn-text px-1 ml-1" onClick={handleOpenDialog}>
|
|
||||||
<Icon.Plus className="w-4 h-auto" />
|
|
||||||
</button>
|
|
||||||
<button className="btn-text px-1" onClick={handleRemoveDialog}>
|
|
||||||
<Icon.Trash2 className="w-4 h-auto" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Modal open={isAddMessageGroupDialogOpen} onClose={handleCloseDialog}>
|
|
||||||
<ModalDialog aria-labelledby="basic-modal-dialog-title" sx={{ maxWidth: 500 }}>
|
|
||||||
<ModalClose />
|
|
||||||
<Typography id="basic-modal-dialog-title" component="h2">
|
|
||||||
{t("ask-ai.create-message-group-title")}
|
|
||||||
</Typography>
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
value={groupName}
|
|
||||||
onChange={(e) => setGroupName(e.target.value)}
|
|
||||||
placeholder={t("ask-ai.label-message-group-name-title")}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="w-full flex justify-end gap-x-2">
|
|
||||||
<Button variant="plain" onClick={handleCancel}>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleAddMessageGroupDlgConfirm}>{t("common.confirm")}</Button>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
<button className="btn close-btn" onClick={() => hide()}>
|
|
||||||
<Icon.X />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="dialog-content-container !w-112 max-w-full">
|
|
||||||
<Stack spacing={2} style={{ width: "100%" }}>
|
|
||||||
{messageList.map((message, index) => (
|
|
||||||
<div key={index} className="w-full flex flex-col justify-start items-start space-y-2">
|
|
||||||
{message.role === "user" ? (
|
|
||||||
<div className="w-full flex flex-row justify-end items-start pl-6">
|
|
||||||
<span className="word-break shadow rounded-lg rounded-tr-none px-3 py-2 opacity-80 bg-gray-100 dark:bg-zinc-700">
|
|
||||||
{message.content}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex flex-row justify-start items-start pr-8 space-x-2">
|
|
||||||
<Icon.Bot className="mt-2 shrink-0 mr-1 w-6 h-auto opacity-80" />
|
|
||||||
<div className="memo-content-wrapper !w-auto flex flex-col justify-start items-start shadow rounded-lg rounded-tl-none px-3 py-2 bg-gray-100 dark:bg-zinc-700">
|
|
||||||
<div className="memo-content-text">{marked(message.content)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
{fetchingState.isLoading && (
|
|
||||||
<p className="w-full py-2 mt-4 flex flex-row justify-center items-center">
|
|
||||||
<Icon.Loader className="w-5 h-auto animate-spin" />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{!isEnabled && (
|
|
||||||
<div className="w-full flex flex-col justify-center items-center mt-4 space-y-2">
|
|
||||||
<p>{t("ask-ai.not_enabled")}</p>
|
|
||||||
<Button onClick={() => handleGotoSystemSetting()}>{t("ask-ai.go-to-settings")}</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="w-full relative mt-4">
|
|
||||||
<Textarea
|
|
||||||
className="w-full"
|
|
||||||
placeholder={t("ask-ai.placeholder")}
|
|
||||||
value={question}
|
|
||||||
minRows={1}
|
|
||||||
maxRows={5}
|
|
||||||
onChange={handleQuestionTextareaChange}
|
|
||||||
onCompositionStart={() => setIsInIME(true)}
|
|
||||||
onCompositionEnd={() => setIsInIME(false)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
<Icon.Send
|
|
||||||
className="cursor-pointer w-7 p-1 h-auto rounded-md bg-gray-100 dark:bg-zinc-800 absolute right-2 bottom-1.5 shadow hover:opacity-80"
|
|
||||||
onClick={handleSendQuestionButtonClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function showAskAIDialog() {
|
|
||||||
const dialogname = "ask-ai-dialog";
|
|
||||||
const dialogElement = document.body.querySelector(`div.${dialogname}`);
|
|
||||||
if (dialogElement) {
|
|
||||||
dialogElement.classList.remove("showoff");
|
|
||||||
dialogElement.classList.add("showup");
|
|
||||||
document.body.classList.add("overflow-hidden");
|
|
||||||
} else {
|
|
||||||
generateDialog(
|
|
||||||
{
|
|
||||||
className: dialogname,
|
|
||||||
dialogName: dialogname,
|
|
||||||
},
|
|
||||||
AskAIDialog
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default showAskAIDialog;
|
|
@ -1,144 +0,0 @@
|
|||||||
import { Option, Select } from "@mui/joy";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useUserStore } from "@/store/module";
|
|
||||||
import Icon from "./Icon";
|
|
||||||
import { generateDialog } from "./Dialog";
|
|
||||||
import BetaBadge from "./BetaBadge";
|
|
||||||
import MyAccountSection from "./Settings/MyAccountSection";
|
|
||||||
import PreferencesSection from "./Settings/PreferencesSection";
|
|
||||||
import MemberSection from "./Settings/MemberSection";
|
|
||||||
import SystemSection from "./Settings/SystemSection";
|
|
||||||
import StorageSection from "./Settings/StorageSection";
|
|
||||||
import SSOSection from "./Settings/SSOSection";
|
|
||||||
import "@/less/setting-dialog.less";
|
|
||||||
|
|
||||||
type SettingSection = "my-account" | "preference" | "member" | "system" | "storage" | "sso";
|
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
|
||||||
selectedSection?: SettingSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
selectedSection: SettingSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingDialog: React.FC<Props> = (props: Props) => {
|
|
||||||
const { destroy, selectedSection } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const user = userStore.state.user;
|
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
selectedSection: selectedSection || "my-account",
|
|
||||||
});
|
|
||||||
const isHost = user?.role === "HOST";
|
|
||||||
|
|
||||||
const handleSectionSelectorItemClick = (settingSection: SettingSection) => {
|
|
||||||
setState({
|
|
||||||
selectedSection: settingSection,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSettingSectionList = () => {
|
|
||||||
let settingList: SettingSection[] = ["my-account", "preference"];
|
|
||||||
if (isHost) {
|
|
||||||
settingList = settingList.concat(["member", "system", "storage", "sso"]);
|
|
||||||
}
|
|
||||||
return settingList;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="dialog-content-container">
|
|
||||||
<button className="btn close-btn" onClick={destroy}>
|
|
||||||
<Icon.X className="icon-img" />
|
|
||||||
</button>
|
|
||||||
<div className="section-selector-container">
|
|
||||||
<span className="section-title">{t("common.basic")}</span>
|
|
||||||
<div className="section-items-container">
|
|
||||||
<span
|
|
||||||
onClick={() => handleSectionSelectorItemClick("my-account")}
|
|
||||||
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
|
|
||||||
>
|
|
||||||
<Icon.User className="w-4 h-auto mr-2 opacity-80" /> {t("setting.my-account")}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
onClick={() => handleSectionSelectorItemClick("preference")}
|
|
||||||
className={`section-item ${state.selectedSection === "preference" ? "selected" : ""}`}
|
|
||||||
>
|
|
||||||
<Icon.Cog className="w-4 h-auto mr-2 opacity-80" /> {t("setting.preference")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{isHost ? (
|
|
||||||
<>
|
|
||||||
<span className="section-title">{t("common.admin")}</span>
|
|
||||||
<div className="section-items-container">
|
|
||||||
<span
|
|
||||||
onClick={() => handleSectionSelectorItemClick("member")}
|
|
||||||
className={`section-item ${state.selectedSection === "member" ? "selected" : ""}`}
|
|
||||||
>
|
|
||||||
<Icon.Users className="w-4 h-auto mr-2 opacity-80" /> {t("setting.member")}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
onClick={() => handleSectionSelectorItemClick("system")}
|
|
||||||
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
|
|
||||||
>
|
|
||||||
<Icon.Settings2 className="w-4 h-auto mr-2 opacity-80" /> {t("setting.system")}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
onClick={() => handleSectionSelectorItemClick("storage")}
|
|
||||||
className={`section-item ${state.selectedSection === "storage" ? "selected" : ""}`}
|
|
||||||
>
|
|
||||||
<Icon.Database className="w-4 h-auto mr-2 opacity-80" /> {t("setting.storage")} <BetaBadge />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
onClick={() => handleSectionSelectorItemClick("sso")}
|
|
||||||
className={`section-item ${state.selectedSection === "sso" ? "selected" : ""}`}
|
|
||||||
>
|
|
||||||
<Icon.Key className="w-4 h-auto mr-2 opacity-80" /> {t("setting.sso")} <BetaBadge />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="section-content-container">
|
|
||||||
<Select
|
|
||||||
className="block sm:!hidden"
|
|
||||||
value={state.selectedSection}
|
|
||||||
onChange={(_, value) => handleSectionSelectorItemClick(value as SettingSection)}
|
|
||||||
>
|
|
||||||
{getSettingSectionList().map((settingSection) => (
|
|
||||||
<Option key={settingSection} value={settingSection}>
|
|
||||||
{t(`setting.${settingSection}`)}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{state.selectedSection === "my-account" ? (
|
|
||||||
<MyAccountSection />
|
|
||||||
) : state.selectedSection === "preference" ? (
|
|
||||||
<PreferencesSection />
|
|
||||||
) : state.selectedSection === "member" ? (
|
|
||||||
<MemberSection />
|
|
||||||
) : state.selectedSection === "system" ? (
|
|
||||||
<SystemSection />
|
|
||||||
) : state.selectedSection === "storage" ? (
|
|
||||||
<StorageSection />
|
|
||||||
) : state.selectedSection === "sso" ? (
|
|
||||||
<SSOSection />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function showSettingDialog(selectedSection?: SettingSection): void {
|
|
||||||
generateDialog(
|
|
||||||
{
|
|
||||||
className: "setting-dialog",
|
|
||||||
dialogName: "setting-dialog",
|
|
||||||
},
|
|
||||||
SettingDialog,
|
|
||||||
{
|
|
||||||
selectedSection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
.archived-memo-dialog {
|
|
||||||
@apply px-4;
|
|
||||||
|
|
||||||
> .dialog-container {
|
|
||||||
@apply w-128 max-w-full mb-8;
|
|
||||||
|
|
||||||
> .dialog-content-container {
|
|
||||||
@apply w-full flex flex-col justify-start items-start;
|
|
||||||
|
|
||||||
> .tip-text-container {
|
|
||||||
@apply w-full h-32 flex flex-col justify-center items-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .archived-memos-container {
|
|
||||||
@apply w-full flex flex-col justify-start items-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,11 @@
|
|||||||
|
.archived-memo-page {
|
||||||
|
@apply w-full max-w-3xl min-h-full flex flex-col justify-start items-center pb-8 bg-zinc-100 dark:bg-zinc-800;
|
||||||
|
|
||||||
|
> .tip-text-container {
|
||||||
|
@apply w-full h-32 flex flex-col justify-center items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .archived-memos-container {
|
||||||
|
@apply w-full flex flex-col justify-start items-start;
|
||||||
|
}
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
.setting-dialog {
|
|
||||||
@apply px-4;
|
|
||||||
|
|
||||||
> .dialog-container {
|
|
||||||
@apply w-180 max-w-full h-full sm:h-auto mb-8 p-0;
|
|
||||||
|
|
||||||
> .dialog-content-container {
|
|
||||||
@apply flex flex-row justify-start items-start relative w-full h-full p-0;
|
|
||||||
|
|
||||||
> .close-btn {
|
|
||||||
@apply z-1 flex flex-col justify-center items-center absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 dark:hover:bg-zinc-700 hover:shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .section-selector-container {
|
|
||||||
@apply hidden sm:flex flex-col justify-start items-start sm:w-52 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-none sm:rounded-l-lg p-4 bg-gray-100 dark:bg-zinc-700;
|
|
||||||
|
|
||||||
> .section-title {
|
|
||||||
@apply text-sm mt-2 sm:mt-4 first:mt-4 mb-1 font-mono text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .section-items-container {
|
|
||||||
@apply w-full h-auto flex flex-row sm:flex-col justify-start items-start;
|
|
||||||
|
|
||||||
> .section-item {
|
|
||||||
@apply flex flex-row justify-start items-center text-base select-none mr-3 sm:mr-0 mt-0 sm:mt-2 text-gray-700 dark:text-gray-300 cursor-pointer hover:opacity-80;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
@apply font-bold hover:opacity-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon-text {
|
|
||||||
@apply text-base mr-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .section-content-container {
|
|
||||||
@apply w-full sm:w-auto p-4 sm:px-6 grow flex flex-col justify-start items-start h-full sm:h-128 overflow-y-scroll hide-scrollbar;
|
|
||||||
|
|
||||||
> .section-container {
|
|
||||||
@apply flex flex-col justify-start items-start w-full my-2;
|
|
||||||
|
|
||||||
.title-text {
|
|
||||||
@apply text-sm mt-4 first:mt-2 mb-3 font-mono text-gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .form-label {
|
|
||||||
@apply flex flex-row items-center w-full mb-2;
|
|
||||||
|
|
||||||
> .normal-text {
|
|
||||||
@apply shrink-0 select-text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,47 @@
|
|||||||
|
.setting-page-wrapper {
|
||||||
|
@apply flex flex-row justify-start items-start relative w-full h-full p-4 rounded-lg bg-white dark:bg-zinc-700 dark:text-gray-200 sm:gap-x-4;
|
||||||
|
|
||||||
|
> .section-selector-container {
|
||||||
|
@apply hidden sm:flex flex-col justify-start items-start sm:w-40 h-auto sm:h-full shrink-0;
|
||||||
|
|
||||||
|
> .section-title {
|
||||||
|
@apply text-sm mt-2 sm:mt-4 first:mt-2 mb-1 font-mono text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .section-items-container {
|
||||||
|
@apply w-full h-auto flex flex-row sm:flex-col justify-start items-start;
|
||||||
|
|
||||||
|
> .section-item {
|
||||||
|
@apply flex flex-row justify-start items-center text-base select-none mr-3 sm:mr-0 mt-0 sm:mt-2 text-gray-700 dark:text-gray-300 cursor-pointer hover:opacity-80;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
@apply font-bold hover:opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon-text {
|
||||||
|
@apply text-base mr-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .section-content-container {
|
||||||
|
@apply w-full sm:w-auto grow flex flex-col justify-start items-start h-full sm:h-128 overflow-y-scroll hide-scrollbar;
|
||||||
|
|
||||||
|
> .section-container {
|
||||||
|
@apply flex flex-col justify-start items-start w-full;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
@apply text-sm mt-4 first:mt-2 mb-3 font-mono text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .form-label {
|
||||||
|
@apply flex flex-row items-center w-full mb-2;
|
||||||
|
|
||||||
|
> .normal-text {
|
||||||
|
@apply shrink-0 select-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { useMemoStore } from "@/store/module";
|
||||||
|
import useLoading from "@/hooks/useLoading";
|
||||||
|
import ArchivedMemo from "@/components/ArchivedMemo";
|
||||||
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
|
import "@/less/archived.less";
|
||||||
|
|
||||||
|
const Archived = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const memoStore = useMemoStore();
|
||||||
|
const loadingState = useLoading();
|
||||||
|
const [archivedMemos, setArchivedMemos] = useState<Memo[]>([]);
|
||||||
|
const memos = memoStore.state.memos;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
memoStore
|
||||||
|
.fetchArchivedMemos()
|
||||||
|
.then((result) => {
|
||||||
|
setArchivedMemos(result);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(error.response.data.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loadingState.setFinish();
|
||||||
|
});
|
||||||
|
}, [memos]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full min-h-full flex flex-col md:flex-row justify-start items-start px-4 sm:px-2 pt-2 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
<MobileHeader showSearch={false} />
|
||||||
|
<div className="archived-memo-page">
|
||||||
|
{loadingState.isLoading ? (
|
||||||
|
<div className="tip-text-container">
|
||||||
|
<p className="tip-text">{t("memo.fetching-data")}</p>
|
||||||
|
</div>
|
||||||
|
) : archivedMemos.length === 0 ? (
|
||||||
|
<div className="tip-text-container">
|
||||||
|
<p className="tip-text">{t("memo.no-archived-memos")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="archived-memos-container">
|
||||||
|
{archivedMemos.map((memo) => (
|
||||||
|
<ArchivedMemo key={`${memo.id}-${memo.updatedTs}`} memo={memo} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Archived;
|
@ -0,0 +1,128 @@
|
|||||||
|
import { Option, Select } from "@mui/joy";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserStore } from "@/store/module";
|
||||||
|
import Icon from "@/components/Icon";
|
||||||
|
import BetaBadge from "@/components/BetaBadge";
|
||||||
|
import MyAccountSection from "@/components/Settings/MyAccountSection";
|
||||||
|
import PreferencesSection from "@/components/Settings/PreferencesSection";
|
||||||
|
import MemberSection from "@/components/Settings/MemberSection";
|
||||||
|
import SystemSection from "@/components/Settings/SystemSection";
|
||||||
|
import StorageSection from "@/components/Settings/StorageSection";
|
||||||
|
import SSOSection from "@/components/Settings/SSOSection";
|
||||||
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
|
import "@/less/setting.less";
|
||||||
|
|
||||||
|
type SettingSection = "my-account" | "preference" | "member" | "system" | "storage" | "sso";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
selectedSection: SettingSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Setting = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const user = userStore.state.user;
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
selectedSection: "my-account",
|
||||||
|
});
|
||||||
|
const isHost = user?.role === "HOST";
|
||||||
|
|
||||||
|
const handleSectionSelectorItemClick = (settingSection: SettingSection) => {
|
||||||
|
setState({
|
||||||
|
selectedSection: settingSection,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSettingSectionList = () => {
|
||||||
|
let settingList: SettingSection[] = ["my-account", "preference"];
|
||||||
|
if (isHost) {
|
||||||
|
settingList = settingList.concat(["member", "system", "storage", "sso"]);
|
||||||
|
}
|
||||||
|
return settingList;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="w-full min-h-full flex flex-col md:flex-row justify-start items-start px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
<MobileHeader showSearch={false} />
|
||||||
|
<div className="setting-page-wrapper">
|
||||||
|
<div className="section-selector-container">
|
||||||
|
<span className="section-title">{t("common.basic")}</span>
|
||||||
|
<div className="section-items-container">
|
||||||
|
<span
|
||||||
|
onClick={() => handleSectionSelectorItemClick("my-account")}
|
||||||
|
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon.User className="w-4 h-auto mr-2 opacity-80" /> {t("setting.my-account")}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleSectionSelectorItemClick("preference")}
|
||||||
|
className={`section-item ${state.selectedSection === "preference" ? "selected" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon.Cog className="w-4 h-auto mr-2 opacity-80" /> {t("setting.preference")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isHost ? (
|
||||||
|
<>
|
||||||
|
<span className="section-title">{t("common.admin")}</span>
|
||||||
|
<div className="section-items-container">
|
||||||
|
<span
|
||||||
|
onClick={() => handleSectionSelectorItemClick("member")}
|
||||||
|
className={`section-item ${state.selectedSection === "member" ? "selected" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon.Users className="w-4 h-auto mr-2 opacity-80" /> {t("setting.member")}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleSectionSelectorItemClick("system")}
|
||||||
|
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon.Settings2 className="w-4 h-auto mr-2 opacity-80" /> {t("setting.system")}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleSectionSelectorItemClick("storage")}
|
||||||
|
className={`section-item ${state.selectedSection === "storage" ? "selected" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon.Database className="w-4 h-auto mr-2 opacity-80" /> {t("setting.storage")} <BetaBadge />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => handleSectionSelectorItemClick("sso")}
|
||||||
|
className={`section-item ${state.selectedSection === "sso" ? "selected" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon.Key className="w-4 h-auto mr-2 opacity-80" /> {t("setting.sso")} <BetaBadge />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="section-content-container">
|
||||||
|
<Select
|
||||||
|
className="block sm:!hidden"
|
||||||
|
value={state.selectedSection}
|
||||||
|
onChange={(_, value) => handleSectionSelectorItemClick(value as SettingSection)}
|
||||||
|
>
|
||||||
|
{getSettingSectionList().map((settingSection) => (
|
||||||
|
<Option key={settingSection} value={settingSection}>
|
||||||
|
{t(`setting.${settingSection}`)}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{state.selectedSection === "my-account" ? (
|
||||||
|
<MyAccountSection />
|
||||||
|
) : state.selectedSection === "preference" ? (
|
||||||
|
<PreferencesSection />
|
||||||
|
) : state.selectedSection === "member" ? (
|
||||||
|
<MemberSection />
|
||||||
|
) : state.selectedSection === "system" ? (
|
||||||
|
<SystemSection />
|
||||||
|
) : state.selectedSection === "storage" ? (
|
||||||
|
<StorageSection />
|
||||||
|
) : state.selectedSection === "sso" ? (
|
||||||
|
<SSOSection />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Setting;
|
@ -1,41 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { t } from "i18next";
|
|
||||||
|
|
||||||
export interface MessageGroup {
|
|
||||||
name: string;
|
|
||||||
messageStorageId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MessageGroupState {
|
|
||||||
groupList: MessageGroup[];
|
|
||||||
getState: () => MessageGroupState;
|
|
||||||
addGroup: (group: MessageGroup) => void;
|
|
||||||
removeGroup: (group: MessageGroup) => MessageGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultMessageGroup: MessageGroup = {
|
|
||||||
name: t("ask-ai.default-message-group-title"),
|
|
||||||
messageStorageId: "message-storage",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMessageGroupStore = create<MessageGroupState>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
groupList: [],
|
|
||||||
getState: () => get(),
|
|
||||||
addGroup: (group: MessageGroup) => set((state) => ({ groupList: [...state.groupList, group] })),
|
|
||||||
removeGroup: (group: MessageGroup) => {
|
|
||||||
set((state) => ({
|
|
||||||
groupList: state.groupList.filter((i) => i.name != group.name || i.messageStorageId != group.messageStorageId),
|
|
||||||
}));
|
|
||||||
localStorage.removeItem(group.messageStorageId);
|
|
||||||
const groupList = get().groupList;
|
|
||||||
return groupList.length > 0 ? groupList[groupList.length - 1] : defaultMessageGroup;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: "message-group-storage",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
@ -1,29 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { MessageGroup } from "@/store/zustand/message-group";
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
role: "user" | "assistant";
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MessageState {
|
|
||||||
messageList: Message[];
|
|
||||||
getState: () => MessageState;
|
|
||||||
addMessage: (message: Message) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMessageStore = (options: MessageGroup) => {
|
|
||||||
return create<MessageState>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
messageList: [],
|
|
||||||
getState: () => get(),
|
|
||||||
addMessage: (message: Message) => set((state) => ({ messageList: [...state.messageList, message] })),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: options.messageStorageId,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
Reference in New Issue