feat: function delete chat dev done

This commit is contained in:
butterfly
2024-04-26 19:33:22 +08:00
parent 48e8c0a194
commit 1a636b0f50
36 changed files with 598 additions and 187 deletions

View File

@@ -0,0 +1,190 @@
import LoadingIcon from "@/app/icons/three-dots.svg";
import ResetIcon from "@/app/icons/reload.svg";
import styles from "../index.module.scss";
import { useEffect, useState } from "react";
import { Avatar, AvatarPicker } from "@/app/components/emoji";
import { Popover } from "@/app/components/ui-lib";
import Locale, { AllLangs, changeLang, getLang } from "@/app/locales";
import Link from "next/link";
import { IconButton } from "@/app/components/button";
import { useUpdateStore } from "@/app/store/update";
import {
SubmitKey,
Theme,
ThemeConfig,
useAppConfig,
} from "@/app/store/config";
import { getClientConfig } from "@/app/config/client";
import { RELEASE_URL, UPDATE_URL } from "@/app/constant";
import List, { ListItem } from "@/app/components/List";
import Select from "@/app/components/Select";
import SlideRange from "@/app/components/SlideRange";
import Switch from "@/app/components/Switch";
export interface AppSettingProps {}
export default function AppSetting(props: AppSettingProps) {
const [checkingUpdate, setCheckingUpdate] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const updateStore = useUpdateStore();
const config = useAppConfig();
const { update: updateConfig, isMobileScreen } = config;
const currentVersion = updateStore.formatVersion(updateStore.version);
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
const hasNewVersion = currentVersion !== remoteId;
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
function checkUpdate(force = false) {
setCheckingUpdate(true);
updateStore.getLatestVersion(force).then(() => {
setCheckingUpdate(false);
});
console.log("[Update] local version ", updateStore.version);
console.log("[Update] remote version ", updateStore.remoteVersion);
}
useEffect(() => {
// checks per minutes
checkUpdate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<List
widgetStyle={{
selectClassName: "min-w-select-mobile md:min-w-select",
rangeNextLine: isMobileScreen,
}}
>
<ListItem title={Locale.Settings.Avatar}>
<Popover
onClose={() => setShowEmojiPicker(false)}
content={
<AvatarPicker
onEmojiClick={(avatar: string) => {
updateConfig((config) => (config.avatar = avatar));
setShowEmojiPicker(false);
}}
/>
}
open={showEmojiPicker}
>
<div
className={styles.avatar}
onClick={() => {
setShowEmojiPicker(!showEmojiPicker);
}}
>
<Avatar avatar={config.avatar} />
</div>
</Popover>
</ListItem>
<ListItem
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
subTitle={
checkingUpdate
? Locale.Settings.Update.IsChecking
: hasNewVersion
? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
: Locale.Settings.Update.IsLatest
}
>
{checkingUpdate ? (
<LoadingIcon />
) : hasNewVersion ? (
<Link href={updateUrl} target="_blank" className="link">
{Locale.Settings.Update.GoToUpdate}
</Link>
) : (
<IconButton
icon={<ResetIcon />}
text={Locale.Settings.Update.CheckUpdate}
onClick={() => checkUpdate(true)}
/>
)}
</ListItem>
<ListItem title={Locale.Settings.SendKey}>
<Select
value={config.submitKey}
options={Object.values(SubmitKey).map((v) => ({
value: v,
label: v,
}))}
onSelect={(v) => {
updateConfig((config) => (config.submitKey = v));
}}
/>
</ListItem>
<ListItem title={Locale.Settings.Theme}>
<Select
value={config.theme}
options={Object.entries(ThemeConfig).map(([k, t]) => ({
value: k as Theme,
label: t.title,
icon: <t.icon />,
}))}
onSelect={(e) => {
updateConfig((config) => (config.theme = e));
}}
/>
</ListItem>
<ListItem title={Locale.Settings.Lang.Name}>
<Select
value={getLang()}
options={AllLangs.map((lang) => ({ value: lang, label: lang }))}
onSelect={(e) => {
changeLang(e);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.FontSize.Title}
subTitle={Locale.Settings.FontSize.SubTitle}
>
<SlideRange
value={config.fontSize}
range={{
start: 12,
stroke: 28,
}}
step={1}
onSlide={(e) => updateConfig((config) => (config.fontSize = e))}
></SlideRange>
</ListItem>
<ListItem
title={Locale.Settings.AutoGenerateTitle.Title}
subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
>
<Switch
value={config.enableAutoGenerateTitle}
onChange={(e) =>
updateConfig((config) => (config.enableAutoGenerateTitle = e))
}
/>
</ListItem>
<ListItem
title={Locale.Settings.SendPreviewBubble.Title}
subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
>
<Switch
value={config.sendPreviewBubble}
onChange={(e) =>
updateConfig((config) => (config.sendPreviewBubble = e))
}
/>
</ListItem>
</List>
);
}

View File

@@ -0,0 +1,150 @@
import { IconButton } from "@/app/components/button";
import { showConfirm } from "@/app/components/ui-lib";
import { useChatStore } from "@/app/store/chat";
import { useAppConfig } from "@/app/store/config";
import Locale from "@/app/locales";
import { useAccessStore } from "@/app/store/access";
import { useEffect, useMemo, useState } from "react";
import { getClientConfig } from "@/app/config/client";
import { OPENAI_BASE_URL, ServiceProvider } from "@/app/constant";
import { useUpdateStore } from "@/app/store/update";
import ResetIcon from "@/app/icons/reload.svg";
import List, { ListItem } from "@/app/components/List";
import Input from "@/app/components/Input";
import Btn from "@/app/components/Btn";
export default function DangerItems() {
const chatStore = useChatStore();
const appConfig = useAppConfig();
const accessStore = useAccessStore();
const updateStore = useUpdateStore();
const { isMobileScreen } = appConfig;
const enabledAccessControl = useMemo(
() => accessStore.enabledAccessControl(),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const clientConfig = useMemo(() => getClientConfig(), []);
const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
const shouldHideBalanceQuery = useMemo(() => {
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
return (
accessStore.hideBalanceQuery ||
isOpenAiUrl ||
accessStore.provider === ServiceProvider.Azure
);
}, [
accessStore.hideBalanceQuery,
accessStore.openaiUrl,
accessStore.provider,
]);
const [loadingUsage, setLoadingUsage] = useState(false);
const usage = {
used: updateStore.used,
subscription: updateStore.subscription,
};
function checkUsage(force = false) {
if (shouldHideBalanceQuery) {
return;
}
setLoadingUsage(true);
updateStore.updateUsage(force).finally(() => {
setLoadingUsage(false);
});
}
const showUsage = accessStore.isAuthorized();
useEffect(() => {
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<List
widgetStyle={{
rangeNextLine: isMobileScreen,
inputNextLine: isMobileScreen,
}}
>
{showAccessCode && (
<ListItem
title={Locale.Settings.Access.AccessCode.Title}
subTitle={Locale.Settings.Access.AccessCode.SubTitle}
>
<Input
value={accessStore.accessCode}
type="password"
placeholder={Locale.Settings.Access.AccessCode.Placeholder}
onChange={(e) => {
accessStore.update((access) => (access.accessCode = e));
}}
/>
</ListItem>
)}
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
<ListItem
title={Locale.Settings.Usage.Title}
subTitle={
showUsage
? loadingUsage
? Locale.Settings.Usage.IsChecking
: Locale.Settings.Usage.SubTitle(
usage?.used ?? "[?]",
usage?.subscription ?? "[?]",
)
: Locale.Settings.Usage.NoAccess
}
>
{!showUsage || loadingUsage ? (
<div />
) : (
<IconButton
icon={<ResetIcon />}
text={Locale.Settings.Usage.Check}
onClick={() => checkUsage(true)}
/>
)}
</ListItem>
) : null}
<ListItem
title={Locale.Settings.Danger.Reset.Title}
subTitle={Locale.Settings.Danger.Reset.SubTitle}
>
<Btn
text={Locale.Settings.Danger.Reset.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
appConfig.reset();
}
}}
type="danger"
/>
</ListItem>
<ListItem
title={Locale.Settings.Danger.Clear.Title}
subTitle={Locale.Settings.Danger.Clear.SubTitle}
>
<Btn
text={Locale.Settings.Danger.Clear.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
chatStore.clearAllData();
}
}}
type="danger"
/>
</ListItem>
</List>
);
}

View File

@@ -0,0 +1,162 @@
import { useState } from "react";
import List, { ListItem } from "@/app/components/List";
import { ContextPrompts, MaskAvatar } from "@/app/components/mask";
import { Path } from "@/app/constant";
import { ModelConfig, useAppConfig } from "@/app/store/config";
import { Mask } from "@/app/store/mask";
import { Updater } from "@/app/typing";
import { copyToClipboard } from "@/app/utils";
import Locale from "@/app/locales";
import { Popover, showConfirm } from "@/app/components/ui-lib";
import { AvatarPicker } from "@/app/components/emoji";
import ModelSetting from "@/app/containers/Settings/components/ModelSetting";
import { IconButton } from "@/app/components/button";
import CopyIcon from "@/app/icons/copy.svg";
import Switch from "@/app/components/Switch";
import Input from "@/app/components/Input";
export default function MaskConfig(props: {
mask: Mask;
updateMask: Updater<Mask>;
extraListItems?: JSX.Element;
readonly?: boolean;
shouldSyncFromGlobal?: boolean;
}) {
const [showPicker, setShowPicker] = useState(false);
const updateConfig = (updater: (config: ModelConfig) => void) => {
if (props.readonly) return;
const config = { ...props.mask.modelConfig };
updater(config);
props.updateMask((mask) => {
mask.modelConfig = config;
// if user changed current session mask, it will disable auto sync
mask.syncGlobalConfig = false;
});
};
const copyMaskLink = () => {
const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
copyToClipboard(maskLink);
};
const globalConfig = useAppConfig();
const { isMobileScreen } = globalConfig;
return (
<>
<ContextPrompts
context={props.mask.context}
updateContext={(updater) => {
const context = props.mask.context.slice();
updater(context);
props.updateMask((mask) => (mask.context = context));
}}
/>
<List
widgetStyle={{
rangeNextLine: isMobileScreen,
}}
>
<ListItem title={Locale.Mask.Config.Avatar}>
<Popover
content={
<AvatarPicker
onEmojiClick={(emoji) => {
props.updateMask((mask) => (mask.avatar = emoji));
setShowPicker(false);
}}
></AvatarPicker>
}
open={showPicker}
onClose={() => setShowPicker(false)}
>
<div
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
</Popover>
</ListItem>
<ListItem title={Locale.Mask.Config.Name}>
<Input
type="text"
value={props.mask.name}
onChange={(e) =>
props.updateMask((mask) => {
mask.name = e;
})
}
></Input>
</ListItem>
<ListItem
title={Locale.Mask.Config.HideContext.Title}
subTitle={Locale.Mask.Config.HideContext.SubTitle}
>
<Switch
value={!!props.mask.hideContext}
onChange={(e) => {
props.updateMask((mask) => {
mask.hideContext = e;
});
}}
></Switch>
</ListItem>
{!props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Share.Title}
subTitle={Locale.Mask.Config.Share.SubTitle}
>
<IconButton
icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink}
/>
</ListItem>
) : null}
{props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Sync.Title}
subTitle={Locale.Mask.Config.Sync.SubTitle}
>
<Switch
value={!!props.mask.syncGlobalConfig}
onChange={async (e) => {
const checked = e;
if (
checked &&
(await showConfirm(Locale.Mask.Config.Sync.Confirm))
) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
mask.modelConfig = { ...globalConfig.modelConfig };
});
} else if (!checked) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
});
}
}}
/>
</ListItem>
) : null}
<ModelSetting
modelConfig={{ ...props.mask.modelConfig }}
updateConfig={updateConfig}
/>
{props.extraListItems}
</List>
</>
);
}

View File

@@ -0,0 +1,39 @@
import List, { ListItem } from "@/app/components/List";
import Switch from "@/app/components/Switch";
import Locale from "@/app/locales";
import { useAppConfig } from "@/app/store/config";
export interface MaskSettingProps {}
export default function MaskSetting(props: MaskSettingProps) {
const config = useAppConfig();
const updateConfig = config.update;
return (
<List>
<ListItem
title={Locale.Settings.Mask.Splash.Title}
subTitle={Locale.Settings.Mask.Splash.SubTitle}
>
<Switch
value={!config.dontShowMaskSplashScreen}
onChange={(e) =>
updateConfig((config) => (config.dontShowMaskSplashScreen = !e))
}
/>
</ListItem>
<ListItem
title={Locale.Settings.Mask.Builtin.Title}
subTitle={Locale.Settings.Mask.Builtin.SubTitle}
>
<Switch
value={config.hideBuiltinMasks}
onChange={(e) =>
updateConfig((config) => (config.hideBuiltinMasks = e))
}
/>
</ListItem>
</List>
);
}

View File

@@ -0,0 +1,211 @@
import { ListItem } from "@/app/components/List";
import {
ModalConfigValidator,
ModelConfig,
useAppConfig,
} from "@/app/store/config";
import { useAllModels } from "@/app/utils/hooks";
import Locale from "@/app/locales";
import Select from "@/app/components/Select";
import SlideRange from "@/app/components/SlideRange";
import Switch from "@/app/components/Switch";
import Input from "@/app/components/Input";
export default function ModelSetting(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
const allModels = useAllModels();
const { isMobileScreen } = useAppConfig();
return (
<>
<ListItem title={Locale.Settings.Model}>
<Select
value={props.modelConfig.model}
options={allModels
.filter((v) => v.available)
.map((v) => ({
value: v.name,
label: `${v.displayName}(${v.provider?.providerName})`,
}))}
onSelect={(e) => {
props.updateConfig(
(config) => (config.model = ModalConfigValidator.model(e)),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Temperature.Title}
subTitle={Locale.Settings.Temperature.SubTitle}
>
<SlideRange
value={props.modelConfig.temperature}
range={{
start: 0,
stroke: 1,
}}
step={0.1}
onSlide={(e) => {
props.updateConfig(
(config) =>
(config.temperature = ModalConfigValidator.temperature(e)),
);
}}
></SlideRange>
</ListItem>
<ListItem
title={Locale.Settings.TopP.Title}
subTitle={Locale.Settings.TopP.SubTitle}
>
<SlideRange
value={props.modelConfig.top_p ?? 1}
range={{
start: 0,
stroke: 1,
}}
step={0.1}
onSlide={(e) => {
props.updateConfig(
(config) => (config.top_p = ModalConfigValidator.top_p(e)),
);
}}
></SlideRange>
</ListItem>
<ListItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<Input
type="number"
min={1024}
max={512000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.max_tokens = ModalConfigValidator.max_tokens(e)),
)
}
></Input>
</ListItem>
{props.modelConfig.model.startsWith("gemini") ? null : (
<>
<ListItem
title={Locale.Settings.PresencePenalty.Title}
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<SlideRange
value={props.modelConfig.presence_penalty}
range={{
start: -2,
stroke: 4,
}}
step={0.1}
onSlide={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(e)),
);
}}
></SlideRange>
</ListItem>
<ListItem
title={Locale.Settings.FrequencyPenalty.Title}
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
>
<SlideRange
value={props.modelConfig.frequency_penalty}
range={{
start: -2,
stroke: 4,
}}
step={0.1}
onSlide={(e) => {
props.updateConfig(
(config) =>
(config.frequency_penalty =
ModalConfigValidator.frequency_penalty(e)),
);
}}
></SlideRange>
</ListItem>
<ListItem
title={Locale.Settings.InjectSystemPrompts.Title}
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<Switch
value={props.modelConfig.enableInjectSystemPrompts}
onChange={(e) =>
props.updateConfig(
(config) => (config.enableInjectSystemPrompts = e),
)
}
/>
</ListItem>
<ListItem
title={Locale.Settings.InputTemplate.Title}
subTitle={Locale.Settings.InputTemplate.SubTitle}
nextline={isMobileScreen}
>
<Input
type="text"
value={props.modelConfig.template}
onChange={(e = "") =>
props.updateConfig((config) => (config.template = e))
}
className="text-center"
></Input>
</ListItem>
</>
)}
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
>
<SlideRange
value={props.modelConfig.historyMessageCount}
range={{
start: 0,
stroke: 64,
}}
step={1}
onSlide={(e) => {
props.updateConfig((config) => (config.historyMessageCount = e));
}}
></SlideRange>
</ListItem>
<ListItem
title={Locale.Settings.CompressThreshold.Title}
subTitle={Locale.Settings.CompressThreshold.SubTitle}
>
<Input
type="number"
min={500}
max={4000}
value={props.modelConfig.compressMessageLengthThreshold}
onChange={(e) =>
props.updateConfig(
(config) => (config.compressMessageLengthThreshold = e),
)
}
></Input>
</ListItem>
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
<Switch
value={props.modelConfig.sendMemory}
onChange={(e) =>
props.updateConfig((config) => (config.sendMemory = e))
}
/>
</ListItem>
</>
);
}

View File

@@ -0,0 +1,60 @@
import { useState } from "react";
import UserPromptModal from "./UserPromptModal";
import List, { ListItem } from "@/app/components/List";
import Locale from "@/app/locales";
import { useAppConfig } from "@/app/store/config";
import { SearchService, usePromptStore } from "@/app/store/prompt";
import Switch from "@/app/components/Switch";
import Btn from "@/app/components/Btn";
export interface PromptSettingProps {}
export default function PromptSetting(props: PromptSettingProps) {
const [shouldShowPromptModal, setShowPromptModal] = useState(false);
const config = useAppConfig();
const updateConfig = config.update;
const builtinCount = SearchService.count.builtin;
const promptStore = usePromptStore();
const customCount = promptStore.getUserPrompts().length ?? 0;
const textStyle = " !text-sm";
return (
<>
<List>
<ListItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<Switch
value={config.disablePromptHint}
onChange={(e) =>
updateConfig((config) => (config.disablePromptHint = e))
}
/>
</ListItem>
<ListItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(builtinCount, customCount)}
>
<div className="flex gap-3">
<Btn
onClick={() => setShowPromptModal(true)}
text={
<span className={textStyle}>{Locale.Settings.Prompt.Edit}</span>
}
></Btn>
</div>
</ListItem>
</List>
{shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} />
)}
</>
);
}

View File

@@ -0,0 +1,280 @@
import { useMemo } from "react";
import {
Anthropic,
Azure,
Google,
OPENAI_BASE_URL,
ServiceProvider,
SlotID,
} from "@/app/constant";
import Locale from "@/app/locales";
import { useAccessStore } from "@/app/store/access";
import { getClientConfig } from "@/app/config/client";
import { useAppConfig } from "@/app/store/config";
import List, { ListItem } from "@/app/components/List";
import Select from "@/app/components/Select";
import Switch from "@/app/components/Switch";
import Input from "@/app/components/Input";
export default function ProviderSetting() {
const accessStore = useAccessStore();
const config = useAppConfig();
const { isMobileScreen } = config;
const clientConfig = useMemo(() => getClientConfig(), []);
return (
<List
id={SlotID.CustomModel}
widgetStyle={{
inputNextLine: isMobileScreen,
}}
>
{!accessStore.hideUserApiKey && (
<>
{
// Conditionally render the following ListItem based on clientConfig.isApp
!clientConfig?.isApp && ( // only show if isApp is false
<ListItem
title={Locale.Settings.Access.CustomEndpoint.Title}
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
>
<Switch
value={accessStore.useCustomConfig}
onChange={(e) =>
accessStore.update((access) => (access.useCustomConfig = e))
}
/>
</ListItem>
)
}
{accessStore.useCustomConfig && (
<>
<ListItem
title={Locale.Settings.Access.Provider.Title}
subTitle={Locale.Settings.Access.Provider.SubTitle}
>
<Select
value={accessStore.provider}
onSelect={(e) => {
accessStore.update((access) => (access.provider = e));
}}
options={Object.entries(ServiceProvider).map(([k, v]) => ({
value: v,
label: k,
}))}
/>
</ListItem>
{accessStore.provider === ServiceProvider.OpenAI && (
<>
<ListItem
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
subTitle={Locale.Settings.Access.OpenAI.Endpoint.SubTitle}
>
<Input
type="text"
value={accessStore.openaiUrl}
placeholder={OPENAI_BASE_URL}
onChange={(e = "") =>
accessStore.update((access) => (access.openaiUrl = e))
}
></Input>
</ListItem>
<ListItem
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
>
<Input
value={accessStore.openaiApiKey}
type="password"
placeholder={
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) => (access.openaiApiKey = e),
);
}}
/>
</ListItem>
</>
)}
{accessStore.provider === ServiceProvider.Azure && (
<>
<ListItem
title={Locale.Settings.Access.Azure.Endpoint.Title}
subTitle={
Locale.Settings.Access.Azure.Endpoint.SubTitle +
Azure.ExampleEndpoint
}
>
<Input
type="text"
value={accessStore.azureUrl}
placeholder={Azure.ExampleEndpoint}
onChange={(e) =>
accessStore.update((access) => (access.azureUrl = e))
}
></Input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiKey.Title}
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
>
<Input
value={accessStore.azureApiKey}
type="password"
placeholder={
Locale.Settings.Access.Azure.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) => (access.azureApiKey = e),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Azure.ApiVerion.Title}
subTitle={Locale.Settings.Access.Azure.ApiVerion.SubTitle}
>
<Input
type="text"
value={accessStore.azureApiVersion}
placeholder="2023-08-01-preview"
onChange={(e) =>
accessStore.update(
(access) => (access.azureApiVersion = e),
)
}
></Input>
</ListItem>
</>
)}
{accessStore.provider === ServiceProvider.Google && (
<>
<ListItem
title={Locale.Settings.Access.Google.Endpoint.Title}
subTitle={
Locale.Settings.Access.Google.Endpoint.SubTitle +
Google.ExampleEndpoint
}
>
<Input
type="text"
value={accessStore.googleUrl}
placeholder={Google.ExampleEndpoint}
onChange={(e) =>
accessStore.update((access) => (access.googleUrl = e))
}
></Input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Google.ApiKey.Title}
subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
>
<Input
value={accessStore.googleApiKey}
type="password"
placeholder={
Locale.Settings.Access.Google.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) => (access.googleApiKey = e),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Google.ApiVersion.Title}
subTitle={Locale.Settings.Access.Google.ApiVersion.SubTitle}
>
<Input
type="text"
value={accessStore.googleApiVersion}
placeholder="2023-08-01-preview"
onChange={(e) =>
accessStore.update(
(access) => (access.googleApiVersion = e),
)
}
></Input>
</ListItem>
</>
)}
{accessStore.provider === ServiceProvider.Anthropic && (
<>
<ListItem
title={Locale.Settings.Access.Anthropic.Endpoint.Title}
subTitle={
Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
Anthropic.ExampleEndpoint
}
>
<Input
type="text"
value={accessStore.anthropicUrl}
placeholder={Anthropic.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.anthropicUrl = e),
)
}
></Input>
</ListItem>
<ListItem
title={Locale.Settings.Access.Anthropic.ApiKey.Title}
subTitle={Locale.Settings.Access.Anthropic.ApiKey.SubTitle}
>
<Input
value={accessStore.anthropicApiKey}
type="password"
placeholder={
Locale.Settings.Access.Anthropic.ApiKey.Placeholder
}
onChange={(e) => {
accessStore.update(
(access) => (access.anthropicApiKey = e),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
subTitle={
Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
}
>
<Input
type="text"
value={accessStore.anthropicApiVersion}
placeholder={Anthropic.Vision}
onChange={(e) =>
accessStore.update(
(access) => (access.anthropicApiVersion = e),
)
}
></Input>
</ListItem>
</>
)}
</>
)}
</>
)}
<ListItem
title={Locale.Settings.Access.CustomModel.Title}
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
>
<Input
type="text"
value={config.customModels}
placeholder="model1,model2,model3"
onChange={(e) => config.update((config) => (config.customModels = e))}
></Input>
</ListItem>
</List>
);
}

View File

@@ -0,0 +1,47 @@
import Locale from "@/app/locales";
import GobackIcon from "@/app/icons/goback.svg";
export interface ChatHeaderProps {
isMobileScreen: boolean;
goback: () => void;
}
export default function SettingHeader(props: ChatHeaderProps) {
const { isMobileScreen, goback } = props;
return (
<div
className={`
relative flex flex-0 justify-between items-center px-6 py-4 gap-chat-header-gap border-b border-settings-header
max-md:h-menu-title-mobile max-md:bg-settings-header-mobile
`}
data-tauri-drag-region
>
{isMobileScreen ? (
<div
className="absolute left-4 top-[50%] translate-y-[-50%] cursor-pointer"
onClick={() => goback()}
>
<GobackIcon />
</div>
) : null}
<div
className={`
flex-1
max-md:flex max-md:flex-col max-md:items-center max-md:justify-center max-md:gap-0.5 max-md:text
md:mr-4
`}
>
<div
className={`
line-clamp-1 cursor-pointer text-text-settings-panel-header-title text-chat-header-title font-common
max-md:text-sm-title max-md:h-chat-header-title-mobile max-md:leading-5
`}
>
{Locale.Settings.Title}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,198 @@
import { Modal } from "@/app/components/ui-lib";
import { useSyncStore } from "@/app/store/sync";
import Locale from "@/app/locales";
import { IconButton } from "@/app/components/button";
import { ProviderType } from "@/app/utils/cloud";
import { STORAGE_KEY } from "@/app/constant";
import { useMemo, useState } from "react";
import ConnectionIcon from "@/app/icons/connection.svg";
import CloudSuccessIcon from "@/app/icons/cloud-success.svg";
import CloudFailIcon from "@/app/icons/cloud-fail.svg";
import ConfirmIcon from "@/app/icons/confirm.svg";
import LoadingIcon from "@/app/icons/three-dots.svg";
import List, { ListItem } from "@/app/components/List";
import Switch from "@/app/components/Switch";
import Select from "@/app/components/Select";
import Input from "@/app/components/Input";
import { useAppConfig } from "@/app/store";
function CheckButton() {
const syncStore = useSyncStore();
const couldCheck = useMemo(() => {
return syncStore.cloudSync();
}, [syncStore]);
const [checkState, setCheckState] = useState<
"none" | "checking" | "success" | "failed"
>("none");
async function check() {
setCheckState("checking");
const valid = await syncStore.check();
setCheckState(valid ? "success" : "failed");
}
if (!couldCheck) return null;
return (
<IconButton
text={Locale.Settings.Sync.Config.Modal.Check}
bordered
onClick={check}
icon={
checkState === "none" ? (
<ConnectionIcon />
) : checkState === "checking" ? (
<LoadingIcon />
) : checkState === "success" ? (
<CloudSuccessIcon />
) : checkState === "failed" ? (
<CloudFailIcon />
) : (
<ConnectionIcon />
)
}
></IconButton>
);
}
export default function SyncConfigModal(props: { onClose?: () => void }) {
const syncStore = useSyncStore();
const config = useAppConfig();
const { isMobileScreen } = config;
return (
<div className="modal-mask">
<Modal
title={Locale.Settings.Sync.Config.Modal.Title}
onClose={() => props.onClose?.()}
actions={[
<CheckButton key="check" />,
<IconButton
key="confirm"
onClick={props.onClose}
icon={<ConfirmIcon />}
bordered
text={Locale.UI.Confirm}
/>,
]}
>
<List
widgetStyle={{
rangeNextLine: isMobileScreen,
}}
>
<ListItem
title={Locale.Settings.Sync.Config.SyncType.Title}
subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
>
<Select
value={syncStore.provider}
options={Object.entries(ProviderType).map(([k, v]) => ({
value: v,
label: k,
}))}
onSelect={(v) => {
syncStore.update((config) => (config.provider = v));
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Sync.Config.Proxy.Title}
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
>
<Switch
value={syncStore.useProxy}
onChange={(e) => {
syncStore.update((config) => (config.useProxy = e));
}}
/>
</ListItem>
{syncStore.useProxy ? (
<ListItem
title={Locale.Settings.Sync.Config.ProxyUrl.Title}
subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
>
<Input
type="text"
value={syncStore.proxyUrl}
onChange={(e) => {
syncStore.update((config) => (config.proxyUrl = e));
}}
></Input>
</ListItem>
) : null}
{syncStore.provider === ProviderType.WebDAV && (
<>
<ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
<Input
type="text"
value={syncStore.webdav.endpoint}
onChange={(e) => {
syncStore.update((config) => (config.webdav.endpoint = e));
}}
></Input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
<Input
type="text"
value={syncStore.webdav.username}
onChange={(e) => {
syncStore.update((config) => (config.webdav.username = e));
}}
></Input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
<Input
value={syncStore.webdav.password}
type="password"
onChange={(e) => {
syncStore.update((config) => (config.webdav.password = e));
}}
></Input>
</ListItem>
</>
)}
{syncStore.provider === ProviderType.UpStash && (
<>
<ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
<Input
type="text"
value={syncStore.upstash.endpoint}
onChange={(e) => {
syncStore.update((config) => (config.upstash.endpoint = e));
}}
></Input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
<Input
type="text"
value={syncStore.upstash.username}
placeholder={STORAGE_KEY}
onChange={(e) => {
syncStore.update((config) => (config.upstash.username = e));
}}
></Input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
<Input
value={syncStore.upstash.apiKey}
type="password"
onChange={(e) => {
syncStore.update((config) => (config.upstash.apiKey = e));
}}
></Input>
</ListItem>
</>
)}
</List>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,100 @@
import { showToast } from "@/app/components/ui-lib";
import { useChatStore } from "@/app/store/chat";
import { useMaskStore } from "@/app/store/mask";
import { usePromptStore } from "@/app/store/prompt";
import { useSyncStore } from "@/app/store/sync";
import { useMemo, useState } from "react";
import Locale from "@/app/locales";
import SyncConfigModal from "./SyncConfigModal";
import List, { ListItem } from "@/app/components/List";
import Btn from "@/app/components/Btn";
export default function SyncItems() {
const syncStore = useSyncStore();
const chatStore = useChatStore();
const promptStore = usePromptStore();
const maskStore = useMaskStore();
const couldSync = useMemo(() => {
return syncStore.cloudSync();
}, [syncStore]);
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
const stateOverview = useMemo(() => {
const sessions = chatStore.sessions;
const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
return {
chat: sessions.length,
message: messageCount,
prompt: Object.keys(promptStore.prompts).length,
mask: Object.keys(maskStore.masks).length,
};
}, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
const textStyle = "!text-sm";
return (
<>
<List>
<ListItem
title={Locale.Settings.Sync.CloudState}
subTitle={
syncStore.lastProvider
? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
syncStore.lastProvider
}]`
: Locale.Settings.Sync.NotSyncYet
}
>
<div className="flex gap-3">
<Btn
onClick={() => {
setShowSyncConfigModal(true);
}}
text={<span className={textStyle}>{Locale.UI.Config}</span>}
></Btn>
{couldSync && (
<Btn
onClick={async () => {
try {
await syncStore.sync();
showToast(Locale.Settings.Sync.Success);
} catch (e) {
showToast(Locale.Settings.Sync.Fail);
console.error("[Sync]", e);
}
}}
text={<span className={textStyle}>{Locale.UI.Sync}</span>}
></Btn>
)}
</div>
</ListItem>
<ListItem
title={Locale.Settings.Sync.LocalState}
subTitle={Locale.Settings.Sync.Overview(stateOverview)}
>
<div className="flex gap-3">
<Btn
onClick={() => {
syncStore.export();
}}
text={<span className={textStyle}>{Locale.UI.Export}</span>}
></Btn>
<Btn
onClick={async () => {
syncStore.import();
}}
text={<span className={textStyle}>{Locale.UI.Import}</span>}
></Btn>
</div>
</ListItem>
</List>
{showSyncConfigModal && (
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
)}
</>
);
}

View File

@@ -0,0 +1,167 @@
import { useEffect, useState } from "react";
import { nanoid } from "nanoid";
import { Prompt, SearchService, usePromptStore } from "@/app/store/prompt";
import { Input as Textarea, Modal } from "@/app/components/ui-lib";
import Locale from "@/app/locales";
import { IconButton } from "@/app/components/button";
import AddIcon from "@/app/icons/add.svg";
import CopyIcon from "@/app/icons/copy.svg";
import ClearIcon from "@/app/icons/clear.svg";
import EditIcon from "@/app/icons/edit.svg";
import EyeIcon from "@/app/icons/eye.svg";
import styles from "../index.module.scss";
import { copyToClipboard } from "@/app/utils";
import Input from "@/app/components/Input";
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
const prompt = promptStore.get(props.id);
return prompt ? (
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.EditModal.Title}
onClose={props.onClose}
actions={[
<IconButton
key=""
onClick={props.onClose}
text={Locale.UI.Confirm}
bordered
/>,
]}
>
<div className={styles["edit-prompt-modal"]}>
<Input
type="text"
value={prompt.title}
readOnly={!prompt.isUser}
className={styles["edit-prompt-title"]}
onChange={(e) =>
promptStore.updatePrompt(props.id, (prompt) => (prompt.title = e))
}
></Input>
<Textarea
value={prompt.content}
readOnly={!prompt.isUser}
className={styles["edit-prompt-content"]}
rows={10}
onInput={(e) =>
promptStore.updatePrompt(
props.id,
(prompt) => (prompt.content = e.currentTarget.value),
)
}
></Textarea>
</div>
</Modal>
</div>
) : null;
}
export default function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore();
const userPrompts = promptStore.getUserPrompts();
const builtinPrompts = SearchService.builtinPrompts;
const allPrompts = userPrompts.concat(builtinPrompts);
const [searchInput, setSearchInput] = useState("");
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
const [editingPromptId, setEditingPromptId] = useState<string>();
useEffect(() => {
if (searchInput.length > 0) {
const searchResult = SearchService.search(searchInput);
setSearchPrompts(searchResult);
} else {
setSearchPrompts([]);
}
}, [searchInput]);
return (
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.Modal.Title}
onClose={() => props.onClose?.()}
actions={[
<IconButton
key="add"
onClick={() => {
const promptId = promptStore.add({
id: nanoid(),
createdAt: Date.now(),
title: "Empty Prompt",
content: "Empty Prompt Content",
});
setEditingPromptId(promptId);
}}
icon={<AddIcon />}
bordered
text={Locale.Settings.Prompt.Modal.Add}
/>,
]}
>
<div className={styles["user-prompt-modal"]}>
<Input
type="text"
className={styles["user-prompt-search"]}
placeholder={Locale.Settings.Prompt.Modal.Search}
value={searchInput}
onChange={(e) => setSearchInput(e)}
></Input>
<div className={styles["user-prompt-list"]}>
{prompts.map((v, _) => (
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
<div className={styles["user-prompt-header"]}>
<div className={styles["user-prompt-title"]}>{v.title}</div>
<div className={styles["user-prompt-content"] + " one-line"}>
{v.content}
</div>
</div>
<div className={styles["user-prompt-buttons"]}>
{v.isUser && (
<IconButton
icon={<ClearIcon />}
className={styles["user-prompt-button"]}
onClick={() => promptStore.remove(v.id!)}
/>
)}
{v.isUser ? (
<IconButton
icon={<EditIcon />}
className={styles["user-prompt-button"]}
onClick={() => setEditingPromptId(v.id)}
/>
) : (
<IconButton
icon={<EyeIcon />}
className={styles["user-prompt-button"]}
onClick={() => setEditingPromptId(v.id)}
/>
)}
<IconButton
icon={<CopyIcon />}
className={styles["user-prompt-button"]}
onClick={() => copyToClipboard(v.content)}
/>
</div>
</div>
))}
</div>
</div>
</Modal>
{editingPromptId !== undefined && (
<EditPromptModal
id={editingPromptId!}
onClose={() => setEditingPromptId(undefined)}
/>
)}
</div>
);
}