mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-13 04:33:42 +08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -18,7 +18,7 @@ export function AuthPage() {
|
||||
const goChat = () => navigate(Path.Chat);
|
||||
const resetAccessCode = () => {
|
||||
accessStore.update((access) => {
|
||||
access.token = "";
|
||||
access.openaiApiKey = "";
|
||||
access.accessCode = "";
|
||||
});
|
||||
}; // Reset access code to empty string
|
||||
@@ -56,11 +56,11 @@ export function AuthPage() {
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Token.Placeholder}
|
||||
value={accessStore.token}
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
value={accessStore.openaiApiKey}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.token = e.currentTarget.value),
|
||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -61,7 +61,10 @@ export function ChatItem(props: {
|
||||
{props.narrow ? (
|
||||
<div className={styles["chat-item-narrow"]}>
|
||||
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["chat-item-narrow-count"]}>
|
||||
{props.count}
|
||||
|
||||
@@ -442,11 +442,27 @@ export function ChatActions(props: {
|
||||
|
||||
// switch model
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
const models = useAllModels()
|
||||
.filter((m) => m.available)
|
||||
.map((m) => m.name);
|
||||
const allModels = useAllModels();
|
||||
const models = useMemo(
|
||||
() => allModels.filter((m) => m.available),
|
||||
[allModels],
|
||||
);
|
||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// if current model is not available
|
||||
// switch to first available model
|
||||
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
|
||||
if (isUnavaliableModel && models.length > 0) {
|
||||
const nextModel = models[0].name as ModelType;
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.mask.modelConfig.model = nextModel),
|
||||
);
|
||||
showToast(nextModel);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentModel, models]);
|
||||
|
||||
return (
|
||||
<div className={styles["chat-input-actions"]}>
|
||||
<div>
|
||||
@@ -525,8 +541,8 @@ export function ChatActions(props: {
|
||||
<Selector
|
||||
defaultSelectedValue={currentModel}
|
||||
items={models.map((m) => ({
|
||||
title: m,
|
||||
value: m,
|
||||
title: m.displayName,
|
||||
value: m.name,
|
||||
}))}
|
||||
onClose={() => setShowModelSelector(false)}
|
||||
onSelection={(s) => {
|
||||
@@ -534,9 +550,6 @@ export function ChatActions(props: {
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.model = s[0] as ModelType;
|
||||
session.mask.syncGlobalConfig = false;
|
||||
session.mask.usePlugins = /^gpt(?!.*03\d{2}$).*$/.test(
|
||||
session.mask.modelConfig.model,
|
||||
);
|
||||
});
|
||||
showToast(s[0]);
|
||||
}}
|
||||
@@ -1028,7 +1041,9 @@ function _Chat() {
|
||||
).then((res) => {
|
||||
if (!res) return;
|
||||
if (payload.key) {
|
||||
accessStore.update((access) => (access.token = payload.key!));
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = payload.key!),
|
||||
);
|
||||
}
|
||||
if (payload.url) {
|
||||
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
||||
@@ -1188,7 +1203,12 @@ function _Chat() {
|
||||
{["system"].includes(message.role) ? (
|
||||
<Avatar avatar="2699-fe0f" />
|
||||
) : (
|
||||
<MaskAvatar mask={session.mask} />
|
||||
<MaskAvatar
|
||||
avatar={session.mask.avatar}
|
||||
model={
|
||||
message.model || session.mask.modelConfig.model
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -186,7 +186,8 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
border: var(--border-in-light);
|
||||
|
||||
*:not(li) {
|
||||
code,
|
||||
pre {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
import styles from "./exporter.module.scss";
|
||||
import {
|
||||
@@ -27,7 +27,7 @@ import { Avatar } from "./emoji";
|
||||
import dynamic from "next/dynamic";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import { toBlob, toJpeg, toPng } from "html-to-image";
|
||||
import { toBlob, toPng } from "html-to-image";
|
||||
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
||||
import { api } from "../client/api";
|
||||
import { prettyObject } from "../utils/format";
|
||||
@@ -41,7 +41,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
export function ExportMessageModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal title={Locale.Export.Title} onClose={props.onClose}>
|
||||
<Modal
|
||||
title={Locale.Export.Title}
|
||||
onClose={props.onClose}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontSize: 14,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
{Locale.Exporter.Description.Title}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ minHeight: "40vh" }}>
|
||||
<MessageExporter />
|
||||
</div>
|
||||
@@ -149,7 +164,7 @@ export function MessageExporter() {
|
||||
if (exportConfig.includeContext) {
|
||||
ret.push(...session.mask.context);
|
||||
}
|
||||
ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
|
||||
ret.push(...session.messages.filter((m) => selection.has(m.id)));
|
||||
return ret;
|
||||
}, [
|
||||
exportConfig.includeContext,
|
||||
@@ -260,7 +275,8 @@ export function RenderExport(props: {
|
||||
});
|
||||
|
||||
props.onRender(renderMsgs);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={domRef}>
|
||||
@@ -437,13 +453,13 @@ export function ImagePreviewer(props: {
|
||||
showToast(Locale.Export.Image.Toast);
|
||||
const dom = previewRef.current;
|
||||
if (!dom) return;
|
||||
|
||||
|
||||
const isApp = getClientConfig()?.isApp;
|
||||
|
||||
|
||||
try {
|
||||
const blob = await toPng(dom);
|
||||
if (!blob) return;
|
||||
|
||||
|
||||
if (isMobile || (isApp && window.__TAURI__)) {
|
||||
if (isApp && window.__TAURI__) {
|
||||
const result = await window.__TAURI__.dialog.save({
|
||||
@@ -459,7 +475,7 @@ export function ImagePreviewer(props: {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
if (result !== null) {
|
||||
const response = await fetch(blob);
|
||||
const buffer = await response.arrayBuffer();
|
||||
@@ -604,8 +620,6 @@ export function MarkdownPreviewer(props: {
|
||||
);
|
||||
}
|
||||
|
||||
// modified by BackTrackZ now it's looks better
|
||||
|
||||
export function JsonPreviewer(props: {
|
||||
messages: ChatMessage[];
|
||||
topic: string;
|
||||
|
||||
@@ -5,13 +5,13 @@ import RemarkBreaks from "remark-breaks";
|
||||
import RehypeKatex from "rehype-katex";
|
||||
import RemarkGfm from "remark-gfm";
|
||||
import RehypeHighlight from "rehype-highlight";
|
||||
import { useRef, useState, RefObject, useEffect } from "react";
|
||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import React from "react";
|
||||
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { showImageModal } from "./ui-lib";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
@@ -99,7 +99,29 @@ export function PreCode(props: { children: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
function escapeDollarNumber(text: string) {
|
||||
let escapedText = "";
|
||||
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
let char = text[i];
|
||||
const nextChar = text[i + 1] || " ";
|
||||
|
||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
||||
char = "\\$";
|
||||
}
|
||||
|
||||
escapedText += char;
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(
|
||||
() => escapeDollarNumber(props.content),
|
||||
[props.content],
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
@@ -124,7 +146,7 @@ function _MarkDownContent(props: { content: string }) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.content}
|
||||
{escapedContent}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ChatMessage,
|
||||
createMessage,
|
||||
ModelConfig,
|
||||
ModelType,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
} from "../store";
|
||||
@@ -58,11 +59,11 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function MaskAvatar(props: { mask: Mask }) {
|
||||
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||
<Avatar avatar={props.mask.avatar} />
|
||||
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
|
||||
return props.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||
<Avatar avatar={props.avatar} />
|
||||
) : (
|
||||
<Avatar model={props.mask.modelConfig.model} />
|
||||
<Avatar model={props.model} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +124,10 @@ export function MaskConfig(props: {
|
||||
onClick={() => setShowPicker(true)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</ListItem>
|
||||
@@ -398,7 +402,7 @@ export function MaskPage() {
|
||||
setSearchText(text);
|
||||
if (text.length > 0) {
|
||||
const result = allMasks.filter((m) =>
|
||||
m.name.toLowerCase().includes(text.toLowerCase())
|
||||
m.name.toLowerCase().includes(text.toLowerCase()),
|
||||
);
|
||||
setSearchMasks(result);
|
||||
} else {
|
||||
@@ -523,7 +527,7 @@ export function MaskPage() {
|
||||
<div className={styles["mask-item"]} key={m.id}>
|
||||
<div className={styles["mask-header"]}>
|
||||
<div className={styles["mask-icon"]}>
|
||||
<MaskAvatar mask={m} />
|
||||
<MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
|
||||
</div>
|
||||
<div className={styles["mask-title"]}>
|
||||
<div className={styles["mask-name"]}>{m.name}</div>
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
max-width: calc(100% - 40px);
|
||||
flex: 1;
|
||||
max-width: calc(100% - 80px);
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
@@ -71,6 +71,12 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import { Updater } from "../typing";
|
||||
import { IconButton } from "./button";
|
||||
@@ -73,11 +73,23 @@ export function MessageSelector(props: {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
||||
const messages = session.messages.filter(
|
||||
(m, i) =>
|
||||
m.id && // message must have id
|
||||
isValid(m) &&
|
||||
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
|
||||
const allMessages = useMemo(() => {
|
||||
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
|
||||
if (startIndex === session.messages.length - 1) {
|
||||
startIndex = 0;
|
||||
}
|
||||
return session.messages.slice(startIndex);
|
||||
}, [session.messages, session.clearContextIndex]);
|
||||
|
||||
const messages = useMemo(
|
||||
() =>
|
||||
allMessages.filter(
|
||||
(m, i) =>
|
||||
m.id && // message must have id
|
||||
isValid(m) &&
|
||||
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
|
||||
),
|
||||
[allMessages],
|
||||
);
|
||||
const messageCount = messages.length;
|
||||
const config = useAppConfig();
|
||||
@@ -176,6 +188,8 @@ export function MessageSelector(props: {
|
||||
<div className={styles["messages"]}>
|
||||
{messages.map((m, i) => {
|
||||
if (!isInSearchResult(m.id!)) return null;
|
||||
const id = m.id ?? i;
|
||||
const isSelected = props.selection.has(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -185,7 +199,6 @@ export function MessageSelector(props: {
|
||||
key={i}
|
||||
onClick={() => {
|
||||
props.updateSelection((selection) => {
|
||||
const id = m.id ?? i;
|
||||
selection.has(id) ? selection.delete(id) : selection.add(id);
|
||||
});
|
||||
onClickIndex(i);
|
||||
@@ -195,7 +208,10 @@ export function MessageSelector(props: {
|
||||
{m.role === "user" ? (
|
||||
<Avatar avatar={config.avatar}></Avatar>
|
||||
) : (
|
||||
<MaskAvatar mask={session.mask} />
|
||||
<MaskAvatar
|
||||
avatar={session.mask.avatar}
|
||||
model={m.model || session.mask.modelConfig.model}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles["body"]}>
|
||||
@@ -206,6 +222,10 @@ export function MessageSelector(props: {
|
||||
{m.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["checkbox"]}>
|
||||
<input type="checkbox" checked={isSelected}></input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -25,11 +25,13 @@ export function ModelConfigList(props: {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{allModels.map((v, i) => (
|
||||
<option value={v.name} key={i} disabled={!v.available}>
|
||||
{v.name}
|
||||
</option>
|
||||
))}
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={v.name} key={i}>
|
||||
{v.displayName}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
|
||||
@@ -17,21 +17,13 @@ import { useCommand } from "../command";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
|
||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
||||
const xmin = Math.max(aRect.x, bRect.x);
|
||||
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
|
||||
const ymin = Math.max(aRect.y, bRect.y);
|
||||
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
|
||||
const width = xmax - xmin;
|
||||
const height = ymax - ymin;
|
||||
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
|
||||
return intersectionArea;
|
||||
}
|
||||
|
||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||||
return (
|
||||
<div className={styles["mask"]} onClick={props.onClick}>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -51,10 +51,13 @@ import Locale, {
|
||||
import { copyToClipboard } from "../utils";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Azure,
|
||||
OPENAI_BASE_URL,
|
||||
Path,
|
||||
RELEASE_URL,
|
||||
STORAGE_KEY,
|
||||
ServiceProvider,
|
||||
SlotID,
|
||||
UPDATE_URL,
|
||||
} from "../constant";
|
||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||
@@ -581,8 +584,16 @@ export function Settings() {
|
||||
const accessStore = useAccessStore();
|
||||
const shouldHideBalanceQuery = useMemo(() => {
|
||||
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
|
||||
return accessStore.hideBalanceQuery || isOpenAiUrl;
|
||||
}, [accessStore.hideBalanceQuery, accessStore.openaiUrl]);
|
||||
return (
|
||||
accessStore.hideBalanceQuery ||
|
||||
isOpenAiUrl ||
|
||||
accessStore.provider === ServiceProvider.Azure
|
||||
);
|
||||
}, [
|
||||
accessStore.hideBalanceQuery,
|
||||
accessStore.openaiUrl,
|
||||
accessStore.provider,
|
||||
]);
|
||||
|
||||
const usage = {
|
||||
used: updateStore.used,
|
||||
@@ -878,16 +889,16 @@ export function Settings() {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<List>
|
||||
{showAccessCode ? (
|
||||
<List id={SlotID.CustomModel}>
|
||||
{showAccessCode && (
|
||||
<ListItem
|
||||
title={Locale.Settings.AccessCode.Title}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
title={Locale.Settings.Access.AccessCode.Title}
|
||||
subTitle={Locale.Settings.Access.AccessCode.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.AccessCode.Placeholder}
|
||||
placeholder={Locale.Settings.Access.AccessCode.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
@@ -895,46 +906,154 @@ export function Settings() {
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
{!accessStore.hideUserApiKey && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Endpoint.Title}
|
||||
subTitle={Locale.Settings.Endpoint.SubTitle}
|
||||
title={Locale.Settings.Access.CustomEndpoint.Title}
|
||||
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.openaiUrl}
|
||||
placeholder="https://api.openai.com/"
|
||||
type="checkbox"
|
||||
checked={accessStore.useCustomConfig}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.openaiUrl = e.currentTarget.value),
|
||||
(access) =>
|
||||
(access.useCustomConfig = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Token.Title}
|
||||
subTitle={Locale.Settings.Token.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.token}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Token.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.token = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
) : null}
|
||||
{accessStore.useCustomConfig && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Provider.Title}
|
||||
subTitle={Locale.Settings.Access.Provider.SubTitle}
|
||||
>
|
||||
<Select
|
||||
value={accessStore.provider}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.provider = e.target
|
||||
.value as ServiceProvider),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Object.entries(ServiceProvider).map(([k, v]) => (
|
||||
<option value={v} key={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
{!shouldHideBalanceQuery ? (
|
||||
{accessStore.provider === "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.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.openaiApiKey}
|
||||
type="text"
|
||||
placeholder={
|
||||
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
|
||||
}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Azure.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.azureApiKey}
|
||||
type="text"
|
||||
placeholder={
|
||||
Locale.Settings.Access.Azure.ApiKey.Placeholder
|
||||
}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.azureApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</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.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
|
||||
<ListItem
|
||||
title={Locale.Settings.Usage.Title}
|
||||
subTitle={
|
||||
@@ -961,8 +1080,8 @@ export function Settings() {
|
||||
) : null}
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.CustomModel.Title}
|
||||
subTitle={Locale.Settings.CustomModel.SubTitle}
|
||||
title={Locale.Settings.Access.CustomModel.Title}
|
||||
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
.select-with-icon-select {
|
||||
height: 100%;
|
||||
border: var(--border-in-light);
|
||||
padding: 10px 25px 10px 10px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
border-radius: 10px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -70,14 +70,12 @@ export function ListItem(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function List(props: {
|
||||
children:
|
||||
| Array<JSX.Element | null | undefined>
|
||||
| JSX.Element
|
||||
| null
|
||||
| undefined;
|
||||
}) {
|
||||
return <div className={styles.list}>{props.children}</div>;
|
||||
export function List(props: { children: React.ReactNode; id?: string }) {
|
||||
return (
|
||||
<div className={styles.list} id={props.id}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Loading() {
|
||||
@@ -99,8 +97,9 @@ export function Loading() {
|
||||
interface ModalProps {
|
||||
title: string;
|
||||
children?: any;
|
||||
actions?: JSX.Element[];
|
||||
actions?: React.ReactNode[];
|
||||
defaultMax?: boolean;
|
||||
footer?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function Modal(props: ModalProps) {
|
||||
@@ -149,6 +148,7 @@ export function Modal(props: ModalProps) {
|
||||
<div className={styles["modal-content"]}>{props.children}</div>
|
||||
|
||||
<div className={styles["modal-footer"]}>
|
||||
{props.footer}
|
||||
<div className={styles["modal-actions"]}>
|
||||
{props.actions?.map((action, i) => (
|
||||
<div key={i} className={styles["modal-action"]}>
|
||||
|
||||
Reference in New Issue
Block a user