chore: add dev guard for memo chat (#1968)

This commit is contained in:
boojack
2023-07-16 13:02:52 +08:00
committed by GitHub
parent 032509ddba
commit 220cba84ae
17 changed files with 284 additions and 163 deletions

View File

@ -1,4 +1,4 @@
import { Input } from "@mui/joy";
import { Button, Input } from "@mui/joy";
import React, { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n";
@ -123,14 +123,14 @@ const CreateTagDialog: React.FC<Props> = (props: Props) => {
{shownSuggestTagNameList.length > 0 && (
<>
<div className="mt-4 mb-1 text-sm w-full flex flex-row justify-start items-center">
<span className="text-gray-400">Tag suggestions</span>
<button className="btn-normal ml-2 px-2 py-0 leading-6 font-mono" onClick={handleToggleShowSuggestionTags}>
<span className="text-gray-400 mr-2">Tag suggestions</span>
<Button size="sm" variant="outlined" onClick={handleToggleShowSuggestionTags}>
{showTagSuggestions ? "hide" : "show"}
</button>
</Button>
</div>
{showTagSuggestions && (
<>
<div className="w-full flex flex-row justify-start items-start flex-wrap">
<div className="w-full flex flex-row justify-start items-start flex-wrap mb-2">
{shownSuggestTagNameList.map((tag) => (
<span
className="max-w-[120px] text-sm mr-2 mt-1 font-mono cursor-pointer truncate dark:text-gray-300 hover:opacity-60"
@ -141,9 +141,9 @@ const CreateTagDialog: React.FC<Props> = (props: Props) => {
</span>
))}
</div>
<button className="btn-normal mt-2 px-2 py-0 leading-6 font-mono" onClick={handleSaveSuggestTagList}>
<Button size="sm" onClick={handleSaveSuggestTagList}>
Save all
</button>
</Button>
</>
)}
</>

View File

@ -1,7 +1,7 @@
import classNames from "classnames";
import { useEffect } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useLayoutStore, useUserStore } from "@/store/module";
import { useGlobalStore, useLayoutStore, useUserStore } from "@/store/module";
import { useTranslate } from "@/utils/i18n";
import { resolution } from "@/utils/layout";
import Icon from "./Icon";
@ -12,6 +12,7 @@ import UpgradeVersionView from "./UpgradeVersionBanner";
const Header = () => {
const t = useTranslate();
const location = useLocation();
const globalStore = useGlobalStore();
const userStore = useUserStore();
const layoutStore = useLayoutStore();
const showHeader = layoutStore.state.showHeader;
@ -111,6 +112,7 @@ const Header = () => {
{!isVisitorMode && (
<>
{globalStore.isDev() && (
<NavLink
to="/memo-chat"
id="header-memo-chat"
@ -125,6 +127,7 @@ const Header = () => {
<Icon.Bot className="mr-3 w-6 h-auto opacity-70" /> {t("memo-chat.title")}
</>
</NavLink>
)}
<NavLink
to="/archived"
id="header-archived"

View File

@ -11,7 +11,7 @@ interface ConversationTabProps {
const ConversationTab = ({ item, selectedConversationId, setSelectedConversationId, closeConversation }: ConversationTabProps) => {
return (
<div
className={`flex rounded-lg h-8 px-3 border dark:border-zinc-600 ${
className={`flex rounded-lg h-8 px-3 cursor-pointer border dark:border-zinc-600 ${
selectedConversationId === item.messageStorageId ? "bg-white dark:bg-zinc-700" : "bg-gray-200 dark:bg-zinc-800 opacity-60"
}`}
key={item.messageStorageId}
@ -21,7 +21,7 @@ const ConversationTab = ({ item, selectedConversationId, setSelectedConversation
>
<div className="truncate m-auto">{item.name}</div>
<Icon.X
className="w-4 h-auto m-auto cursor-pointer"
className="ml-1 w-4 h-auto m-auto cursor-pointer opacity-60"
onClick={(e: any) => {
closeConversation(e);
}}

View File

@ -21,11 +21,11 @@ const ResourceListView = (props: Props) => {
return (
<div
key={resource.id}
className="max-w-full flex flex-row justify-start items-center flex-nowrap bg-gray-100 dark:bg-zinc-800 hover:opacity-80 px-2 py-1 rounded cursor-pointer text-gray-500"
className="max-w-full flex flex-row justify-start items-center flex-nowrap bg-gray-100 dark:bg-zinc-800 px-2 py-1 rounded text-gray-500"
>
<ResourceIcon resourceType={resource.type} className="w-4 h-auto mr-1" />
<span className="text-sm max-w-xs truncate font-mono">{resource.filename}</span>
<Icon.X className="w-4 h-auto ml-1 hover:opacity-80" onClick={() => handleDeleteResource(resource.id)} />
<ResourceIcon resource={resource} className="w-4 h-auto mr-1" />
<span className="text-sm max-w-xs truncate">{resource.filename}</span>
<Icon.X className="w-4 h-auto ml-1 cursor-pointer hover:opacity-80" onClick={() => handleDeleteResource(resource.id)} />
</div>
);
})}

View File

@ -1,4 +1,3 @@
import { Button } from "@mui/joy";
import { isNumber, last, uniq } from "lodash-es";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "react-hot-toast";
@ -422,9 +421,9 @@ const MemoEditor = (props: Props) => {
<div className="editor-footer-container">
<MemoVisibilitySelector value={state.memoVisibility} onChange={handleMemoVisibilityChange} />
<div className="buttons-container">
<Button disabled={!allowSave} onClick={handleSaveBtnClick}>
<button className="action-btn confirm-btn" disabled={!allowSave} onClick={handleSaveBtnClick}>
{t("editor.save")}
</Button>
</button>
</div>
</div>
</div>

View File

@ -31,6 +31,7 @@ const MemoFilter = () => {
}}
>
<Icon.Target className="icon-text" /> {shortcut?.title}
<Icon.X className="w-4 h-auto ml-1 opacity-40" />
</div>
<div
className={"filter-item-container " + (tagQuery ? "" : "!hidden")}
@ -39,6 +40,7 @@ const MemoFilter = () => {
}}
>
<Icon.Tag className="icon-text" /> {tagQuery}
<Icon.X className="w-4 h-auto ml-1 opacity-40" />
</div>
<div
className={"filter-item-container " + (memoType ? "" : "!hidden")}
@ -48,6 +50,7 @@ const MemoFilter = () => {
>
<Icon.Box className="icon-text" />{" "}
{t(getTextWithMemoType(memoType as MemoSpecType) as Exclude<ReturnType<typeof getTextWithMemoType>, "">)}
<Icon.X className="w-4 h-auto ml-1 opacity-40" />
</div>
<div
className={"filter-item-container " + (visibility ? "" : "!hidden")}
@ -56,6 +59,7 @@ const MemoFilter = () => {
}}
>
<Icon.Eye className="icon-text" /> {visibility}
<Icon.X className="w-4 h-auto ml-1 opacity-40" />
</div>
{duration && duration.from < duration.to ? (
<div
@ -70,6 +74,7 @@ const MemoFilter = () => {
to: getDateString(duration.to),
interpolation: { escapeValue: false },
})}
<Icon.X className="w-4 h-auto ml-1 opacity-40" />
</div>
) : null}
<div
@ -79,6 +84,7 @@ const MemoFilter = () => {
}}
>
<Icon.Search className="icon-text" /> {textQuery}
<Icon.X className="w-4 h-auto ml-1 opacity-40" />
</div>
</div>
);

View File

@ -1,18 +1,33 @@
import classNames from "classnames";
import { getResourceUrl } from "@/utils/resource";
import Icon from "./Icon";
import SquareDiv from "./kit/SquareDiv";
import showPreviewImageDialog from "./PreviewImageDialog";
interface Props {
className: string;
resourceType: string;
resource: Resource;
}
const ResourceIcon = (props: Props) => {
const { className, resourceType } = props;
const { className, resource } = props;
let ResourceIcon = Icon.FileText;
if (resourceType.includes("image")) {
ResourceIcon = Icon.Image;
if (resource.type.includes("image")) {
const url = getResourceUrl(resource);
return (
<SquareDiv key={resource.id} className={classNames("cursor-pointer rounded hover:shadow", className)}>
<img
className="min-h-full min-w-full w-auto h-auto rounded"
src={resource.externalLink ? url : url + "?thumbnail=1"}
onClick={() => showPreviewImageDialog([url], 0)}
decoding="async"
loading="lazy"
/>
</SquareDiv>
);
}
const ResourceIcon = Icon.FileText;
return <ResourceIcon className={className} />;
};

View File

@ -1,4 +1,4 @@
import { Table } from "@mui/joy";
import { Button, Input } from "@mui/joy";
import React, { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n";
@ -7,7 +7,6 @@ import * as api from "@/helpers/api";
import Dropdown from "../kit/Dropdown";
import { showCommonDialog } from "../Dialog/CommonDialog";
import showChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog";
import "@/less/settings/member-section.less";
interface State {
createUserUsername: string;
@ -30,7 +29,7 @@ const PreferencesSection = () => {
const fetchUserList = async () => {
const { data } = await api.getUserList();
setUserList(data);
setUserList(data.sort((a, b) => a.id - b.id));
};
const handleUsernameInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -117,52 +116,52 @@ const PreferencesSection = () => {
return (
<div className="section-container member-section-container">
<p className="title-text">{t("setting.member-section.create-a-member")}</p>
<div className="create-member-container">
<div className="input-form-container">
<span className="field-text">{t("common.username")}</span>
<input
type="text"
autoComplete="new-password"
placeholder={t("common.username")}
value={state.createUserUsername}
onChange={handleUsernameInputChange}
/>
<div className="w-full flex flex-col justify-start items-start gap-2">
<div className="flex flex-col justify-start items-start gap-1">
<span className="text-sm">{t("common.username")}</span>
<Input type="text" placeholder={t("common.username")} value={state.createUserUsername} onChange={handleUsernameInputChange} />
</div>
<div className="input-form-container">
<span className="field-text">{t("common.password")}</span>
<input
type="password"
autoComplete="new-password"
placeholder={t("common.password")}
value={state.createUserPassword}
onChange={handlePasswordInputChange}
/>
<div className="flex flex-col justify-start items-start gap-1">
<span className="text-sm">{t("common.password")}</span>
<Input type="password" placeholder={t("common.password")} value={state.createUserPassword} onChange={handlePasswordInputChange} />
</div>
<div className="btns-container">
<button className="btn-normal" onClick={handleCreateUserBtnClick}>
{t("common.create")}
</button>
<Button onClick={handleCreateUserBtnClick}>{t("common.create")}</Button>
</div>
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="w-full flex flex-row justify-between items-center mt-6">
<div className="title-text">{t("setting.member-list")}</div>
</div>
<Table>
<div className="w-full overflow-x-auto">
<div className="inline-block min-w-full align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th>ID</th>
<th>{t("common.username")}</th>
<th></th>
<th scope="col" className="py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900">
ID
</th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900">
{t("common.username")}
</th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900">
{t("common.nickname")}
</th>
<th scope="col" className="px-3 py-2 text-left text-sm font-semibold text-gray-900">
{t("common.email")}
</th>
<th scope="col" className="relative py-2 pl-3 pr-4"></th>
</tr>
</thead>
<tbody>
<tbody className="divide-y divide-gray-200">
{userList.map((user) => (
<tr key={user.id}>
<td className="field-text id-text">{user.id}</td>
<td className="field-text username-text">{user.username}</td>
<td className="flex flex-row justify-end items-center">
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900">{user.id}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.username}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.nickname}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.email}</td>
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">
{currentUser?.id === user.id ? (
<span className="tip-text">{t("common.yourself")}</span>
<span>{t("common.yourself")}</span>
) : (
<Dropdown
actions={
@ -204,7 +203,9 @@ const PreferencesSection = () => {
</tr>
))}
</tbody>
</Table>
</table>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,102 @@
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n";
import { Button, Input } from "@mui/joy";
import { useGlobalStore } from "@/store/module";
import * as api from "@/helpers/api";
import LearnMore from "../LearnMore";
import "@/less/settings/system-section.less";
interface OpenAIConfig {
key: string;
host: string;
}
const OpenAISection = () => {
const t = useTranslate();
const globalStore = useGlobalStore();
const [openAIConfig, setOpenAIConfig] = useState<OpenAIConfig>({
key: "",
host: "",
});
useEffect(() => {
globalStore.fetchSystemStatus();
}, []);
useEffect(() => {
api.getSystemSetting().then(({ data: systemSettings }) => {
const openAIConfigSetting = systemSettings.find((setting) => setting.name === "openai-config");
if (openAIConfigSetting) {
setOpenAIConfig(JSON.parse(openAIConfigSetting.value));
}
});
}, []);
const handleOpenAIConfigKeyChanged = (value: string) => {
setOpenAIConfig({
...openAIConfig,
key: value,
});
};
const handleOpenAIConfigHostChanged = (value: string) => {
setOpenAIConfig({
...openAIConfig,
host: value,
});
};
const handleSaveOpenAIConfig = async () => {
try {
await api.upsertSystemSetting({
name: "openai-config",
value: JSON.stringify(openAIConfig),
});
} catch (error) {
console.error(error);
return;
}
toast.success("OpenAI Config updated");
};
return (
<div className="section-container system-section-container">
<p className="title-text">{t("setting.openai")}</p>
<div className="form-label">
<div className="flex flex-row items-center">
<span className="text-sm mr-1">{t("setting.system-section.openai-api-key")}</span>
<LearnMore title={t("setting.system-section.openai-api-key-description")} url="https://platform.openai.com/account/api-keys" />
</div>
</div>
<Input
className="w-full"
sx={{
fontFamily: "monospace",
fontSize: "14px",
}}
placeholder={t("setting.system-section.openai-api-key-placeholder")}
value={openAIConfig.key}
onChange={(event) => handleOpenAIConfigKeyChanged(event.target.value)}
/>
<div className="form-label mt-2">
<span className="normal-text">{t("setting.system-section.openai-api-host")}</span>
</div>
<Input
className="w-full"
sx={{
fontFamily: "monospace",
fontSize: "14px",
}}
placeholder={t("setting.system-section.openai-api-host-placeholder")}
value={openAIConfig.host}
onChange={(event) => handleOpenAIConfigHostChanged(event.target.value)}
/>
<div className="mt-4">
<Button onClick={handleSaveOpenAIConfig}>{t("common.save")}</Button>
</div>
</div>
);
};
export default OpenAISection;

View File

@ -1,3 +1,4 @@
import { Button, Input } from "@mui/joy";
import { useState } from "react";
import { useTranslate } from "@/utils/i18n";
import { toast } from "react-hot-toast";
@ -103,9 +104,9 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
{t("setting.system-section.server-name")}
<span className="text-sm text-gray-400 ml-1">({t("setting.system-section.customize-server.default")})</span>
</p>
<input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
<Input className="w-full" type="text" value={state.name} onChange={handleNameChanged} />
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.icon-url")}</p>
<input type="text" className="input-text" value={state.logoUrl} onChange={handleLogoUrlChanged} />
<Input className="w-full" type="text" value={state.logoUrl} onChange={handleLogoUrlChanged} />
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.description")}</p>
<Textarea minRows="2" maxRows="4" className="!input-text" value={state.description} onChange={handleDescriptionChanged} />
<p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.locale")}</p>
@ -114,17 +115,15 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
<AppearanceSelect className="!w-full" value={state.appearance} onChange={handleAppearanceSelectChange} />
<div className="mt-4 w-full flex flex-row justify-between items-center space-x-2">
<div className="flex flex-row justify-start items-center">
<button className="btn-normal" onClick={handleRestoreButtonClick}>
<Button variant="outlined" onClick={handleRestoreButtonClick}>
{t("common.restore")}
</button>
</Button>
</div>
<div className="flex flex-row justify-end items-center">
<button className="btn-text" onClick={handleCloseButtonClick}>
<Button variant="plain" onClick={handleCloseButtonClick}>
{t("common.cancel")}
</button>
<button className="btn-primary" onClick={handleSaveButtonClick}>
{t("common.save")}
</button>
</Button>
<Button onClick={handleSaveButtonClick}>{t("common.save")}</Button>
</div>
</div>
</div>

View File

@ -21,7 +21,7 @@
@layer components {
.btn-normal {
@apply select-none flex flex-row justify-center items-center border dark:border-zinc-700 cursor-pointer px-3 text-sm leading-8 rounded-md hover:opacity-80 hover:shadow disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:shadow-none;
@apply select-none flex flex-row justify-center items-center border dark:border-zinc-800 cursor-pointer px-3 text-sm leading-8 rounded-md hover:opacity-80 hover:shadow disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:shadow-none;
}
.btn-primary {

View File

@ -1,21 +0,0 @@
.member-section-container {
> .create-member-container {
@apply w-full flex flex-col justify-start items-start;
> .input-form-container {
@apply w-full mb-3 flex flex-row justify-start items-center;
> .field-text {
@apply text-sm text-gray-600 dark:text-gray-400 w-20 text-right pr-2;
}
> input {
@apply border dark:border-zinc-700 rounded text-sm leading-7 shadow-inner px-2 dark:bg-zinc-800;
}
}
> .btns-container {
@apply w-full mb-6 pl-20 flex flex-row justify-start items-center;
}
}
}

View File

@ -173,6 +173,7 @@
"member": "Member",
"member-list": "Member list",
"system": "System",
"openai": "OpenAI",
"storage": "Storage",
"sso": "SSO",
"account-section": {

View File

@ -160,13 +160,13 @@ const Auth = () => {
{actionBtnLoadingState.isLoading && <Icon.Loader className="w-4 h-auto mr-2 animate-spin dark:text-gray-300" />}
{systemStatus?.allowSignUp && (
<>
<Button variant={"plain"} loading={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
<Button variant={"plain"} disabled={actionBtnLoadingState.isLoading} onClick={handleSignUpButtonClick}>
{t("common.sign-up")}
</Button>
<span className="mr-2 font-mono text-gray-200">/</span>
</>
)}
<Button type="submit" loading={actionBtnLoadingState.isLoading} onClick={handleSignInButtonClick}>
<Button type="submit" disabled={actionBtnLoadingState.isLoading} onClick={handleSignInButtonClick}>
{t("common.sign-in")}
</Button>
</div>

View File

@ -57,7 +57,10 @@ const Explore = () => {
})
: memos;
const sortedMemos = shownMemos.filter((m) => m.rowStatus === "NORMAL" && m.visibility !== "PRIVATE");
const sortedMemos = shownMemos
.filter((m) => m.rowStatus === "NORMAL" && m.visibility !== "PRIVATE")
.sort((mi, mj) => mj.displayTs - mi.displayTs);
const handleFetchMoreClick = async () => {
try {
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, memos.length);

View File

@ -119,18 +119,16 @@ const MemoChat = () => {
};
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="flex space-x-2">
<div className="w-full flex flex-row justify-between items-center">
<p className="flex flex-row justify-start items-center select-none rounded">
<div className="w-full flex">
<div className="w-auto flex flex-row justify-start items-center select-none shrink-0 mr-4">
<Icon.Bot className="w-5 h-auto mr-1" /> {t("memo-chat.title")}
</p>
</div>
<span className="flex flex-row w-full justify-start items-center">
<div className="flex space-x-2 max-w-md overflow-scroll">
<div className="flex flex-row w-auto justify-start items-center overflow-y-hidden overflow-x-auto">
<div className="flex space-x-2 overflow-x-auto">
{conversationList.map((item: Conversation) => (
<ConversationTab
key={item.messageStorageId}
@ -146,8 +144,7 @@ const MemoChat = () => {
/>
))}
</div>
<button className="btn-text px-1 ml-1">
<button className="btn-text px-1 ml-1 shrink-0">
<Icon.Plus
className="w-4 h-auto"
onClick={() => {
@ -155,7 +152,7 @@ const MemoChat = () => {
}}
/>
</button>
</span>
</div>
</div>
<div className="dialog-content-container w-full">

View File

@ -1,19 +1,20 @@
import { Option, Select } from "@mui/joy";
import { useState } from "react";
import { useTranslate } from "@/utils/i18n";
import { useUserStore } from "@/store/module";
import { useGlobalStore, 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 OpenAISection from "@/components/Settings/OpenAISection";
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";
type SettingSection = "my-account" | "preference" | "member" | "system" | "openai" | "storage" | "sso";
interface State {
selectedSection: SettingSection;
@ -21,6 +22,7 @@ interface State {
const Setting = () => {
const t = useTranslate();
const globalStore = useGlobalStore();
const userStore = useUserStore();
const user = userStore.state.user;
const [state, setState] = useState<State>({
@ -37,8 +39,12 @@ const Setting = () => {
const getSettingSectionList = () => {
let settingList: SettingSection[] = ["my-account", "preference"];
if (isHost) {
if (globalStore.isDev()) {
settingList = settingList.concat(["member", "system", "openai", "storage", "sso"]);
} else {
settingList = settingList.concat(["member", "system", "storage", "sso"]);
}
}
return settingList;
};
@ -78,6 +84,14 @@ const Setting = () => {
>
<Icon.Settings2 className="w-4 h-auto mr-2 opacity-80" /> {t("setting.system")}
</span>
{globalStore.isDev() && (
<span
onClick={() => handleSectionSelectorItemClick("openai")}
className={`section-item ${state.selectedSection === "openai" ? "selected" : ""}`}
>
<Icon.Bot className="w-4 h-auto mr-2 opacity-80" /> {t("setting.openai")} <BetaBadge />
</span>
)}
<span
onClick={() => handleSectionSelectorItemClick("storage")}
className={`section-item ${state.selectedSection === "storage" ? "selected" : ""}`}
@ -114,6 +128,8 @@ const Setting = () => {
<MemberSection />
) : state.selectedSection === "system" ? (
<SystemSection />
) : state.selectedSection === "openai" ? (
<OpenAISection />
) : state.selectedSection === "storage" ? (
<StorageSection />
) : state.selectedSection === "sso" ? (