mirror of
https://github.com/usememos/memos.git
synced 2025-02-19 04:40:40 +01:00
parent
69225b507b
commit
32e2f1d339
@ -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 != ""))
|
||||
})
|
||||
}
|
||||
|
@ -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")}
|
||||
|
@ -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 { 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 && (
|
||||
|
@ -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 { 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 = () => {
|
||||
|
@ -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: {
|
||||
|
@ -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 {
|
||||
@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;
|
||||
|
@ -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" });
|
||||
|
||||
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">
|
||||
|
@ -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">
|
||||
|
@ -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 />}
|
||||
|
@ -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
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 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");
|
||||
|
@ -1,2 +1 @@
|
||||
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