chore: update page routes (#1790)

chore: update routers
This commit is contained in:
boojack 2023-06-03 13:03:22 +08:00 committed by GitHub
parent 69225b507b
commit 32e2f1d339
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 324 additions and 611 deletions

View File

@ -46,27 +46,4 @@ func (s *Server) registerOpenAIRoutes(g *echo.Group) {
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 != ""))
})
}

View File

@ -53,9 +53,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
return (
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
<div className="memo-top-wrapper">
<span className="time-text">
{t("memo.archived-at")} {getDateTimeString(memo.updatedTs)}
</span>
<span className="time-text">{getDateTimeString(memo.updatedTs)}</span>
<div className="btns-container">
<span className="btn-text" onClick={handleRestoreMemoClick}>
{t("common.restore")}

View File

@ -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;

View File

@ -1,19 +1,15 @@
import { useEffect } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useGlobalStore, useLayoutStore, useUserStore } from "@/store/module";
import { useLayoutStore, useUserStore } from "@/store/module";
import { resolution } from "@/utils/layout";
import Icon from "./Icon";
import UserBanner from "./UserBanner";
import showSettingDialog from "./SettingDialog";
import showAskAIDialog from "./AskAIDialog";
import showArchivedMemoDialog from "./ArchivedMemoDialog";
import showAboutSiteDialog from "./AboutSiteDialog";
import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog";
const Header = () => {
const { t } = useTranslation();
const globalStore = useGlobalStore();
const location = useLocation();
const userStore = useUserStore();
const layoutStore = useLayoutStore();
@ -109,37 +105,40 @@ const Header = () => {
</NavLink>
{!isVisitorMode && (
<>
<button
id="header-ask-ai"
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"
onClick={() => showAskAIDialog()}
<NavLink
to="/archived"
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.Bot className="mr-3 w-6 h-auto opacity-70" /> {t("ask-ai.title")}
</button>
<button
id="header-archived-memo"
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"
onClick={() => showArchivedMemoDialog()}
<>
<Icon.Archive className="mr-3 w-6 h-auto opacity-70" /> {t("common.archived")}
</>
</NavLink>
<NavLink
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>
<button
id="header-settings"
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"
onClick={() => showSettingDialog()}
>
<Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")}
</button>
{globalStore.isDev() && (
<div className="pr-3 pl-1 w-full">
<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"
onClick={() => showMemoEditorDialog()}
>
<Icon.Edit3 className="w-4 h-auto mr-1" /> New
</button>
</div>
)}
<>
<Icon.Settings className="mr-3 w-6 h-auto opacity-70" /> {t("common.settings")}
</>
</NavLink>
<div className="pr-3 pl-1 w-full">
<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"
onClick={() => showMemoEditorDialog()}
>
<Icon.Edit3 className="w-4 h-auto mr-1" /> New
</button>
</div>
</>
)}
{isVisitorMode && (

View File

@ -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,
}
);
}

View File

@ -1,14 +1,15 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useGlobalStore, useUserStore } from "@/store/module";
import Dropdown from "./kit/Dropdown";
import Icon from "./Icon";
import UserAvatar from "./UserAvatar";
import showAboutSiteDialog from "./AboutSiteDialog";
import showSettingDialog from "./SettingDialog";
const UserBanner = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const globalStore = useGlobalStore();
const userStore = useUserStore();
const { systemStatus } = globalStore.state;
@ -22,7 +23,7 @@ const UserBanner = () => {
}, [user]);
const handleMyAccountClick = () => {
showSettingDialog("my-account");
navigate("/setting");
};
const handleAboutBtnClick = () => {

View File

@ -261,14 +261,6 @@ export function deleteIdentityProvider(id: IdentityProviderId) {
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() {
const { data } = await axios.get(`https://api.github.com/repos/usememos/memos`, {
headers: {

View File

@ -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;
}
}
}
}

View 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;
}
}

View File

@ -1,5 +1,5 @@
.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 {
@apply flex flex-col justify-start items-center w-full my-6;

View File

@ -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
View 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;
}
}
}
}
}

View 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;

View File

@ -86,7 +86,7 @@ const DailyReview = () => {
const currentDayOfWeek = currentDate.toLocaleDateString(locale, { weekday: "short" });
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} />
<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">

View File

@ -84,7 +84,7 @@ const Explore = () => {
};
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} />
{!loadingState.isLoading && (
<main className="relative w-full h-auto flex flex-col justify-start items-start -mt-2">

View File

@ -32,7 +32,7 @@ function Home() {
return (
<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 />
<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 />}

View File

@ -194,7 +194,7 @@ const ResourcesDashboard = () => {
};
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} />
<div className="w-full relative" onDragEnter={handleDrag}>
{dragActive && (

128
web/src/pages/Setting.tsx Normal file
View 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;

View File

@ -5,6 +5,8 @@ import store from "@/store";
import { initialGlobalState, initialUserState } from "@/store/module";
import DailyReview from "@/pages/DailyReview";
import ResourcesDashboard from "@/pages/ResourcesDashboard";
import Setting from "@/pages/Setting";
import Archived from "@/pages/Archived";
const Root = lazy(() => import("@/layouts/Root"));
const Auth = lazy(() => import("@/pages/Auth"));
@ -138,6 +140,44 @@ const router = createBrowserRouter([
// 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;
if (isNullorUndefined(host)) {
return redirect("/auth");

View File

@ -1,2 +1 @@
export { useMemoCacheStore } from "./memo";
export { useMessageStore } from "./message";

View File

@ -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",
}
)
);

View File

@ -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,
}
)
);
};