feat: redesign settings page
This commit is contained in:
parent
f7074bba8c
commit
c99086447e
|
@ -2,11 +2,11 @@ import * as React from "react";
|
|||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
export default function IconButton(props: {
|
||||
export default function Btn(props: {
|
||||
onClick?: () => void;
|
||||
icon?: JSX.Element;
|
||||
type?: ButtonType;
|
||||
text?: string;
|
||||
text?: React.ReactNode;
|
||||
bordered?: boolean;
|
||||
shadow?: boolean;
|
||||
className?: string;
|
||||
|
@ -20,8 +20,6 @@ export default function IconButton(props: {
|
|||
icon,
|
||||
type,
|
||||
text,
|
||||
bordered,
|
||||
shadow,
|
||||
className,
|
||||
title,
|
||||
disabled,
|
||||
|
@ -29,18 +27,30 @@ export default function IconButton(props: {
|
|||
autoFocus,
|
||||
} = props;
|
||||
|
||||
let btnClassName;
|
||||
|
||||
switch (type) {
|
||||
case "primary":
|
||||
btnClassName = `${disabled ? "bg-blue-300" : "bg-blue-600"} text-white`;
|
||||
break;
|
||||
case "danger":
|
||||
btnClassName = `${
|
||||
disabled ? "bg-blue-300" : "bg-blue-600"
|
||||
} text-text-danger`;
|
||||
break;
|
||||
default:
|
||||
btnClassName = `${
|
||||
disabled ? "bg-gray-100" : "bg-gray-300"
|
||||
} text-gray-500`;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
${className ?? ""}
|
||||
py-2 px-3 flex items-center justify-center gap-1 rounded-action-btn shadow-btn transition-all duration-300 select-none
|
||||
${
|
||||
type === "primary"
|
||||
? `${disabled ? "bg-blue-300" : "bg-blue-600"}`
|
||||
: `${disabled ? "bg-gray-100" : "bg-gray-300"}`
|
||||
}
|
||||
${disabled ? "cursor-not-allowed" : "cursor-pointer"}
|
||||
${type === "primary" ? `text-white` : `text-gray-500`}
|
||||
${btnClassName}
|
||||
`}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
export interface CardProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
title?: ReactNode;
|
||||
inMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function Card(props: CardProps) {
|
||||
const { className, children, title, inMobile } = props;
|
||||
|
||||
let titleClassName = "ml-4 mb-3";
|
||||
if (inMobile) {
|
||||
titleClassName = "ml-3 mb-3";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div
|
||||
className={`capitalize font-black font-setting-card-title text-sm-mobile font-weight-setting-card-title ${titleClassName}`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className={`px-4 py-1 rounded-lg bg-white ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import PasswordVisible from "@/app/icons/passwordVisible.svg";
|
||||
import PasswordInvisible from "@/app/icons/passwordInvisible.svg";
|
||||
import {
|
||||
DetailedHTMLProps,
|
||||
InputHTMLAttributes,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import List from "@/app/components/List";
|
||||
|
||||
export interface CommonInputProps
|
||||
extends Omit<
|
||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
"onChange" | "type" | "value"
|
||||
> {
|
||||
className?: string;
|
||||
}
|
||||
export interface NumberInputProps {
|
||||
onChange?: (v: number) => void;
|
||||
type?: "number";
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export interface TextInputProps {
|
||||
onChange?: (v: string) => void;
|
||||
type?: "text" | "password";
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
onChange?: ((v: string) => void) | ((v: number) => void);
|
||||
type?: "text" | "password" | "number";
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
export default function Input(
|
||||
props: CommonInputProps & NumberInputProps,
|
||||
): JSX.Element;
|
||||
export default function Input(
|
||||
props: CommonInputProps & TextInputProps,
|
||||
): JSX.Element;
|
||||
export default function Input(props: CommonInputProps & InputProps) {
|
||||
const { value, type = "text", onChange, className, ...rest } = props;
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const internalType = (show && "text") || type;
|
||||
|
||||
const { update } = useContext(List.ListContext);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
update?.({ type: "input" });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-[100%] rounded-chat-input bg-gray-100 flex gap-3 items-center px-3 py-2 ${className}`}
|
||||
>
|
||||
<input
|
||||
{...rest}
|
||||
className=" overflow-hidden text-black text-sm-title leading-input outline-none flex-1"
|
||||
type={internalType}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (type === "number") {
|
||||
const v = e.currentTarget.valueAsNumber;
|
||||
(onChange as NumberInputProps["onChange"])?.(v);
|
||||
} else {
|
||||
const v = e.currentTarget.value;
|
||||
(onChange as TextInputProps["onChange"])?.(v);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{type == "password" && (
|
||||
<div onClick={() => setShow((pre) => !pre)}>
|
||||
{show ? <PasswordVisible /> : <PasswordInvisible />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface WidgetStyle {
|
||||
selectClassName?: string;
|
||||
inputClassName?: string;
|
||||
rangeClassName?: string;
|
||||
switchClassName?: string;
|
||||
inputNextLine?: boolean;
|
||||
rangeNextLine?: boolean;
|
||||
}
|
||||
|
||||
interface ChildrenMeta {
|
||||
type?: "unknown" | "input" | "range";
|
||||
}
|
||||
|
||||
export interface ListProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
id?: string;
|
||||
isMobileScreen?: boolean;
|
||||
widgetStyle?: WidgetStyle;
|
||||
}
|
||||
|
||||
export interface ListItemProps {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
nextline?: boolean;
|
||||
}
|
||||
|
||||
export const ListContext = createContext<
|
||||
{ isMobileScreen?: boolean; update?: (m: ChildrenMeta) => void } & WidgetStyle
|
||||
>({ isMobileScreen: false });
|
||||
|
||||
export function ListItem(props: ListItemProps) {
|
||||
const {
|
||||
className = "",
|
||||
onClick,
|
||||
title,
|
||||
subTitle,
|
||||
children,
|
||||
nextline,
|
||||
} = props;
|
||||
|
||||
const context = useContext(ListContext);
|
||||
|
||||
const [childrenMeta, setMeta] = useState<ChildrenMeta>({});
|
||||
|
||||
const { isMobileScreen, inputNextLine, rangeNextLine } = context;
|
||||
|
||||
let containerClassName = "py-3";
|
||||
let titleClassName = "";
|
||||
if (isMobileScreen) {
|
||||
containerClassName = "py-2";
|
||||
titleClassName = "";
|
||||
}
|
||||
|
||||
let internalNextLine;
|
||||
|
||||
switch (childrenMeta.type) {
|
||||
case "input":
|
||||
internalNextLine = !!(nextline || inputNextLine);
|
||||
break;
|
||||
case "range":
|
||||
internalNextLine = !!(nextline || rangeNextLine);
|
||||
break;
|
||||
default:
|
||||
internalNextLine = false;
|
||||
}
|
||||
if (childrenMeta.type === "input") {
|
||||
console.log("===============", internalNextLine, nextline, inputNextLine);
|
||||
}
|
||||
|
||||
const update = useCallback((m: ChildrenMeta) => {
|
||||
setMeta(m);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative after:h-[0.5px] after:bottom-0 after:w-[100%] after:left-0 after:absolute last:after:hidden after:bg-gray-100 ${
|
||||
internalNextLine ? "" : "flex gap-3"
|
||||
} justify-between items-center px-0 ${containerClassName} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 flex flex-col justify-start gap-1 ${titleClassName}`}
|
||||
>
|
||||
<div className=" font-common text-sm-mobile font-weight-[500] line-clamp-1">
|
||||
{title}
|
||||
</div>
|
||||
{subTitle && <div className={` text-sm text-gray-300`}>{subTitle}</div>}
|
||||
</div>
|
||||
<ListContext.Provider value={{ ...context, update }}>
|
||||
<div
|
||||
className={`${
|
||||
internalNextLine ? "mt-[0.625rem]" : "max-w-[70%]"
|
||||
} flex items-center justify-center`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ListContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function List(props: ListProps) {
|
||||
const { className, children, id, widgetStyle } = props;
|
||||
const { isMobileScreen } = useContext(ListContext);
|
||||
return (
|
||||
<ListContext.Provider value={{ isMobileScreen, ...widgetStyle }}>
|
||||
<div className={`flex flex-col w-[100%] ${className}`} id={id}>
|
||||
{children}
|
||||
</div>
|
||||
</ListContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
List.ListItem = ListItem;
|
||||
List.ListContext = ListContext;
|
||||
|
||||
export default List;
|
|
@ -1,16 +1,17 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { Path } from "@/app/constant";
|
||||
import useDragSideBar from "@/app/hooks/useDragSideBar";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import {
|
||||
ComponentType,
|
||||
Context,
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
} from "@/app/constant";
|
||||
import useDrag from "@/app/hooks/useDrag";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import { updateGlobalCSSVars } from "@/app/utils/client";
|
||||
import { ComponentType, useRef, useState } from "react";
|
||||
|
||||
import DragIcon from "@/app/icons/drag.svg";
|
||||
import { useAppConfig } from "@/app/store/config";
|
||||
|
||||
export interface MenuWrapperInspectProps {
|
||||
setShowPanel?: (v: boolean) => void;
|
||||
|
@ -28,10 +29,35 @@ export default function MenuLayout<
|
|||
const [showPanel, setShowPanel] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
// drag side bar
|
||||
const { onDragStart } = useDragSideBar();
|
||||
const { onDragStart } = useDrag({
|
||||
customToggle: () => {
|
||||
config.update((config) => {
|
||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
});
|
||||
},
|
||||
customDragMove: (nextWidth: number) => {
|
||||
const { menuWidth } = updateGlobalCSSVars(nextWidth);
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--menu-width",
|
||||
`${menuWidth}px`,
|
||||
);
|
||||
config.update((config) => {
|
||||
config.sidebarWidth = nextWidth;
|
||||
});
|
||||
},
|
||||
customLimit: (x: number) =>
|
||||
Math.max(
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_SIDEBAR_WIDTH, startDragWidth.current + x),
|
||||
),
|
||||
});
|
||||
|
||||
let containerClassName = "flex h-[100%] w-[100%]";
|
||||
let listClassName =
|
||||
|
@ -64,7 +90,10 @@ export default function MenuLayout<
|
|||
{!isMobileScreen && (
|
||||
<div
|
||||
className={`group absolute right-0 h-[100%] flex items-center`}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
onPointerDown={(e) => {
|
||||
startDragWidth.current = config.sidebarWidth;
|
||||
onDragStart(e as any);
|
||||
}}
|
||||
>
|
||||
<div className="opacity-0 group-hover:bg-[rgba($color: #000000, $alpha: 0.01) group-hover:opacity-20">
|
||||
<DragIcon />
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import useRelativePosition from "@/app/hooks/useRelativePosition";
|
||||
import { getCSSVar } from "@/app/utils";
|
||||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const ArrowIcon = ({ color }: { color: string }) => {
|
||||
return (
|
||||
|
@ -19,6 +21,20 @@ const ArrowIcon = ({ color }: { color: string }) => {
|
|||
};
|
||||
|
||||
const baseZIndex = 100;
|
||||
const popoverRootName = "popoverRoot";
|
||||
let popoverRoot = document.querySelector(
|
||||
`#${popoverRootName}`,
|
||||
) as HTMLDivElement;
|
||||
if (!popoverRoot) {
|
||||
popoverRoot = document.createElement("div");
|
||||
document.body.appendChild(popoverRoot);
|
||||
popoverRoot.style.height = "0px";
|
||||
popoverRoot.style.width = "100%";
|
||||
popoverRoot.style.position = "fixed";
|
||||
popoverRoot.style.bottom = "0";
|
||||
popoverRoot.style.zIndex = "100";
|
||||
popoverRoot.id = "popoverRootName";
|
||||
}
|
||||
|
||||
export default function Popover(props: {
|
||||
content?: JSX.Element | string;
|
||||
|
@ -46,6 +62,21 @@ export default function Popover(props: {
|
|||
} = props;
|
||||
|
||||
const [internalShow, setShow] = useState(false);
|
||||
const { position, getRelativePosition } = useRelativePosition({
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
const {
|
||||
distanceToBottomBoundary = 0,
|
||||
distanceToLeftBoundary = 0,
|
||||
distanceToRightBoundary = -10000,
|
||||
distanceToTopBoundary = 0,
|
||||
targetH = 0,
|
||||
targetW = 0,
|
||||
} = position?.poi || {};
|
||||
|
||||
let placementStyle: React.CSSProperties = {};
|
||||
const popoverCommonClass = `absolute p-2 box-border`;
|
||||
|
||||
const mergedShow = show ?? internalShow;
|
||||
|
||||
|
@ -56,6 +87,13 @@ export default function Popover(props: {
|
|||
|
||||
switch (placement) {
|
||||
case "b":
|
||||
placementStyle = {
|
||||
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary + targetW}px - ${
|
||||
targetW * 0.02
|
||||
}px)`,
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
placementClassName =
|
||||
"top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
|
@ -67,32 +105,51 @@ export default function Popover(props: {
|
|||
// placementClassName = '';
|
||||
// break;
|
||||
case "rb":
|
||||
placementClassName = "top-[calc(100%+0.5rem)] translate-x-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "lt":
|
||||
placementClassName =
|
||||
"bottom-[calc(100%+0.5rem)] left-[100%] translate-x-[calc(-98%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lb":
|
||||
placementClassName =
|
||||
"top-[calc(100%+0.5rem)] left-[100%] translate-x-[calc(-98%)]";
|
||||
placementStyle = {
|
||||
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
|
||||
right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "top-[calc(100%+0.5rem)] right-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "rt":
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] translate-x-[calc(-2%)]";
|
||||
placementStyle = {
|
||||
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
|
||||
right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] right-[calc(-2%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lt":
|
||||
placementStyle = {
|
||||
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] left-[calc(-2%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lb":
|
||||
placementStyle = {
|
||||
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "top-[calc(100%+0.5rem)] left-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "t":
|
||||
default:
|
||||
placementStyle = {
|
||||
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary + targetW}px - ${
|
||||
targetW * 0.02
|
||||
}px)`,
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
placementClassName =
|
||||
"bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
}
|
||||
|
||||
const popoverCommonClass = "absolute p-2 box-border";
|
||||
|
||||
if (noArrow) {
|
||||
arrowClassName = "hidden";
|
||||
}
|
||||
|
@ -109,6 +166,12 @@ export default function Popover(props: {
|
|||
e.preventDefault();
|
||||
onShow?.(!mergedShow);
|
||||
setShow(!mergedShow);
|
||||
if (!mergedShow) {
|
||||
getRelativePosition(e.currentTarget, "");
|
||||
window.document.documentElement.style.overflow = "hidden";
|
||||
} else {
|
||||
window.document.documentElement.style.overflow = "auto";
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -119,12 +182,15 @@ export default function Popover(props: {
|
|||
<ArrowIcon color={internalBgColor} />
|
||||
</div>
|
||||
)}
|
||||
{createPortal(
|
||||
<div
|
||||
className={`${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
|
||||
style={{ zIndex: baseZIndex + 1 }}
|
||||
className={`${popoverCommonClass} ${popoverClassName}`}
|
||||
style={{ zIndex: baseZIndex + 1, ...placementStyle }}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</div>,
|
||||
popoverRoot,
|
||||
)}
|
||||
<div
|
||||
className=" fixed w-[100%] h-[100%] top-0 left-0 right-0 bottom-0"
|
||||
style={{ zIndex: baseZIndex }}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import SelectIcon from "@/app/icons/downArrowIcon.svg";
|
||||
import Popover from "@/app/components/Popover";
|
||||
import React, { useContext, useMemo, useRef } from "react";
|
||||
import useRelativePosition, {
|
||||
Orientation,
|
||||
} from "@/app/hooks/useRelativePosition";
|
||||
import List from "@/app/components/List";
|
||||
|
||||
export type Option<Value> = {
|
||||
value: Value;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export interface SearchProps<Value> {
|
||||
value?: string;
|
||||
onSelect?: (v: Value) => void;
|
||||
options?: Option<Value>[];
|
||||
inMobile?: boolean;
|
||||
}
|
||||
|
||||
const Select = <Value extends number | string>(props: SearchProps<Value>) => {
|
||||
const { value, onSelect, options = [], inMobile } = props;
|
||||
|
||||
const { isMobileScreen, selectClassName } = useContext(List.ListContext);
|
||||
|
||||
const optionsRef = useRef<Option<Value>[]>([]);
|
||||
optionsRef.current = options;
|
||||
const selectedOption = useMemo(
|
||||
() => optionsRef.current.find((o) => o.value === value),
|
||||
[value],
|
||||
);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { position, getRelativePosition } = useRelativePosition({
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
let headerH = 100;
|
||||
let baseH = position?.poi.distanceToBottomBoundary || 0;
|
||||
if (isMobileScreen) {
|
||||
headerH = 60;
|
||||
}
|
||||
if (position?.poi.relativePosition[1] === Orientation.bottom) {
|
||||
baseH = position?.poi.distanceToTopBoundary;
|
||||
}
|
||||
|
||||
const maxHeight = `${baseH - headerH}px`;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={`px-2 py-2 flex flex-col gap-1 overflow-y-auto overflow-x-hidden`}
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
{options?.map((o) => (
|
||||
<div
|
||||
key={o.value}
|
||||
className={`flex items-center p-3 gap-2 ${
|
||||
selectedOption?.value === o.value ? "bg-gray-100 rounded-md" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelect?.(o.value);
|
||||
}}
|
||||
>
|
||||
{!!o.icon && <div className="">{o.icon}</div>}
|
||||
<div className={`flex-1`}>{o.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
noArrow
|
||||
placement={
|
||||
position?.poi.relativePosition[1] !== Orientation.bottom ? "rb" : "rt"
|
||||
}
|
||||
popoverClassName="border-actions-popover border-gray-200 rounded-md shadow-actions-popover w-actions-popover bg-white"
|
||||
onShow={(e) => {
|
||||
getRelativePosition(contentRef.current!, "");
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-3 py-2 px-3 bg-gray-100 rounded-action-btn font-time text-sm-title ${selectClassName}`}
|
||||
ref={contentRef}
|
||||
>
|
||||
<div className={`flex items-center gap-2 flex-1`}>
|
||||
{!!selectedOption?.icon && (
|
||||
<div className={``}>{selectedOption?.icon}</div>
|
||||
)}
|
||||
<div className={`flex-1`}>{selectedOption?.label}</div>
|
||||
</div>
|
||||
<div className={``}>
|
||||
<SelectIcon />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
|
@ -0,0 +1,92 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { ListContext } from "../List";
|
||||
|
||||
interface SlideRangeProps {
|
||||
className?: string;
|
||||
description?: string;
|
||||
range?: {
|
||||
start?: number;
|
||||
stroke?: number;
|
||||
};
|
||||
onSlide?: (v: number) => void;
|
||||
value?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
const margin = 15;
|
||||
|
||||
export default function SlideRange(props: SlideRangeProps) {
|
||||
const {
|
||||
className = "",
|
||||
description = "",
|
||||
range = {},
|
||||
value,
|
||||
onSlide,
|
||||
step,
|
||||
} = props;
|
||||
const { start = 0, stroke = 1 } = range;
|
||||
|
||||
const { rangeClassName, update } = useContext(ListContext);
|
||||
|
||||
const slideRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const transformToWidth = (x: number = start) => {
|
||||
const abs = x - start;
|
||||
const maxWidth = (slideRef.current?.clientWidth || 1) - margin * 2;
|
||||
const radio = stroke / maxWidth;
|
||||
return abs / radio;
|
||||
};
|
||||
|
||||
const setProperty = (value?: number) => {
|
||||
const initWidth = transformToWidth(value);
|
||||
slideRef.current?.style.setProperty(
|
||||
"--slide-value-size",
|
||||
`${initWidth + margin}px`,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProperty(value);
|
||||
update?.({ type: "range" });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col justify-center items-end gap-1 w-[100%] ${className} ${rangeClassName}`}
|
||||
>
|
||||
{!!description && (
|
||||
<div className="text-text-hint text-common text-sm">{description}</div>
|
||||
)}
|
||||
<div
|
||||
className="flex my-1.5 relative w-[100%] h-1.5 bg-gray-200 rounded-slide"
|
||||
ref={slideRef}
|
||||
>
|
||||
<div className="absolute top-0 h-[100%] w-[var(--slide-value-size)] pointer-events-none bg-gray-500 rounded-slide">
|
||||
|
||||
</div>
|
||||
<div
|
||||
className=" absolute w-[30px] top-[50%] translate-y-[-50%] left-[var(--slide-value-size)] translate-x-[-50%] pointer-events-none h-slide-btn leading-slide-btn text-sm-mobile text-center rounded-slide border-[1px] border-gray-300 bg-white"
|
||||
// onPointerDown={onPointerDown}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
className="w-[100%] h-[100%] opacity-0"
|
||||
value={value}
|
||||
min={start}
|
||||
max={start + stroke}
|
||||
step={step}
|
||||
onChange={(e) => {
|
||||
setProperty(e.target.valueAsNumber);
|
||||
onSlide?.(e.target.valueAsNumber);
|
||||
}}
|
||||
style={{
|
||||
marginLeft: margin,
|
||||
marginRight: margin,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import * as RadixSwitch from "@radix-ui/react-switch";
|
||||
import { useContext } from "react";
|
||||
import List from "../List";
|
||||
|
||||
interface SwitchProps {
|
||||
value: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Switch(props: SwitchProps) {
|
||||
const { value, onChange } = props;
|
||||
|
||||
const { switchClassName = "" } = useContext(List.ListContext);
|
||||
return (
|
||||
<RadixSwitch.Root
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
className={` flex w-switch h-switch bg-gray-200 p-0.5 box-content rounded-md ${switchClassName} ${
|
||||
value ? "bg-switch-checked justify-end" : "bg-gray-200 justify-start"
|
||||
}`}
|
||||
>
|
||||
<RadixSwitch.Thumb
|
||||
className={` bg-white block w-4 h-4 drop-shadow-sm rounded-md`}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
);
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--white);
|
||||
|
||||
.auth-logo {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
@ -33,4 +35,18 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
appearance: none;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ export function ChatActions(props: {
|
|||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
placement="lt"
|
||||
placement="rt"
|
||||
noArrow
|
||||
popoverClassName="border-actions-popover border-gray-200 rounded-md shadow-actions-popover w-actions-popover bg-white "
|
||||
>
|
||||
|
@ -219,7 +219,7 @@ export function ChatActions(props: {
|
|||
key={act.text}
|
||||
content={act.text}
|
||||
popoverClassName={`${popoverClassName}`}
|
||||
placement={ind ? "t" : "rt"}
|
||||
placement={ind ? "t" : "lt"}
|
||||
>
|
||||
<div
|
||||
className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn"
|
||||
|
@ -239,7 +239,7 @@ export function ChatActions(props: {
|
|||
key={act.text}
|
||||
content={act.text}
|
||||
popoverClassName={`${popoverClassName}`}
|
||||
placement={ind === arr.length - 1 ? "lt" : "t"}
|
||||
placement={ind === arr.length - 1 ? "rt" : "t"}
|
||||
>
|
||||
<div
|
||||
className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn"
|
||||
|
|
|
@ -153,10 +153,6 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
{messages.map((message, i) => {
|
||||
const isUser = message.role === "user";
|
||||
const isContext = i < context.length;
|
||||
const showActions =
|
||||
i > 0 &&
|
||||
!(message.preview || message.content.length === 0) &&
|
||||
!isContext;
|
||||
|
||||
const shouldShowClearContextDivider = i === clearContextIndex - 1;
|
||||
|
||||
|
@ -237,7 +233,7 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
|
|||
message={message}
|
||||
inputRef={inputRef}
|
||||
isUser={isUser}
|
||||
showActions={showActions}
|
||||
isContext={isContext}
|
||||
setIsLoading={setIsLoading}
|
||||
setShowPromptModal={setShowPromptModal}
|
||||
/>
|
||||
|
|
|
@ -25,7 +25,6 @@ import ChatInputPanel, { ChatInputPanelInstance } from "./ChatInputPanel";
|
|||
import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel";
|
||||
import { useAllModels } from "@/app/utils/hooks";
|
||||
import useRows from "@/app/hooks/useRows";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import SessionConfigModel from "./SessionConfigModal";
|
||||
import useScrollToBottom from "@/app/hooks/useScrollToBottom";
|
||||
|
||||
|
@ -34,6 +33,8 @@ function _Chat() {
|
|||
const session = chatStore.currentSession();
|
||||
const config = useAppConfig();
|
||||
|
||||
const { isMobileScreen } = config;
|
||||
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||
|
||||
|
@ -44,7 +45,6 @@ function _Chat() {
|
|||
const chatInputPanelRef = useRef<ChatInputPanelInstance | null>(null);
|
||||
|
||||
const [hitBottom, setHitBottom] = useState(true);
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const [attachImages, setAttachImages] = useState<string[]>([]);
|
||||
|
||||
|
@ -295,11 +295,7 @@ function _Chat() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<PromptToast
|
||||
showToast={!hitBottom}
|
||||
showModal={showPromptModal}
|
||||
setShowModal={setShowPromptModal}
|
||||
/>
|
||||
<PromptToast showToast={!hitBottom} setShowModal={setShowPromptModal} />
|
||||
|
||||
{showPromptModal && (
|
||||
<SessionConfigModel onClose={() => setShowPromptModal(false)} />
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ContextPrompts } from "@/app/components/mask";
|
|||
|
||||
import CancelIcon from "@/app/icons/cancel.svg";
|
||||
import ConfirmIcon from "@/app/icons/confirm.svg";
|
||||
import Input from "@/app/components/Input";
|
||||
|
||||
export function EditMessageModal(props: { onClose: () => void }) {
|
||||
const chatStore = useChatStore();
|
||||
|
@ -47,15 +48,16 @@ export function EditMessageModal(props: { onClose: () => void }) {
|
|||
title={Locale.Chat.EditMessage.Topic.Title}
|
||||
subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
|
||||
>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={session.topic}
|
||||
onInput={(e) =>
|
||||
onChange={(e) =>
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.topic = e.currentTarget.value),
|
||||
(session) => (session.topic = e || ""),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
className=" text-center"
|
||||
></Input>
|
||||
</ListItem>
|
||||
</List>
|
||||
<ContextPrompts
|
||||
|
|
|
@ -23,7 +23,8 @@ export type RenderMessage = ChatMessage & { preview?: boolean };
|
|||
export interface MessageActionsProps {
|
||||
message: RenderMessage;
|
||||
isUser: boolean;
|
||||
showActions: boolean;
|
||||
isContext: boolean;
|
||||
showActions?: boolean;
|
||||
inputRef: RefObject<HTMLTextAreaElement>;
|
||||
className?: string;
|
||||
setIsLoading?: (value: boolean) => void;
|
||||
|
@ -96,12 +97,33 @@ const genActionsShema = (
|
|||
];
|
||||
};
|
||||
|
||||
enum GroupType {
|
||||
"streaming" = "streaming",
|
||||
"isContext" = "isContext",
|
||||
"normal" = "normal",
|
||||
}
|
||||
|
||||
const groupsTypes = {
|
||||
[GroupType.streaming]: [[Locale.Chat.Actions.Stop]],
|
||||
[GroupType.isContext]: [["Edit"]],
|
||||
[GroupType.normal]: [
|
||||
[
|
||||
Locale.Chat.Actions.Retry,
|
||||
"Edit",
|
||||
Locale.Chat.Actions.Copy,
|
||||
Locale.Chat.Actions.Pin,
|
||||
Locale.Chat.Actions.Delete,
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export default function MessageActions(props: MessageActionsProps) {
|
||||
const {
|
||||
className,
|
||||
message,
|
||||
isUser,
|
||||
showActions,
|
||||
isContext,
|
||||
showActions = true,
|
||||
setIsLoading,
|
||||
inputRef,
|
||||
setShowPromptModal,
|
||||
|
@ -228,6 +250,12 @@ export default function MessageActions(props: MessageActionsProps) {
|
|||
|
||||
const onCopy = () => copyToClipboard(getMessageTextContent(message));
|
||||
|
||||
const groupsType = [
|
||||
message.streaming && GroupType.streaming,
|
||||
isContext && GroupType.isContext,
|
||||
GroupType.normal,
|
||||
].find((i) => i) as GroupType;
|
||||
|
||||
return (
|
||||
showActions && (
|
||||
<div
|
||||
|
@ -254,19 +282,7 @@ export default function MessageActions(props: MessageActionsProps) {
|
|||
onResend,
|
||||
onUserStop,
|
||||
})}
|
||||
groups={
|
||||
message.streaming
|
||||
? [[Locale.Chat.Actions.Stop]]
|
||||
: [
|
||||
[
|
||||
Locale.Chat.Actions.Retry,
|
||||
"Edit",
|
||||
Locale.Chat.Actions.Copy,
|
||||
Locale.Chat.Actions.Pin,
|
||||
Locale.Chat.Actions.Delete,
|
||||
],
|
||||
]
|
||||
}
|
||||
groups={groupsTypes[groupsType]}
|
||||
className="flex flex-row gap-1 p-1"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,6 @@ import styles from "./index.module.scss";
|
|||
|
||||
export default function PromptToast(props: {
|
||||
showToast?: boolean;
|
||||
showModal?: boolean;
|
||||
setShowModal: (_: boolean) => void;
|
||||
}) {
|
||||
const chatStore = useChatStore();
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { ListItem, Modal, showConfirm } from "@/app/components/ui-lib";
|
||||
import { Modal, showConfirm } from "@/app/components/ui-lib";
|
||||
import { useChatStore } from "@/app/store/chat";
|
||||
import { useMaskStore } from "@/app/store/mask";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Locale from "@/app/locales";
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import { MaskConfig } from "@/app/components/mask";
|
||||
import { Path } from "@/app/constant";
|
||||
|
||||
import ResetIcon from "@/app/icons/reload.svg";
|
||||
import CopyIcon from "@/app/icons/copy.svg";
|
||||
import MaskConfig from "@/app/containers/Settings/MaskConfig";
|
||||
import { ListItem } from "@/app/components/List";
|
||||
|
||||
export default function SessionConfigModel(props: { onClose: () => void }) {
|
||||
const chatStore = useChatStore();
|
||||
|
|
|
@ -17,7 +17,6 @@ import { showConfirm } from "@/app/components/ui-lib";
|
|||
import AddIcon from "@/app/icons/addIcon.svg";
|
||||
import NextChatTitle from "@/app/icons/nextchatTitle.svg";
|
||||
// import { ListHoodProps } from "@/app/containers/types";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import { getTime } from "@/app/utils";
|
||||
import DeleteIcon from "@/app/icons/deleteIcon.svg";
|
||||
import LogIcon from "@/app/icons/logIcon.svg";
|
||||
|
@ -120,8 +119,10 @@ export default MenuLayout(function SessionList(props) {
|
|||
],
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const config = useAppConfig();
|
||||
|
||||
const { isMobileScreen } = config;
|
||||
|
||||
const chatStore = useChatStore();
|
||||
const { pathname: currentPath } = useLocation();
|
||||
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
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: isMobileScreen
|
||||
? "min-w-select-mobile"
|
||||
: "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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
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
|
||||
}, []);
|
||||
|
||||
const btnStyle = " !shadow-none !bg-gray-50";
|
||||
const textStyle = " !text-sm";
|
||||
|
||||
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}
|
||||
className={btnStyle}
|
||||
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}
|
||||
className={btnStyle}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
|
||||
chatStore.clearAllData();
|
||||
}
|
||||
}}
|
||||
type="danger"
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
}
|
|
@ -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/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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
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 btnStyle = " !shadow-none !bg-gray-50";
|
||||
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
|
||||
className={btnStyle}
|
||||
onClick={() => setShowPromptModal(true)}
|
||||
text={
|
||||
<span className={textStyle}>{Locale.Settings.Prompt.Edit}</span>
|
||||
}
|
||||
></Btn>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
{shouldShowPromptModal && (
|
||||
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
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 btnStyle = " !shadow-none !bg-gray-50";
|
||||
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
|
||||
className={btnStyle}
|
||||
onClick={() => {
|
||||
setShowSyncConfigModal(true);
|
||||
}}
|
||||
text={<span className={textStyle}>{Locale.UI.Config}</span>}
|
||||
></Btn>
|
||||
{couldSync && (
|
||||
<Btn
|
||||
className={btnStyle}
|
||||
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
|
||||
className={btnStyle}
|
||||
onClick={() => {
|
||||
syncStore.export();
|
||||
}}
|
||||
text={<span className={textStyle}>{Locale.UI.Export}</span>}
|
||||
></Btn>
|
||||
<Btn
|
||||
className={btnStyle}
|
||||
onClick={async () => {
|
||||
syncStore.import();
|
||||
}}
|
||||
text={<span className={textStyle}>{Locale.UI.Import}</span>}
|
||||
></Btn>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{showSyncConfigModal && (
|
||||
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,3 @@
|
|||
.settings {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import Locale from "@/app/locales";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import MenuLayout from "@/app/components/MenuLayout";
|
||||
|
||||
import Panel from "./SettingPanel";
|
||||
|
||||
import GotoIcon from "@/app/icons/goto.svg";
|
||||
import { useAppConfig } from "@/app/store";
|
||||
|
||||
export default MenuLayout(function SettingList(props) {
|
||||
const { setShowPanel } = props;
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const config = useAppConfig();
|
||||
|
||||
const { isMobileScreen } = config;
|
||||
|
||||
let layoutClassName = "pt-7 px-4";
|
||||
let titleClassName = "pb-5";
|
||||
|
|
|
@ -16,7 +16,6 @@ import { useAppConfig } from "@/app/store";
|
|||
import { Path, REPO_URL } from "@/app/constant";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import useHotKey from "@/app/hooks/useHotKey";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import ActionsBar from "@/app/components/ActionsBar";
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
|
@ -24,7 +23,7 @@ export function SideBar(props: { className?: string }) {
|
|||
const loc = useLocation();
|
||||
|
||||
const config = useAppConfig();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const { isMobileScreen } = config;
|
||||
|
||||
useHotKey();
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.discover-assistant-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
fill: var(--light-opacity-white-60, rgba(255, 255, 255, 0.60));
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import styles from "./index.module.scss";
|
||||
import { useMobileScreen } from "@/app/utils";
|
||||
import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "@/app/locales";
|
||||
|
||||
import Search from "@/app/components/Search";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
interface Filter {
|
||||
assistantKeyword?: string;
|
||||
}
|
||||
|
||||
export default function DiscoverAssistant() {
|
||||
const navigate = useNavigate();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const [filter, setFilter] = useState<Filter>();
|
||||
|
||||
const filteredAssistant = useMemo(() => {}, [filter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["discover-assistant-container"]}>
|
||||
<div className={styles["discover-assistant-container-title"]}></div>
|
||||
<div className={styles["discover-assistant-container-subtitle"]}></div>
|
||||
<div className={styles["discover-assistant-container-search"]}>
|
||||
<Search
|
||||
value={filter?.assistantKeyword}
|
||||
onSearch={(keyword) => {
|
||||
setFilter((pre) => ({ ...pre, keyword }));
|
||||
}}
|
||||
placeholder={Locale.Discover.SearchPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
require("../polyfill");
|
||||
|
||||
import { HashRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useLayoutEffect } from "react";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { Path } from "@/app/constant";
|
||||
|
@ -12,12 +12,13 @@ import { getISOLang } from "@/app/locales";
|
|||
import { useSwitchTheme } from "@/app/hooks/useSwitchTheme";
|
||||
import { AuthPage } from "@/app/components/auth";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { useAccessStore } from "@/app/store";
|
||||
import { useAccessStore, useAppConfig } from "@/app/store";
|
||||
import { useLoadData } from "@/app/hooks/useLoadData";
|
||||
import Loading from "@/app/components/Loading";
|
||||
import Screen from "@/app/components/Screen";
|
||||
import { SideBar } from "./Sidebar";
|
||||
import GlobalLoading from "@/app/components/GlobalLoading";
|
||||
import { MOBILE_MAX_WIDTH } from "../hooks/useListenWinResize";
|
||||
|
||||
const Settings = dynamic(
|
||||
async () => await import("@/app/containers/Settings"),
|
||||
|
@ -84,14 +85,19 @@ export default function Home() {
|
|||
useSwitchTheme();
|
||||
useLoadData();
|
||||
useHtmlLang();
|
||||
const config = useAppConfig();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Config] got config from build time", getClientConfig());
|
||||
useAccessStore.getState().fetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
loadAsyncGoogleFont();
|
||||
config.update(
|
||||
(config) =>
|
||||
(config.isMobileScreen = window.innerWidth <= MOBILE_MAX_WIDTH),
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!useHasHydrated()) {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { RefObject, useRef } from "react";
|
||||
|
||||
export default function useDrag(options: {
|
||||
customDragMove: (nextWidth: number, start?: number) => void;
|
||||
customToggle: () => void;
|
||||
customLimit?: (x: number, start?: number) => number;
|
||||
customDragEnd?: (nextWidth: number, start?: number) => void;
|
||||
}) {
|
||||
const { customDragMove, customToggle, customLimit, customDragEnd } =
|
||||
options || {};
|
||||
const limit = customLimit;
|
||||
|
||||
const startX = useRef(0);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const toggleSideBar = customToggle;
|
||||
|
||||
const onDragMove = customDragMove;
|
||||
|
||||
const onDragStart = (e: MouseEvent) => {
|
||||
// Remembers the initial width each time the mouse is pressed
|
||||
startX.current = e.clientX;
|
||||
const dragStartTime = Date.now();
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 20) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit?.(d, startX.current) ?? d;
|
||||
|
||||
onDragMove(nextWidth, startX.current);
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: MouseEvent) => {
|
||||
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
||||
window.removeEventListener("pointermove", handleDragMove);
|
||||
window.removeEventListener("pointerup", handleDragEnd);
|
||||
|
||||
// if user click the drag icon, should toggle the sidebar
|
||||
const shouldFireClick = Date.now() - dragStartTime < 300;
|
||||
if (shouldFireClick) {
|
||||
toggleSideBar();
|
||||
} else {
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit?.(d, startX.current) ?? d;
|
||||
customDragEnd?.(nextWidth, startX.current);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleDragMove);
|
||||
window.addEventListener("pointerup", handleDragEnd);
|
||||
};
|
||||
|
||||
return {
|
||||
onDragStart,
|
||||
};
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import {
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
} from "@/app/constant";
|
||||
import { useAppConfig } from "@/app/store/config";
|
||||
import { useRef } from "react";
|
||||
import { updateGlobalCSSVars } from "@/app/utils/client";
|
||||
|
||||
export default function useDragSideBar() {
|
||||
const limit = (x: number) =>
|
||||
Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, x));
|
||||
|
||||
const config = useAppConfig();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const toggleSideBar = () => {
|
||||
config.update((config) => {
|
||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
});
|
||||
};
|
||||
|
||||
const onDragStart = (e: MouseEvent) => {
|
||||
// Remembers the initial width each time the mouse is pressed
|
||||
startX.current = e.clientX;
|
||||
startDragWidth.current = config.sidebarWidth;
|
||||
const dragStartTime = Date.now();
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 20) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
|
||||
const { menuWidth } = updateGlobalCSSVars(nextWidth);
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--menu-width",
|
||||
`${menuWidth}px`,
|
||||
);
|
||||
config.update((config) => {
|
||||
config.sidebarWidth = nextWidth;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
||||
window.removeEventListener("pointermove", handleDragMove);
|
||||
window.removeEventListener("pointerup", handleDragEnd);
|
||||
|
||||
// if user click the drag icon, should toggle the sidebar
|
||||
const shouldFireClick = Date.now() - dragStartTime < 300;
|
||||
if (shouldFireClick) {
|
||||
toggleSideBar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleDragMove);
|
||||
window.addEventListener("pointerup", handleDragEnd);
|
||||
};
|
||||
|
||||
// useLayoutEffect(() => {
|
||||
// const barWidth = limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
// document.documentElement.style.setProperty("--menu-width", `${barWidth}px`);
|
||||
// }, [config.sidebarWidth]);
|
||||
|
||||
return {
|
||||
onDragStart,
|
||||
};
|
||||
}
|
|
@ -8,7 +8,6 @@ import {
|
|||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
SIDEBAR_ID,
|
||||
} from "@/app/constant";
|
||||
import { useAppConfig } from "@/app/store/config";
|
||||
import { updateGlobalCSSVars } from "@/app/utils/client";
|
||||
|
@ -49,5 +48,8 @@ export default function useListenWinResize() {
|
|||
config.update((config) => {
|
||||
config.sidebarWidth = menuWidth;
|
||||
});
|
||||
config.update((config) => {
|
||||
config.isMobileScreen = size.width <= MOBILE_MAX_WIDTH;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useWindowSize } from "@/app/hooks/useWindowSize";
|
||||
|
||||
export const MOBILE_MAX_WIDTH = 768;
|
||||
import { MOBILE_MAX_WIDTH } from "@/app/hooks/useListenWinResize";
|
||||
|
||||
export default function useMobileScreen() {
|
||||
const { width } = useWindowSize();
|
||||
|
|
|
@ -2,7 +2,7 @@ import { RefObject, useState } from "react";
|
|||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
export interface Options {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
containerRef?: RefObject<HTMLElement | null>;
|
||||
delay?: number;
|
||||
offsetDistance?: number;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ interface Position {
|
|||
}
|
||||
|
||||
export default function useRelativePosition({
|
||||
containerRef,
|
||||
containerRef = { current: window.document.body },
|
||||
delay = 100,
|
||||
offsetDistance = 0,
|
||||
}: Options) {
|
||||
|
@ -49,6 +49,7 @@ export default function useRelativePosition({
|
|||
width: targetW,
|
||||
height: targetH,
|
||||
} = target.getBoundingClientRect();
|
||||
|
||||
const {
|
||||
x: containerX,
|
||||
y: containerY,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { autoGrowTextArea } from "../utils";
|
||||
import useMobileScreen from "./useMobileScreen";
|
||||
import { useAppConfig } from "../store";
|
||||
|
||||
export default function useRows({
|
||||
inputRef,
|
||||
|
@ -9,14 +9,14 @@ export default function useRows({
|
|||
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}) {
|
||||
const [inputRows, setInputRows] = useState(2);
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const config = useAppConfig();
|
||||
|
||||
const measure = useDebouncedCallback(
|
||||
() => {
|
||||
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
||||
const inputRows = Math.min(
|
||||
20,
|
||||
Math.max(2 + (isMobileScreen ? -1 : 1), rows),
|
||||
Math.max(2 + (config.isMobileScreen ? -1 : 1), rows),
|
||||
);
|
||||
setInputRows(inputRows);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.91496 1.69334C7.36752 2.11888 7.25215 2.75585 7.05615 3.20591L7.05576 3.20682C6.76923 3.86173 6.61177 4.57947 6.61761 5.33877L6.61763 5.34085C6.62929 8.15802 8.97294 10.5654 11.8442 10.6828L11.8446 10.6828C12.2688 10.7005 12.6741 10.6711 13.0675 10.6007C13.3386 10.5513 13.6087 10.5418 13.8548 10.5992C14.1063 10.6578 14.3642 10.7963 14.525 11.0595C14.6852 11.3216 14.6914 11.6128 14.6324 11.861C14.5744 12.1051 14.4476 12.3415 14.2854 12.5613C12.887 14.4744 10.5595 15.6853 7.96279 15.5727L7.96243 15.5727C4.27649 15.4106 1.1912 12.463 0.935785 8.81417C0.702559 5.58027 2.62215 2.76506 5.4097 1.58951C5.85844 1.39947 6.48064 1.28494 6.91496 1.69334ZM6.03358 2.60725C5.98613 2.61983 5.92991 2.63882 5.86504 2.66633L5.864 2.66677C3.50753 3.66053 1.90679 6.02819 2.10194 8.73064L2.10203 8.73198C2.3146 11.7729 4.90597 14.268 8.0138 14.4047C10.2099 14.4998 12.1686 13.4768 13.3423 11.8703L13.3441 11.8679C13.3819 11.8167 13.4114 11.7709 13.4342 11.7308C13.3894 11.7337 13.3369 11.74 13.2762 11.751L13.2744 11.7514C12.7978 11.8367 12.3071 11.8722 11.7964 11.851C8.31568 11.7086 5.46338 8.80347 5.4485 5.34673C5.4415 4.41787 5.63468 3.53833 5.98441 2.73875C6.00608 2.68894 6.02205 2.64512 6.03358 2.60725Z" fill="#18182A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.16681 3.65466C1.34891 3.47064 1.6457 3.46909 1.82972 3.65119L5.67028 7.45175C5.85294 7.6325 6.14706 7.6325 6.32972 7.45175L10.1703 3.65119C10.3543 3.46909 10.6511 3.47064 10.8332 3.65466C11.0153 3.83867 11.0137 4.13546 10.8297 4.31756L6.98915 8.11812C6.44119 8.66037 5.55881 8.66038 5.01085 8.11812L1.17028 4.31756C0.986269 4.13546 0.984716 3.83867 1.16681 3.65466Z" fill="#A5A5B3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 539 B |
|
@ -0,0 +1,4 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99998 4.26665C5.93812 4.26665 4.26665 5.93812 4.26665 7.99998C4.26665 10.0618 5.93812 11.7333 7.99998 11.7333C10.0618 11.7333 11.7333 10.0618 11.7333 7.99998C11.7333 5.93812 10.0618 4.26665 7.99998 4.26665ZM3.06665 7.99998C3.06665 5.27538 5.27538 3.06665 7.99998 3.06665C10.7246 3.06665 12.9333 5.27538 12.9333 7.99998C12.9333 10.7246 10.7246 12.9333 7.99998 12.9333C5.27538 12.9333 3.06665 10.7246 3.06665 7.99998Z" fill="#18182A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99994 0.733398C8.33131 0.733398 8.59994 1.00203 8.59994 1.3334V1.38673C8.59994 1.7181 8.33131 1.98673 7.99994 1.98673C7.66857 1.98673 7.39994 1.7181 7.39994 1.38673V1.3334C7.39994 1.00203 7.66857 0.733398 7.99994 0.733398ZM2.81568 2.8158C3.04999 2.58149 3.42989 2.58149 3.66421 2.8158L3.75087 2.90247C3.98519 3.13678 3.98519 3.51668 3.75087 3.751C3.51656 3.98531 3.13666 3.98531 2.90235 3.751L2.81568 3.66433C2.58136 3.43001 2.58136 3.05012 2.81568 2.8158ZM13.1842 2.8158C13.4185 3.05011 13.4185 3.43001 13.1842 3.66433L13.0975 3.751C12.8632 3.98531 12.4833 3.98531 12.249 3.751C12.0147 3.51668 12.0147 3.13678 12.249 2.90247L12.3357 2.8158C12.57 2.58149 12.9499 2.58149 13.1842 2.8158ZM0.733276 8.00006C0.733276 7.66869 1.00191 7.40007 1.33328 7.40007H1.38661C1.71798 7.40007 1.98661 7.66869 1.98661 8.00006C1.98661 8.33144 1.71798 8.60006 1.38661 8.60006H1.33328C1.00191 8.60006 0.733276 8.33144 0.733276 8.00006ZM14.0133 8.00006C14.0133 7.66869 14.2819 7.40007 14.6133 7.40007H14.6666C14.998 7.40007 15.2666 7.66869 15.2666 8.00006C15.2666 8.33144 14.998 8.60006 14.6666 8.60006H14.6133C14.2819 8.60006 14.0133 8.33144 14.0133 8.00006ZM3.75087 12.2491C3.98519 12.4835 3.98519 12.8633 3.75087 13.0977L3.66421 13.1843C3.42989 13.4186 3.04999 13.4186 2.81568 13.1843C2.58136 12.95 2.58136 12.5701 2.81568 12.3358L2.90235 12.2491C3.13666 12.0148 3.51656 12.0148 3.75087 12.2491ZM12.249 12.2491C12.4833 12.0148 12.8632 12.0148 13.0975 12.2491L13.1842 12.3358C13.4185 12.5701 13.4185 12.95 13.1842 13.1843C12.9499 13.4186 12.57 13.4186 12.3357 13.1843L12.249 13.0977C12.0147 12.8633 12.0147 12.4834 12.249 12.2491ZM7.99994 14.0134C8.33131 14.0134 8.59994 14.282 8.59994 14.6134V14.6667C8.59994 14.9981 8.33131 15.2667 7.99994 15.2667C7.66857 15.2667 7.39994 14.9981 7.39994 14.6667V14.6134C7.39994 14.282 7.66857 14.0134 7.99994 14.0134Z" fill="#18182A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.70887 1.86152C3.44038 1.59303 3.00508 1.59303 2.7366 1.86152C2.46811 2.13001 2.46811 2.56531 2.7366 2.83379L3.61941 3.7166C2.29621 4.83906 1.48662 6.26092 1.09421 7.07648C0.811834 7.66332 0.812396 8.33899 1.09463 8.92529C1.45359 9.67099 2.1548 10.9094 3.26892 11.9687C4.39043 13.0349 5.95193 13.9374 8.00001 13.9374C9.78251 13.9374 11.1979 13.2534 12.2747 12.3719L13.2366 13.3338C13.5051 13.6023 13.9404 13.6023 14.2089 13.3338C14.4774 13.0653 14.4774 12.63 14.2089 12.3615L3.70887 1.86152ZM11.3856 11.4828L10.1163 10.2135C9.56695 10.7388 8.82117 11.0624 8.00001 11.0624C6.30864 11.0624 4.93751 9.69131 4.93751 7.99994C4.93751 7.17879 5.26119 6.433 5.78643 5.88363L4.50681 4.60401C3.3311 5.57087 2.58821 6.85446 2.2206 7.61846C2.10331 7.86222 2.10336 8.13888 2.22093 8.38312C2.54536 9.05708 3.16779 10.1477 4.1302 11.0627C5.08523 11.9707 6.35315 12.6874 8.00001 12.6874C9.38374 12.6874 10.4986 12.1818 11.3856 11.4828ZM6.51577 2.22912C6.98137 2.12171 7.4761 2.06244 8.00001 2.06244C10.0482 2.06244 11.6097 2.96525 12.7312 4.03174C13.8454 5.09121 14.5466 6.32994 14.9055 7.07576C15.1877 7.66226 15.1879 8.33839 14.9055 8.92506C14.7402 9.26846 14.5047 9.71135 14.1927 10.1912C14.0046 10.4806 13.6175 10.5627 13.3281 10.3746C13.0387 10.1864 12.9566 9.79932 13.1447 9.50993C13.4225 9.08268 13.6325 8.6877 13.7792 8.38291C13.8966 8.13902 13.8966 7.86191 13.7791 7.61782C13.4547 6.94373 12.8323 5.85279 11.8699 4.93756C10.9148 4.02935 9.64688 3.31244 8.00001 3.31244C7.5712 3.31244 7.17073 3.36085 6.79676 3.44713C6.46041 3.52472 6.12485 3.31496 6.04726 2.97862C5.96967 2.64227 6.17943 2.30671 6.51577 2.22912ZM6.508 6.97036C6.30559 7.26299 6.18751 7.6176 6.18751 7.99994C6.18751 9.00096 6.999 9.81244 8.00001 9.81244C8.38235 9.81244 8.73697 9.69436 9.02959 9.49196L6.508 6.97036Z" fill="#88889A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.26876 4.0318C4.39025 2.96531 5.9518 2.0625 7.99999 2.0625C10.0482 2.0625 11.6097 2.96531 12.7312 4.0318C13.8453 5.09127 14.5465 6.33001 14.9055 7.07582C15.1877 7.66241 15.1877 8.33878 14.9054 8.92535C14.5464 9.67105 13.8452 10.9095 12.7311 11.9687C11.6096 13.035 10.0481 13.9375 7.99999 13.9375C5.9519 13.9375 4.3904 13.035 3.26889 11.9687C2.15477 10.9095 1.45356 9.67105 1.0946 8.92535C0.812237 8.33878 0.812223 7.66241 1.09452 7.07582C1.45345 6.33001 2.15464 5.09127 3.26876 4.0318ZM4.13015 4.93762C3.16771 5.85285 2.54528 6.94379 2.22087 7.61789C2.10345 7.86188 2.10346 8.13922 2.2209 8.38318C2.54533 9.05714 3.16776 10.1478 4.13017 11.0628C5.0852 11.9708 6.35312 12.6875 7.99999 12.6875C9.64685 12.6875 10.9148 11.9708 11.8698 11.0628C12.8322 10.1478 13.4546 9.05714 13.7791 8.38318C13.8965 8.13922 13.8965 7.86188 13.7791 7.61789C13.4547 6.94379 12.8323 5.85285 11.8698 4.93762C10.9148 4.02941 9.64685 3.3125 7.99999 3.3125C6.35312 3.3125 5.0852 4.02941 4.13015 4.93762ZM7.99999 6.1875C6.99897 6.1875 6.18749 6.99898 6.18749 8C6.18749 9.00102 6.99897 9.8125 7.99999 9.8125C9.001 9.8125 9.81249 9.00102 9.81249 8C9.81249 6.99898 9.001 6.1875 7.99999 6.1875ZM4.93749 8C4.93749 6.30863 6.30861 4.9375 7.99999 4.9375C9.69136 4.9375 11.0625 6.30863 11.0625 8C11.0625 9.69137 9.69136 11.0625 7.99999 11.0625C6.30861 11.0625 4.93749 9.69137 4.93749 8Z" fill="#88889A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,6 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.35004 2.35016C2.09657 2.60363 1.93328 3.10612 1.93328 4.2934V8.52006C1.93328 9.70763 2.09668 10.2096 2.34964 10.4621C2.60243 10.7144 3.10422 10.8767 4.29159 10.8734L4.29328 10.8734L11.7066 10.8734C12.895 10.8734 13.3971 10.7114 13.6502 10.4587C13.9031 10.2063 14.0666 9.70458 14.0666 8.5134V4.2934C14.0666 3.10606 13.9033 2.60395 13.6494 2.35056C13.3951 2.09676 12.8908 1.9334 11.6999 1.9334H4.29328C3.106 1.9334 2.60351 2.09669 2.35004 2.35016ZM1.50151 1.50163C2.13638 0.866771 3.11389 0.733398 4.29328 0.733398H11.6999C12.8824 0.733398 13.8614 0.866702 14.4971 1.50124C15.1333 2.13618 15.2666 3.11407 15.2666 4.2934V8.5134C15.2666 9.69555 15.1334 10.6738 14.498 11.3081C13.8628 11.942 12.8849 12.0734 11.7066 12.0734H4.29405C3.11516 12.0767 2.1373 11.9456 1.50191 11.3114C0.866538 10.6772 0.733276 9.69917 0.733276 8.52006V4.2934C0.733276 3.11401 0.866649 2.1365 1.50151 1.50163Z" fill="#18182A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.9999 10.8801C8.33127 10.8801 8.5999 11.1488 8.5999 11.4801V14.6668C8.5999 14.9982 8.33127 15.2668 7.9999 15.2668C7.66853 15.2668 7.3999 14.9982 7.3999 14.6668V11.4801C7.3999 11.1488 7.66853 10.8801 7.9999 10.8801Z" fill="#18182A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.733276 8.66665C0.733276 8.33528 1.00191 8.06665 1.33328 8.06665H14.6666C14.998 8.06665 15.2666 8.33528 15.2666 8.66665C15.2666 8.99802 14.998 9.26665 14.6666 9.26665H1.33328C1.00191 9.26665 0.733276 8.99802 0.733276 8.66665Z" fill="#18182A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.3999 14.6667C4.3999 14.3353 4.66853 14.0667 4.9999 14.0667H10.9999C11.3313 14.0667 11.5999 14.3353 11.5999 14.6667C11.5999 14.998 11.3313 15.2667 10.9999 15.2667H4.9999C4.66853 15.2667 4.3999 14.998 4.3999 14.6667Z" fill="#18182A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -129,7 +129,11 @@ const cn = {
|
|||
Title: "设置",
|
||||
SubTitle: "所有设置选项",
|
||||
GeneralSettings: "通用设置",
|
||||
Basic: {
|
||||
Title: "基础设置",
|
||||
},
|
||||
Danger: {
|
||||
Title: "系统设置",
|
||||
Reset: {
|
||||
Title: "重置所有设置",
|
||||
SubTitle: "重置所有设置项回默认值",
|
||||
|
@ -181,6 +185,7 @@ const cn = {
|
|||
SubTitle: "根据对话内容生成合适的标题",
|
||||
},
|
||||
Sync: {
|
||||
Title: "数据设置",
|
||||
CloudState: "云端数据",
|
||||
NotSyncYet: "还没有进行过同步",
|
||||
Success: "同步成功",
|
||||
|
@ -224,6 +229,7 @@ const cn = {
|
|||
ImportFailed: "导入失败",
|
||||
},
|
||||
Mask: {
|
||||
Title: "面具设置",
|
||||
Splash: {
|
||||
Title: "面具启动页",
|
||||
SubTitle: "新建聊天时,展示面具启动页",
|
||||
|
@ -234,6 +240,7 @@ const cn = {
|
|||
},
|
||||
},
|
||||
Prompt: {
|
||||
Title: "提示语设置",
|
||||
Disable: {
|
||||
Title: "禁用提示词自动补全",
|
||||
SubTitle: "在输入框开头输入 / 即可触发自动补全",
|
||||
|
@ -271,6 +278,7 @@ const cn = {
|
|||
},
|
||||
|
||||
Access: {
|
||||
title: "接口设置",
|
||||
AccessCode: {
|
||||
Title: "访问密码",
|
||||
SubTitle: "管理员已开启加密访问",
|
||||
|
@ -352,7 +360,9 @@ const cn = {
|
|||
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
||||
},
|
||||
},
|
||||
|
||||
Models: {
|
||||
Title: "模型设置",
|
||||
},
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
|
|
|
@ -132,7 +132,11 @@ const en: LocaleType = {
|
|||
Title: "Settings",
|
||||
SubTitle: "All Settings",
|
||||
GeneralSettings: "General settings",
|
||||
Basic: {
|
||||
Title: "Basic Settings",
|
||||
},
|
||||
Danger: {
|
||||
Title: "System Settings",
|
||||
Reset: {
|
||||
Title: "Reset All Settings",
|
||||
SubTitle: "Reset all setting items to default",
|
||||
|
@ -184,6 +188,7 @@ const en: LocaleType = {
|
|||
SubTitle: "Generate a suitable title based on the conversation content",
|
||||
},
|
||||
Sync: {
|
||||
Title: "Data Settings",
|
||||
CloudState: "Last Update",
|
||||
NotSyncYet: "Not sync yet",
|
||||
Success: "Sync Success",
|
||||
|
@ -228,6 +233,7 @@ const en: LocaleType = {
|
|||
ImportFailed: "Failed to import from file",
|
||||
},
|
||||
Mask: {
|
||||
Title: "Mask Settings",
|
||||
Splash: {
|
||||
Title: "Mask Splash Screen",
|
||||
SubTitle: "Show a mask splash screen before starting new chat",
|
||||
|
@ -238,6 +244,7 @@ const en: LocaleType = {
|
|||
},
|
||||
},
|
||||
Prompt: {
|
||||
Title: "Prompt Settings",
|
||||
Disable: {
|
||||
Title: "Disable auto-completion",
|
||||
SubTitle: "Input / to trigger auto-completion",
|
||||
|
@ -275,6 +282,7 @@ const en: LocaleType = {
|
|||
NoAccess: "Enter API Key to check balance",
|
||||
},
|
||||
Access: {
|
||||
title: "API Settings",
|
||||
AccessCode: {
|
||||
Title: "Access Code",
|
||||
SubTitle: "Access control Enabled",
|
||||
|
@ -357,7 +365,9 @@ const en: LocaleType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
Models: {
|
||||
Title: "Model Settings",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { LLMModel } from "../client/api";
|
||||
import { isMacOS } from "../utils";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
|
@ -8,6 +7,9 @@ import {
|
|||
StoreKey,
|
||||
} from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import System from "@/app/icons/systemIcon.svg";
|
||||
import Light from "@/app/icons/lightIcon.svg";
|
||||
import Dark from "@/app/icons/darkIcon.svg";
|
||||
|
||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||
|
||||
|
@ -25,6 +27,21 @@ export enum Theme {
|
|||
Light = "light",
|
||||
}
|
||||
|
||||
export const ThemeConfig = {
|
||||
[Theme.Auto]: {
|
||||
icon: System,
|
||||
title: "Follow System",
|
||||
},
|
||||
[Theme.Light]: {
|
||||
icon: Light,
|
||||
title: "Light model",
|
||||
},
|
||||
[Theme.Dark]: {
|
||||
icon: Dark,
|
||||
title: "Dark model",
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
lastUpdate: Date.now(), // timestamp, to merge state
|
||||
|
||||
|
@ -45,6 +62,8 @@ export const DEFAULT_CONFIG = {
|
|||
customModels: "",
|
||||
models: DEFAULT_MODELS as any as LLMModel[],
|
||||
|
||||
isMobileScreen: false,
|
||||
|
||||
modelConfig: {
|
||||
model: "gpt-3.5-turbo" as ModelType,
|
||||
temperature: 0.5,
|
||||
|
|
|
@ -30,3 +30,8 @@ body {
|
|||
--siderbar-mobile-height: 3.125rem;
|
||||
--max-message-width: calc(var(--chat-panel-max-width) * 0.6);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
|
|
@ -144,10 +144,10 @@ label {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
}
|
||||
// input {
|
||||
// text-align: center;
|
||||
// font-family: inherit;
|
||||
// }
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
|
@ -219,20 +219,20 @@ input[type="range"]::-ms-thumb:hover {
|
|||
@include thumbHover();
|
||||
}
|
||||
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
appearance: none;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
font-family: inherit;
|
||||
}
|
||||
// input[type="number"],
|
||||
// input[type="text"],
|
||||
// input[type="password"] {
|
||||
// appearance: none;
|
||||
// border-radius: 10px;
|
||||
// border: var(--border-in-light);
|
||||
// min-height: 36px;
|
||||
// box-sizing: border-box;
|
||||
// background: var(--white);
|
||||
// color: var(--black);
|
||||
// padding: 0 10px;
|
||||
// max-width: 50%;
|
||||
// font-family: inherit;
|
||||
// }
|
||||
|
||||
div.math {
|
||||
overflow-x: auto;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"@fortaine/fetch-event-source": "^3.0.6",
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
|
|
|
@ -16,10 +16,12 @@ module.exports = {
|
|||
},
|
||||
fontWeight: {
|
||||
'setting-title': '700',
|
||||
'setting-card-title': '600',
|
||||
},
|
||||
fontFamily: {
|
||||
'common': ['Satoshi Variable', 'Variable'],
|
||||
'time': ['Hind', 'Variable']
|
||||
'time': ['Hind', 'Variable'],
|
||||
'setting-card-title': ['PingFang HK', 'PingFang']
|
||||
},
|
||||
screens: {
|
||||
sm: '480px',
|
||||
|
@ -29,6 +31,10 @@ module.exports = {
|
|||
'2xl': '1980px'
|
||||
},
|
||||
extend: {
|
||||
lineHeight: {
|
||||
'slide-btn': "17px",
|
||||
'input': '22px',
|
||||
},
|
||||
backdropBlur: {
|
||||
'chat-header': '20px',
|
||||
},
|
||||
|
@ -36,6 +42,14 @@ module.exports = {
|
|||
'chat-input-mobile': '19px',
|
||||
'chat-input': '60px',
|
||||
},
|
||||
minWidth: {
|
||||
'select-mobile-lg': '200px',
|
||||
'select-mobile': '170px',
|
||||
'select': '240px',
|
||||
'slide-range-mobile-lg': '200px',
|
||||
'slide-range-mobile': '170px',
|
||||
'slide-range': '240px',
|
||||
},
|
||||
width: {
|
||||
'md': '15rem',
|
||||
'lg': '21.25rem',
|
||||
|
@ -43,6 +57,7 @@ module.exports = {
|
|||
'page': 'calc(100% - var(--menu-width))',
|
||||
'thumbnail': '5rem',
|
||||
'actions-popover': '203px',
|
||||
'switch': '2.25rem',
|
||||
},
|
||||
height: {
|
||||
mobile: 'var(--siderbar-mobile-height)',
|
||||
|
@ -53,6 +68,8 @@ module.exports = {
|
|||
'chat-input': '60px',
|
||||
'chat-panel-mobile': '- var(--siderbar-mobile-height)',
|
||||
'setting-panel-mobile': 'calc(100vh - var(--siderbar-mobile-height))',
|
||||
'slide-btn': '18px',
|
||||
'switch': '1rem',
|
||||
},
|
||||
flexBasis: {
|
||||
'sidebar': 'var(--menu-width)',
|
||||
|
@ -75,12 +92,14 @@ module.exports = {
|
|||
maxHeight: {},
|
||||
maxWidth: {
|
||||
'message-width': 'var(--max-message-width)',
|
||||
'setting-list': '710px',
|
||||
},
|
||||
backgroundColor: {
|
||||
'select-btn': 'rgba(0, 0, 0, 0.05)',
|
||||
'chat-actions-popover-color': 'var(--tip-popover-color)',
|
||||
'chat-panel': 'var(--chat-panel-bg)',
|
||||
'global': '#E3E3ED',
|
||||
'switch-checked': '#2E42F3',
|
||||
},
|
||||
boxShadow: {
|
||||
'btn': '0px 4px 10px 0px rgba(60, 68, 255, 0.14)',
|
||||
|
@ -88,7 +107,11 @@ module.exports = {
|
|||
'actions-popover': '0px 14px 40px 0px rgba(0, 0, 0, 0.12)',
|
||||
'actions-bar': '0px 4px 30px 0px rgba(0, 0, 0, 0.10)',
|
||||
'prompt-hint-container': 'inset 0 4px 8px 0 rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
'text-hint': '#A5A5B3',
|
||||
'text-danger': '#FF5454',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
'none': '0',
|
||||
|
@ -102,6 +125,7 @@ module.exports = {
|
|||
'actions-bar-btn': '0.375rem',
|
||||
'chat-input': '0.5rem',
|
||||
'chat-img': '0.5rem',
|
||||
'slide': '0.625rem',
|
||||
},
|
||||
borderWidth: {
|
||||
DEFAULT: '1px',
|
||||
|
|
95
yarn.lock
95
yarn.lock
|
@ -1040,6 +1040,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.13.10":
|
||||
version "7.24.4"
|
||||
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
|
||||
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.18.10", "@babel/template@^7.20.7":
|
||||
version "7.20.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
|
||||
|
@ -1330,6 +1337,94 @@
|
|||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.0.tgz#7d8dacb7fdef0e4387caf7396cbd77f179867d06"
|
||||
integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==
|
||||
|
||||
"@radix-ui/primitive@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
|
||||
integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-compose-refs@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989"
|
||||
integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-context@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c"
|
||||
integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-primitive@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
|
||||
integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
|
||||
"@radix-ui/react-slot@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
|
||||
integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
|
||||
"@radix-ui/react-switch@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e"
|
||||
integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
|
||||
integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-controllable-state@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286"
|
||||
integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
|
||||
"@radix-ui/react-use-layout-effect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399"
|
||||
integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66"
|
||||
integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-size@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
|
||||
integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
|
||||
"@remix-run/router@1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||
|
|
Loading…
Reference in New Issue