mirror of
https://github.com/usememos/memos.git
synced 2025-02-20 21:30:55 +01:00
parent
69225b507b
commit
32e2f1d339
@ -46,27 +46,4 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
|
|||||||
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(result))
|
return c.JSON(http.StatusOK, composeResponse(result))
|
||||||
})
|
})
|
||||||
|
|
||||||
g.GET("/openai/enabled", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
openAIConfigSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
|
||||||
Name: api.SystemSettingOpenAIConfigName,
|
|
||||||
})
|
|
||||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find openai key").SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
openAIConfig := api.OpenAIConfig{}
|
|
||||||
if openAIConfigSetting != nil {
|
|
||||||
err = json.Unmarshal([]byte(openAIConfigSetting.Value), &openAIConfig)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal openai system setting value").SetInternal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if openAIConfig.Key == "" {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "OpenAI API key not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(openAIConfig.Key != ""))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<span className="time-text">
|
<span className="time-text">{getDateTimeString(memo.updatedTs)}</span>
|
||||||
{t("memo.archived-at")} {getDateTimeString(memo.updatedTs)}
|
|
||||||
</span>
|
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<span className="btn-text" onClick={handleRestoreMemoClick}>
|
<span className="btn-text" onClick={handleRestoreMemoClick}>
|
||||||
{t("common.restore")}
|
{t("common.restore")}
|
||||||
|
@ -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,19 +1,15 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { NavLink, useLocation } from "react-router-dom";
|
import { NavLink, useLocation } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGlobalStore, useLayoutStore, useUserStore } from "@/store/module";
|
import { useLayoutStore, useUserStore } from "@/store/module";
|
||||||
import { resolution } from "@/utils/layout";
|
import { resolution } from "@/utils/layout";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import UserBanner from "./UserBanner";
|
import UserBanner from "./UserBanner";
|
||||||
import showSettingDialog from "./SettingDialog";
|
|
||||||
import showAskAIDialog from "./AskAIDialog";
|
|
||||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
|
||||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||||
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const globalStore = useGlobalStore();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
@ -109,37 +105,40 @@ const Header = () => {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
{!isVisitorMode && (
|
{!isVisitorMode && (
|
||||||
<>
|
<>
|
||||||
<button
|
<NavLink
|
||||||
id="header-ask-ai"
|
to="/archived"
|
||||||
className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
id="header-setting"
|
||||||
onClick={() => showAskAIDialog()}
|
className={({ isActive }) =>
|
||||||
|
`${
|
||||||
|
isActive && "bg-white dark:bg-zinc-700 shadow"
|
||||||
|
} px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon.Bot className="mr-3 w-6 h-auto opacity-70" /> {t("ask-ai.title")}
|
<>
|
||||||
</button>
|
<Icon.Archive className="mr-3 w-6 h-auto opacity-70" /> {t("common.archived")}
|
||||||
<button
|
</>
|
||||||
id="header-archived-memo"
|
</NavLink>
|
||||||
className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
<NavLink
|
||||||
onClick={() => showArchivedMemoDialog()}
|
to="/setting"
|
||||||
|
id="header-setting"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`${
|
||||||
|
isActive && "bg-white dark:bg-zinc-700 shadow"
|
||||||
|
} px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon.Archive className="mr-3 w-6 h-auto opacity-70" /> {t("common.archived")}
|
<>
|
||||||
</button>
|
<Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")}
|
||||||
<button
|
</>
|
||||||
id="header-settings"
|
</NavLink>
|
||||||
className="px-4 pr-5 py-2 rounded-full flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
<div className="pr-3 pl-1 w-full">
|
||||||
onClick={() => showSettingDialog()}
|
<button
|
||||||
>
|
className="mt-2 w-full py-3 rounded-full flex flex-row justify-center items-center bg-green-600 font-medium text-white dark:opacity-80 hover:shadow hover:opacity-90"
|
||||||
<Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")}
|
onClick={() => showMemoEditorDialog()}
|
||||||
</button>
|
>
|
||||||
{globalStore.isDev() && (
|
<Icon.Edit3 className="w-4 h-auto mr-1" /> New
|
||||||
<div className="pr-3 pl-1 w-full">
|
</button>
|
||||||
<button
|
</div>
|
||||||
className="mt-2 w-full py-3 rounded-full flex flex-row justify-center items-center bg-green-600 font-medium text-white dark:opacity-80 hover:shadow hover:opacity-90"
|
|
||||||
onClick={() => showMemoEditorDialog()}
|
|
||||||
>
|
|
||||||
<Icon.Edit3 className="w-4 h-auto mr-1" /> New
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isVisitorMode && (
|
{isVisitorMode && (
|
||||||
|
@ -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,14 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useGlobalStore, useUserStore } from "@/store/module";
|
import { useGlobalStore, useUserStore } from "@/store/module";
|
||||||
import Dropdown from "./kit/Dropdown";
|
import Dropdown from "./kit/Dropdown";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import UserAvatar from "./UserAvatar";
|
import UserAvatar from "./UserAvatar";
|
||||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||||
import showSettingDialog from "./SettingDialog";
|
|
||||||
|
|
||||||
const UserBanner = () => {
|
const UserBanner = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { systemStatus } = globalStore.state;
|
const { systemStatus } = globalStore.state;
|
||||||
@ -22,7 +23,7 @@ const UserBanner = () => {
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const handleMyAccountClick = () => {
|
const handleMyAccountClick = () => {
|
||||||
showSettingDialog("my-account");
|
navigate("/setting");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAboutBtnClick = () => {
|
const handleAboutBtnClick = () => {
|
||||||
|
@ -261,14 +261,6 @@ export function deleteIdentityProvider(id: IdentityProviderId) {
|
|||||||
return axios.delete(`/api/idp/${id}`);
|
return axios.delete(`/api/idp/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postChatCompletion(messages: any[]) {
|
|
||||||
return axios.post<ResponseObject<string>>(`/api/openai/chat-completion`, messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkOpenAIEnabled() {
|
|
||||||
return axios.get<ResponseObject<boolean>>(`/api/openai/enabled`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRepoStarCount() {
|
export async function getRepoStarCount() {
|
||||||
const { data } = await axios.get(`https://api.github.com/repos/usememos/memos`, {
|
const { data } = await axios.get(`https://api.github.com/repos/usememos/memos`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
web/src/less/archived.less
Normal file
11
web/src/less/archived.less
Normal file
@ -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,5 +1,5 @@
|
|||||||
.memo-list-container {
|
.memo-list-container {
|
||||||
@apply flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-16 hide-scrollbar;
|
@apply flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar;
|
||||||
|
|
||||||
> .status-text-container {
|
> .status-text-container {
|
||||||
@apply flex flex-col justify-start items-center w-full my-6;
|
@apply flex flex-col justify-start items-center w-full my-6;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
47
web/src/less/setting.less
Normal file
47
web/src/less/setting.less
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
web/src/pages/Archived.tsx
Normal file
56
web/src/pages/Archived.tsx
Normal file
@ -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;
|
@ -86,7 +86,7 @@ const DailyReview = () => {
|
|||||||
const currentDayOfWeek = currentDate.toLocaleDateString(locale, { weekday: "short" });
|
const currentDayOfWeek = currentDate.toLocaleDateString(locale, { weekday: "short" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
<section className="w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<MobileHeader showSearch={false} />
|
<MobileHeader showSearch={false} />
|
||||||
<div className="w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
|
<div className="w-full flex flex-col justify-start items-start px-4 py-3 rounded-xl bg-white dark:bg-zinc-700 text-black dark:text-gray-300">
|
||||||
<div className="relative w-full flex flex-row justify-between items-center">
|
<div className="relative w-full flex flex-row justify-between items-center">
|
||||||
|
@ -84,7 +84,7 @@ const Explore = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
<section className="w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<MobileHeader showSearch={false} />
|
<MobileHeader showSearch={false} />
|
||||||
{!loadingState.isLoading && (
|
{!loadingState.isLoading && (
|
||||||
<main className="relative w-full h-auto flex flex-col justify-start items-start -mt-2">
|
<main className="relative w-full h-auto flex flex-col justify-start items-start -mt-2">
|
||||||
|
@ -32,7 +32,7 @@ function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-start items-start">
|
<div className="w-full flex flex-row justify-start items-start">
|
||||||
<div className="flex-grow shrink w-auto max-w-2xl px-4 sm:px-2 sm:pt-4">
|
<div className="flex-grow shrink w-auto px-4 sm:px-2 sm:pt-4">
|
||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
|
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
|
||||||
{!userStore.isVisitorMode() && <MemoEditor />}
|
{!userStore.isVisitorMode() && <MemoEditor />}
|
||||||
|
@ -194,7 +194,7 @@ const ResourcesDashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-2xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
<section className="w-full max-w-3xl min-h-full flex flex-col justify-start items-center px-4 sm:px-2 sm:pt-4 pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<MobileHeader showSearch={false} />
|
<MobileHeader showSearch={false} />
|
||||||
<div className="w-full relative" onDragEnter={handleDrag}>
|
<div className="w-full relative" onDragEnter={handleDrag}>
|
||||||
{dragActive && (
|
{dragActive && (
|
||||||
|
128
web/src/pages/Setting.tsx
Normal file
128
web/src/pages/Setting.tsx
Normal file
@ -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;
|
@ -5,6 +5,8 @@ import store from "@/store";
|
|||||||
import { initialGlobalState, initialUserState } from "@/store/module";
|
import { initialGlobalState, initialUserState } from "@/store/module";
|
||||||
import DailyReview from "@/pages/DailyReview";
|
import DailyReview from "@/pages/DailyReview";
|
||||||
import ResourcesDashboard from "@/pages/ResourcesDashboard";
|
import ResourcesDashboard from "@/pages/ResourcesDashboard";
|
||||||
|
import Setting from "@/pages/Setting";
|
||||||
|
import Archived from "@/pages/Archived";
|
||||||
|
|
||||||
const Root = lazy(() => import("@/layouts/Root"));
|
const Root = lazy(() => import("@/layouts/Root"));
|
||||||
const Auth = lazy(() => import("@/pages/Auth"));
|
const Auth = lazy(() => import("@/pages/Auth"));
|
||||||
@ -138,6 +140,44 @@ const router = createBrowserRouter([
|
|||||||
// do nth
|
// do nth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { host } = store.getState().user;
|
||||||
|
if (isNullorUndefined(host)) {
|
||||||
|
return redirect("/auth");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "archived",
|
||||||
|
element: <Archived />,
|
||||||
|
loader: async () => {
|
||||||
|
await initialGlobalStateLoader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initialUserState();
|
||||||
|
} catch (error) {
|
||||||
|
// do nth
|
||||||
|
}
|
||||||
|
|
||||||
|
const { host } = store.getState().user;
|
||||||
|
if (isNullorUndefined(host)) {
|
||||||
|
return redirect("/auth");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "setting",
|
||||||
|
element: <Setting />,
|
||||||
|
loader: async () => {
|
||||||
|
await initialGlobalStateLoader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initialUserState();
|
||||||
|
} catch (error) {
|
||||||
|
// do nth
|
||||||
|
}
|
||||||
|
|
||||||
const { host } = store.getState().user;
|
const { host } = store.getState().user;
|
||||||
if (isNullorUndefined(host)) {
|
if (isNullorUndefined(host)) {
|
||||||
return redirect("/auth");
|
return redirect("/auth");
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export { useMemoCacheStore } from "./memo";
|
export { useMemoCacheStore } from "./memo";
|
||||||
export { useMessageStore } from "./message";
|
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user