feat: redesign settings page

This commit is contained in:
butterfly 2024-04-24 15:44:24 +08:00
parent f7074bba8c
commit c99086447e
55 changed files with 2603 additions and 1446 deletions

View File

@ -2,11 +2,11 @@ import * as React from "react";
export type ButtonType = "primary" | "danger" | null; export type ButtonType = "primary" | "danger" | null;
export default function IconButton(props: { export default function Btn(props: {
onClick?: () => void; onClick?: () => void;
icon?: JSX.Element; icon?: JSX.Element;
type?: ButtonType; type?: ButtonType;
text?: string; text?: React.ReactNode;
bordered?: boolean; bordered?: boolean;
shadow?: boolean; shadow?: boolean;
className?: string; className?: string;
@ -20,8 +20,6 @@ export default function IconButton(props: {
icon, icon,
type, type,
text, text,
bordered,
shadow,
className, className,
title, title,
disabled, disabled,
@ -29,18 +27,30 @@ export default function IconButton(props: {
autoFocus, autoFocus,
} = props; } = 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 ( return (
<button <button
className={` className={`
${className ?? ""} ${className ?? ""}
py-2 px-3 flex items-center justify-center gap-1 rounded-action-btn shadow-btn transition-all duration-300 select-none 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"} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}
${type === "primary" ? `text-white` : `text-gray-500`} ${btnClassName}
`} `}
onClick={onClick} onClick={onClick}
title={title} title={title}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -1,16 +1,17 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Path } from "@/app/constant";
import useDragSideBar from "@/app/hooks/useDragSideBar";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import { import {
ComponentType, DEFAULT_SIDEBAR_WIDTH,
Context, MAX_SIDEBAR_WIDTH,
createContext, MIN_SIDEBAR_WIDTH,
useContext, Path,
useState, } from "@/app/constant";
} from "react"; 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 DragIcon from "@/app/icons/drag.svg";
import { useAppConfig } from "@/app/store/config";
export interface MenuWrapperInspectProps { export interface MenuWrapperInspectProps {
setShowPanel?: (v: boolean) => void; setShowPanel?: (v: boolean) => void;
@ -28,10 +29,35 @@ export default function MenuLayout<
const [showPanel, setShowPanel] = useState(false); const [showPanel, setShowPanel] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const config = useAppConfig();
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
// drag side bar // 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 containerClassName = "flex h-[100%] w-[100%]";
let listClassName = let listClassName =
@ -64,7 +90,10 @@ export default function MenuLayout<
{!isMobileScreen && ( {!isMobileScreen && (
<div <div
className={`group absolute right-0 h-[100%] flex items-center`} 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"> <div className="opacity-0 group-hover:bg-[rgba($color: #000000, $alpha: 0.01) group-hover:opacity-20">
<DragIcon /> <DragIcon />

View File

@ -1,5 +1,7 @@
import useRelativePosition from "@/app/hooks/useRelativePosition";
import { getCSSVar } from "@/app/utils"; import { getCSSVar } from "@/app/utils";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
const ArrowIcon = ({ color }: { color: string }) => { const ArrowIcon = ({ color }: { color: string }) => {
return ( return (
@ -19,6 +21,20 @@ const ArrowIcon = ({ color }: { color: string }) => {
}; };
const baseZIndex = 100; 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: { export default function Popover(props: {
content?: JSX.Element | string; content?: JSX.Element | string;
@ -46,6 +62,21 @@ export default function Popover(props: {
} = props; } = props;
const [internalShow, setShow] = useState(false); 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; const mergedShow = show ?? internalShow;
@ -56,6 +87,13 @@ export default function Popover(props: {
switch (placement) { switch (placement) {
case "b": case "b":
placementStyle = {
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
left: `calc(${distanceToLeftBoundary + targetW}px - ${
targetW * 0.02
}px)`,
transform: "translateX(-50%)",
};
placementClassName = placementClassName =
"top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]"; "top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]"; arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
@ -67,32 +105,51 @@ export default function Popover(props: {
// placementClassName = ''; // placementClassName = '';
// break; // break;
case "rb": case "rb":
placementClassName = "top-[calc(100%+0.5rem)] translate-x-[calc(-2%)]"; placementStyle = {
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]"; top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
break; right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
case "lt": };
placementClassName = placementClassName = "top-[calc(100%+0.5rem)] right-[calc(-2%)]";
"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%)]";
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]"; arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
break; break;
case "rt": 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%)]"; arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
break; 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": case "t":
default: default:
placementStyle = {
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
left: `calc(${distanceToLeftBoundary + targetW}px - ${
targetW * 0.02
}px)`,
transform: "translateX(-50%)",
};
placementClassName = placementClassName =
"bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]"; "bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]"; arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
} }
const popoverCommonClass = "absolute p-2 box-border";
if (noArrow) { if (noArrow) {
arrowClassName = "hidden"; arrowClassName = "hidden";
} }
@ -109,6 +166,12 @@ export default function Popover(props: {
e.preventDefault(); e.preventDefault();
onShow?.(!mergedShow); onShow?.(!mergedShow);
setShow(!mergedShow); setShow(!mergedShow);
if (!mergedShow) {
getRelativePosition(e.currentTarget, "");
window.document.documentElement.style.overflow = "hidden";
} else {
window.document.documentElement.style.overflow = "auto";
}
}} }}
> >
{children} {children}
@ -119,12 +182,15 @@ export default function Popover(props: {
<ArrowIcon color={internalBgColor} /> <ArrowIcon color={internalBgColor} />
</div> </div>
)} )}
<div {createPortal(
className={`${popoverCommonClass} ${placementClassName} ${popoverClassName}`} <div
style={{ zIndex: baseZIndex + 1 }} className={`${popoverCommonClass} ${popoverClassName}`}
> style={{ zIndex: baseZIndex + 1, ...placementStyle }}
{content} >
</div> {content}
</div>,
popoverRoot,
)}
<div <div
className=" fixed w-[100%] h-[100%] top-0 left-0 right-0 bottom-0" className=" fixed w-[100%] h-[100%] top-0 left-0 right-0 bottom-0"
style={{ zIndex: baseZIndex }} style={{ zIndex: baseZIndex }}

View File

@ -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;

View File

@ -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">
&nbsp;
</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>
);
}

View File

@ -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>
);
}

View File

@ -6,6 +6,8 @@
width: 100%; width: 100%;
flex-direction: column; flex-direction: column;
background-color: var(--white);
.auth-logo { .auth-logo {
transform: scale(1.4); transform: scale(1.4);
} }
@ -33,4 +35,18 @@
margin-bottom: 10px; 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;
}
} }

View File

@ -198,7 +198,7 @@ export function ChatActions(props: {
<Popover <Popover
content={content} content={content}
trigger="click" trigger="click"
placement="lt" placement="rt"
noArrow noArrow
popoverClassName="border-actions-popover border-gray-200 rounded-md shadow-actions-popover w-actions-popover bg-white " 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} key={act.text}
content={act.text} content={act.text}
popoverClassName={`${popoverClassName}`} popoverClassName={`${popoverClassName}`}
placement={ind ? "t" : "rt"} placement={ind ? "t" : "lt"}
> >
<div <div
className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn" 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} key={act.text}
content={act.text} content={act.text}
popoverClassName={`${popoverClassName}`} popoverClassName={`${popoverClassName}`}
placement={ind === arr.length - 1 ? "lt" : "t"} placement={ind === arr.length - 1 ? "rt" : "t"}
> >
<div <div
className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn" className="h-[32px] w-[32px] flex items-center justify-center hover:bg-gray-200 hover:rounded-action-btn"

View File

@ -153,10 +153,6 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
{messages.map((message, i) => { {messages.map((message, i) => {
const isUser = message.role === "user"; const isUser = message.role === "user";
const isContext = i < context.length; const isContext = i < context.length;
const showActions =
i > 0 &&
!(message.preview || message.content.length === 0) &&
!isContext;
const shouldShowClearContextDivider = i === clearContextIndex - 1; const shouldShowClearContextDivider = i === clearContextIndex - 1;
@ -237,7 +233,7 @@ export default function ChatMessagePanel(props: ChatMessagePanelProps) {
message={message} message={message}
inputRef={inputRef} inputRef={inputRef}
isUser={isUser} isUser={isUser}
showActions={showActions} isContext={isContext}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
setShowPromptModal={setShowPromptModal} setShowPromptModal={setShowPromptModal}
/> />

View File

@ -25,7 +25,6 @@ import ChatInputPanel, { ChatInputPanelInstance } from "./ChatInputPanel";
import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel"; import ChatMessagePanel, { RenderMessage } from "./ChatMessagePanel";
import { useAllModels } from "@/app/utils/hooks"; import { useAllModels } from "@/app/utils/hooks";
import useRows from "@/app/hooks/useRows"; import useRows from "@/app/hooks/useRows";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import SessionConfigModel from "./SessionConfigModal"; import SessionConfigModel from "./SessionConfigModal";
import useScrollToBottom from "@/app/hooks/useScrollToBottom"; import useScrollToBottom from "@/app/hooks/useScrollToBottom";
@ -34,6 +33,8 @@ function _Chat() {
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const config = useAppConfig(); const config = useAppConfig();
const { isMobileScreen } = config;
const [showExport, setShowExport] = useState(false); const [showExport, setShowExport] = useState(false);
const [showModelSelector, setShowModelSelector] = useState(false); const [showModelSelector, setShowModelSelector] = useState(false);
@ -44,7 +45,6 @@ function _Chat() {
const chatInputPanelRef = useRef<ChatInputPanelInstance | null>(null); const chatInputPanelRef = useRef<ChatInputPanelInstance | null>(null);
const [hitBottom, setHitBottom] = useState(true); const [hitBottom, setHitBottom] = useState(true);
const isMobileScreen = useMobileScreen();
const [attachImages, setAttachImages] = useState<string[]>([]); const [attachImages, setAttachImages] = useState<string[]>([]);
@ -295,11 +295,7 @@ function _Chat() {
/> />
)} )}
<PromptToast <PromptToast showToast={!hitBottom} setShowModal={setShowPromptModal} />
showToast={!hitBottom}
showModal={showPromptModal}
setShowModal={setShowPromptModal}
/>
{showPromptModal && ( {showPromptModal && (
<SessionConfigModel onClose={() => setShowPromptModal(false)} /> <SessionConfigModel onClose={() => setShowPromptModal(false)} />

View File

@ -8,6 +8,7 @@ import { ContextPrompts } from "@/app/components/mask";
import CancelIcon from "@/app/icons/cancel.svg"; import CancelIcon from "@/app/icons/cancel.svg";
import ConfirmIcon from "@/app/icons/confirm.svg"; import ConfirmIcon from "@/app/icons/confirm.svg";
import Input from "@/app/components/Input";
export function EditMessageModal(props: { onClose: () => void }) { export function EditMessageModal(props: { onClose: () => void }) {
const chatStore = useChatStore(); const chatStore = useChatStore();
@ -47,15 +48,16 @@ export function EditMessageModal(props: { onClose: () => void }) {
title={Locale.Chat.EditMessage.Topic.Title} title={Locale.Chat.EditMessage.Topic.Title}
subTitle={Locale.Chat.EditMessage.Topic.SubTitle} subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
> >
<input <Input
type="text" type="text"
value={session.topic} value={session.topic}
onInput={(e) => onChange={(e) =>
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
(session) => (session.topic = e.currentTarget.value), (session) => (session.topic = e || ""),
) )
} }
></input> className=" text-center"
></Input>
</ListItem> </ListItem>
</List> </List>
<ContextPrompts <ContextPrompts

View File

@ -23,7 +23,8 @@ export type RenderMessage = ChatMessage & { preview?: boolean };
export interface MessageActionsProps { export interface MessageActionsProps {
message: RenderMessage; message: RenderMessage;
isUser: boolean; isUser: boolean;
showActions: boolean; isContext: boolean;
showActions?: boolean;
inputRef: RefObject<HTMLTextAreaElement>; inputRef: RefObject<HTMLTextAreaElement>;
className?: string; className?: string;
setIsLoading?: (value: boolean) => void; 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) { export default function MessageActions(props: MessageActionsProps) {
const { const {
className, className,
message, message,
isUser, isUser,
showActions, isContext,
showActions = true,
setIsLoading, setIsLoading,
inputRef, inputRef,
setShowPromptModal, setShowPromptModal,
@ -228,6 +250,12 @@ export default function MessageActions(props: MessageActionsProps) {
const onCopy = () => copyToClipboard(getMessageTextContent(message)); const onCopy = () => copyToClipboard(getMessageTextContent(message));
const groupsType = [
message.streaming && GroupType.streaming,
isContext && GroupType.isContext,
GroupType.normal,
].find((i) => i) as GroupType;
return ( return (
showActions && ( showActions && (
<div <div
@ -254,19 +282,7 @@ export default function MessageActions(props: MessageActionsProps) {
onResend, onResend,
onUserStop, onUserStop,
})} })}
groups={ groups={groupsTypes[groupsType]}
message.streaming
? [[Locale.Chat.Actions.Stop]]
: [
[
Locale.Chat.Actions.Retry,
"Edit",
Locale.Chat.Actions.Copy,
Locale.Chat.Actions.Pin,
Locale.Chat.Actions.Delete,
],
]
}
className="flex flex-row gap-1 p-1" className="flex flex-row gap-1 p-1"
/> />
</div> </div>

View File

@ -7,7 +7,6 @@ import styles from "./index.module.scss";
export default function PromptToast(props: { export default function PromptToast(props: {
showToast?: boolean; showToast?: boolean;
showModal?: boolean;
setShowModal: (_: boolean) => void; setShowModal: (_: boolean) => void;
}) { }) {
const chatStore = useChatStore(); const chatStore = useChatStore();

View File

@ -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 { useChatStore } from "@/app/store/chat";
import { useMaskStore } from "@/app/store/mask"; import { useMaskStore } from "@/app/store/mask";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import { IconButton } from "@/app/components/button"; import { IconButton } from "@/app/components/button";
import { MaskConfig } from "@/app/components/mask";
import { Path } from "@/app/constant"; import { Path } from "@/app/constant";
import ResetIcon from "@/app/icons/reload.svg"; import ResetIcon from "@/app/icons/reload.svg";
import CopyIcon from "@/app/icons/copy.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 }) { export default function SessionConfigModel(props: { onClose: () => void }) {
const chatStore = useChatStore(); const chatStore = useChatStore();

View File

@ -17,7 +17,6 @@ import { showConfirm } from "@/app/components/ui-lib";
import AddIcon from "@/app/icons/addIcon.svg"; import AddIcon from "@/app/icons/addIcon.svg";
import NextChatTitle from "@/app/icons/nextchatTitle.svg"; import NextChatTitle from "@/app/icons/nextchatTitle.svg";
// import { ListHoodProps } from "@/app/containers/types"; // import { ListHoodProps } from "@/app/containers/types";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import { getTime } from "@/app/utils"; import { getTime } from "@/app/utils";
import DeleteIcon from "@/app/icons/deleteIcon.svg"; import DeleteIcon from "@/app/icons/deleteIcon.svg";
import LogIcon from "@/app/icons/logIcon.svg"; import LogIcon from "@/app/icons/logIcon.svg";
@ -120,8 +119,10 @@ export default MenuLayout(function SessionList(props) {
], ],
); );
const navigate = useNavigate(); const navigate = useNavigate();
const isMobileScreen = useMobileScreen();
const config = useAppConfig(); const config = useAppConfig();
const { isMobileScreen } = config;
const chatStore = useChatStore(); const chatStore = useChatStore();
const { pathname: currentPath } = useLocation(); const { pathname: currentPath } = useLocation();

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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)} />
)}
</>
);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,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)} />
)}
</>
);
}

View File

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

View File

@ -1,8 +1,3 @@
.settings {
padding: 20px;
overflow: auto;
}
.avatar { .avatar {
cursor: pointer; cursor: pointer;
position: relative; position: relative;

View File

@ -1,14 +1,16 @@
import Locale from "@/app/locales"; import Locale from "@/app/locales";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import MenuLayout from "@/app/components/MenuLayout"; import MenuLayout from "@/app/components/MenuLayout";
import Panel from "./SettingPanel"; import Panel from "./SettingPanel";
import GotoIcon from "@/app/icons/goto.svg"; import GotoIcon from "@/app/icons/goto.svg";
import { useAppConfig } from "@/app/store";
export default MenuLayout(function SettingList(props) { export default MenuLayout(function SettingList(props) {
const { setShowPanel } = props; const { setShowPanel } = props;
const isMobileScreen = useMobileScreen(); const config = useAppConfig();
const { isMobileScreen } = config;
let layoutClassName = "pt-7 px-4"; let layoutClassName = "pt-7 px-4";
let titleClassName = "pb-5"; let titleClassName = "pb-5";

View File

@ -16,7 +16,6 @@ import { useAppConfig } from "@/app/store";
import { Path, REPO_URL } from "@/app/constant"; import { Path, REPO_URL } from "@/app/constant";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import useHotKey from "@/app/hooks/useHotKey"; import useHotKey from "@/app/hooks/useHotKey";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import ActionsBar from "@/app/components/ActionsBar"; import ActionsBar from "@/app/components/ActionsBar";
export function SideBar(props: { className?: string }) { export function SideBar(props: { className?: string }) {
@ -24,7 +23,7 @@ export function SideBar(props: { className?: string }) {
const loc = useLocation(); const loc = useLocation();
const config = useAppConfig(); const config = useAppConfig();
const isMobileScreen = useMobileScreen(); const { isMobileScreen } = config;
useHotKey(); useHotKey();

View File

@ -1,6 +0,0 @@
.discover-assistant-container {
display: flex;
flex-direction: column;
fill: var(--light-opacity-white-60, rgba(255, 255, 255, 0.60));
}

View File

@ -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>
</>
);
}

View File

@ -3,7 +3,7 @@
require("../polyfill"); require("../polyfill");
import { HashRouter as Router, Routes, Route } from "react-router-dom"; 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 dynamic from "next/dynamic";
import { Path } from "@/app/constant"; import { Path } from "@/app/constant";
@ -12,12 +12,13 @@ import { getISOLang } from "@/app/locales";
import { useSwitchTheme } from "@/app/hooks/useSwitchTheme"; import { useSwitchTheme } from "@/app/hooks/useSwitchTheme";
import { AuthPage } from "@/app/components/auth"; import { AuthPage } from "@/app/components/auth";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { useAccessStore } from "@/app/store"; import { useAccessStore, useAppConfig } from "@/app/store";
import { useLoadData } from "@/app/hooks/useLoadData"; import { useLoadData } from "@/app/hooks/useLoadData";
import Loading from "@/app/components/Loading"; import Loading from "@/app/components/Loading";
import Screen from "@/app/components/Screen"; import Screen from "@/app/components/Screen";
import { SideBar } from "./Sidebar"; import { SideBar } from "./Sidebar";
import GlobalLoading from "@/app/components/GlobalLoading"; import GlobalLoading from "@/app/components/GlobalLoading";
import { MOBILE_MAX_WIDTH } from "../hooks/useListenWinResize";
const Settings = dynamic( const Settings = dynamic(
async () => await import("@/app/containers/Settings"), async () => await import("@/app/containers/Settings"),
@ -84,14 +85,19 @@ export default function Home() {
useSwitchTheme(); useSwitchTheme();
useLoadData(); useLoadData();
useHtmlLang(); useHtmlLang();
const config = useAppConfig();
useEffect(() => { useEffect(() => {
console.log("[Config] got config from build time", getClientConfig()); console.log("[Config] got config from build time", getClientConfig());
useAccessStore.getState().fetch(); useAccessStore.getState().fetch();
}, []); }, []);
useEffect(() => { useLayoutEffect(() => {
loadAsyncGoogleFont(); loadAsyncGoogleFont();
config.update(
(config) =>
(config.isMobileScreen = window.innerWidth <= MOBILE_MAX_WIDTH),
);
}, []); }, []);
if (!useHasHydrated()) { if (!useHasHydrated()) {

59
app/hooks/useDrag.ts Normal file
View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -8,7 +8,6 @@ import {
DEFAULT_SIDEBAR_WIDTH, DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH,
SIDEBAR_ID,
} from "@/app/constant"; } from "@/app/constant";
import { useAppConfig } from "@/app/store/config"; import { useAppConfig } from "@/app/store/config";
import { updateGlobalCSSVars } from "@/app/utils/client"; import { updateGlobalCSSVars } from "@/app/utils/client";
@ -49,5 +48,8 @@ export default function useListenWinResize() {
config.update((config) => { config.update((config) => {
config.sidebarWidth = menuWidth; config.sidebarWidth = menuWidth;
}); });
config.update((config) => {
config.isMobileScreen = size.width <= MOBILE_MAX_WIDTH;
});
}); });
} }

View File

@ -1,6 +1,5 @@
import { useWindowSize } from "@/app/hooks/useWindowSize"; import { useWindowSize } from "@/app/hooks/useWindowSize";
import { MOBILE_MAX_WIDTH } from "@/app/hooks/useListenWinResize";
export const MOBILE_MAX_WIDTH = 768;
export default function useMobileScreen() { export default function useMobileScreen() {
const { width } = useWindowSize(); const { width } = useWindowSize();

View File

@ -2,7 +2,7 @@ import { RefObject, useState } from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
export interface Options { export interface Options {
containerRef: RefObject<HTMLDivElement | null>; containerRef?: RefObject<HTMLElement | null>;
delay?: number; delay?: number;
offsetDistance?: number; offsetDistance?: number;
} }
@ -32,7 +32,7 @@ interface Position {
} }
export default function useRelativePosition({ export default function useRelativePosition({
containerRef, containerRef = { current: window.document.body },
delay = 100, delay = 100,
offsetDistance = 0, offsetDistance = 0,
}: Options) { }: Options) {
@ -49,6 +49,7 @@ export default function useRelativePosition({
width: targetW, width: targetW,
height: targetH, height: targetH,
} = target.getBoundingClientRect(); } = target.getBoundingClientRect();
const { const {
x: containerX, x: containerX,
y: containerY, y: containerY,

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { autoGrowTextArea } from "../utils"; import { autoGrowTextArea } from "../utils";
import useMobileScreen from "./useMobileScreen"; import { useAppConfig } from "../store";
export default function useRows({ export default function useRows({
inputRef, inputRef,
@ -9,14 +9,14 @@ export default function useRows({
inputRef: React.RefObject<HTMLTextAreaElement>; inputRef: React.RefObject<HTMLTextAreaElement>;
}) { }) {
const [inputRows, setInputRows] = useState(2); const [inputRows, setInputRows] = useState(2);
const isMobileScreen = useMobileScreen(); const config = useAppConfig();
const measure = useDebouncedCallback( const measure = useDebouncedCallback(
() => { () => {
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1; const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
const inputRows = Math.min( const inputRows = Math.min(
20, 20,
Math.max(2 + (isMobileScreen ? -1 : 1), rows), Math.max(2 + (config.isMobileScreen ? -1 : 1), rows),
); );
setInputRows(inputRows); setInputRows(inputRows);
}, },

3
app/icons/darkIcon.svg Normal file
View File

@ -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

View File

@ -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

4
app/icons/lightIcon.svg Normal file
View File

@ -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

View File

@ -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

View File

@ -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

6
app/icons/systemIcon.svg Normal file
View File

@ -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

View File

@ -129,7 +129,11 @@ const cn = {
Title: "设置", Title: "设置",
SubTitle: "所有设置选项", SubTitle: "所有设置选项",
GeneralSettings: "通用设置", GeneralSettings: "通用设置",
Basic: {
Title: "基础设置",
},
Danger: { Danger: {
Title: "系统设置",
Reset: { Reset: {
Title: "重置所有设置", Title: "重置所有设置",
SubTitle: "重置所有设置项回默认值", SubTitle: "重置所有设置项回默认值",
@ -181,6 +185,7 @@ const cn = {
SubTitle: "根据对话内容生成合适的标题", SubTitle: "根据对话内容生成合适的标题",
}, },
Sync: { Sync: {
Title: "数据设置",
CloudState: "云端数据", CloudState: "云端数据",
NotSyncYet: "还没有进行过同步", NotSyncYet: "还没有进行过同步",
Success: "同步成功", Success: "同步成功",
@ -224,6 +229,7 @@ const cn = {
ImportFailed: "导入失败", ImportFailed: "导入失败",
}, },
Mask: { Mask: {
Title: "面具设置",
Splash: { Splash: {
Title: "面具启动页", Title: "面具启动页",
SubTitle: "新建聊天时,展示面具启动页", SubTitle: "新建聊天时,展示面具启动页",
@ -234,6 +240,7 @@ const cn = {
}, },
}, },
Prompt: { Prompt: {
Title: "提示语设置",
Disable: { Disable: {
Title: "禁用提示词自动补全", Title: "禁用提示词自动补全",
SubTitle: "在输入框开头输入 / 即可触发自动补全", SubTitle: "在输入框开头输入 / 即可触发自动补全",
@ -271,6 +278,7 @@ const cn = {
}, },
Access: { Access: {
title: "接口设置",
AccessCode: { AccessCode: {
Title: "访问密码", Title: "访问密码",
SubTitle: "管理员已开启加密访问", SubTitle: "管理员已开启加密访问",
@ -352,7 +360,9 @@ const cn = {
SubTitle: "增加自定义模型可选项,使用英文逗号隔开", SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
}, },
}, },
Models: {
Title: "模型设置",
},
Model: "模型 (model)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",

View File

@ -132,7 +132,11 @@ const en: LocaleType = {
Title: "Settings", Title: "Settings",
SubTitle: "All Settings", SubTitle: "All Settings",
GeneralSettings: "General settings", GeneralSettings: "General settings",
Basic: {
Title: "Basic Settings",
},
Danger: { Danger: {
Title: "System Settings",
Reset: { Reset: {
Title: "Reset All Settings", Title: "Reset All Settings",
SubTitle: "Reset all setting items to default", SubTitle: "Reset all setting items to default",
@ -184,6 +188,7 @@ const en: LocaleType = {
SubTitle: "Generate a suitable title based on the conversation content", SubTitle: "Generate a suitable title based on the conversation content",
}, },
Sync: { Sync: {
Title: "Data Settings",
CloudState: "Last Update", CloudState: "Last Update",
NotSyncYet: "Not sync yet", NotSyncYet: "Not sync yet",
Success: "Sync Success", Success: "Sync Success",
@ -228,6 +233,7 @@ const en: LocaleType = {
ImportFailed: "Failed to import from file", ImportFailed: "Failed to import from file",
}, },
Mask: { Mask: {
Title: "Mask Settings",
Splash: { Splash: {
Title: "Mask Splash Screen", Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat", SubTitle: "Show a mask splash screen before starting new chat",
@ -238,6 +244,7 @@ const en: LocaleType = {
}, },
}, },
Prompt: { Prompt: {
Title: "Prompt Settings",
Disable: { Disable: {
Title: "Disable auto-completion", Title: "Disable auto-completion",
SubTitle: "Input / to trigger auto-completion", SubTitle: "Input / to trigger auto-completion",
@ -275,6 +282,7 @@ const en: LocaleType = {
NoAccess: "Enter API Key to check balance", NoAccess: "Enter API Key to check balance",
}, },
Access: { Access: {
title: "API Settings",
AccessCode: { AccessCode: {
Title: "Access Code", Title: "Access Code",
SubTitle: "Access control Enabled", SubTitle: "Access control Enabled",
@ -357,7 +365,9 @@ const en: LocaleType = {
}, },
}, },
}, },
Models: {
Title: "Model Settings",
},
Model: "Model", Model: "Model",
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",

View File

@ -1,5 +1,4 @@
import { LLMModel } from "../client/api"; import { LLMModel } from "../client/api";
import { isMacOS } from "../utils";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { import {
DEFAULT_INPUT_TEMPLATE, DEFAULT_INPUT_TEMPLATE,
@ -8,6 +7,9 @@ import {
StoreKey, StoreKey,
} from "../constant"; } from "../constant";
import { createPersistStore } from "../utils/store"; 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"]; export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
@ -25,6 +27,21 @@ export enum Theme {
Light = "light", 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 = { export const DEFAULT_CONFIG = {
lastUpdate: Date.now(), // timestamp, to merge state lastUpdate: Date.now(), // timestamp, to merge state
@ -45,6 +62,8 @@ export const DEFAULT_CONFIG = {
customModels: "", customModels: "",
models: DEFAULT_MODELS as any as LLMModel[], models: DEFAULT_MODELS as any as LLMModel[],
isMobileScreen: false,
modelConfig: { modelConfig: {
model: "gpt-3.5-turbo" as ModelType, model: "gpt-3.5-turbo" as ModelType,
temperature: 0.5, temperature: 0.5,

View File

@ -30,3 +30,8 @@ body {
--siderbar-mobile-height: 3.125rem; --siderbar-mobile-height: 3.125rem;
--max-message-width: calc(var(--chat-panel-max-width) * 0.6); --max-message-width: calc(var(--chat-panel-max-width) * 0.6);
} }
input {
text-align: inherit;
background-color: inherit;
}

View File

@ -144,10 +144,10 @@ label {
cursor: pointer; cursor: pointer;
} }
input { // input {
text-align: center; // text-align: center;
font-family: inherit; // font-family: inherit;
} // }
input[type="checkbox"] { input[type="checkbox"] {
cursor: pointer; cursor: pointer;
@ -219,20 +219,20 @@ input[type="range"]::-ms-thumb:hover {
@include thumbHover(); @include thumbHover();
} }
input[type="number"], // input[type="number"],
input[type="text"], // input[type="text"],
input[type="password"] { // input[type="password"] {
appearance: none; // appearance: none;
border-radius: 10px; // border-radius: 10px;
border: var(--border-in-light); // border: var(--border-in-light);
min-height: 36px; // min-height: 36px;
box-sizing: border-box; // box-sizing: border-box;
background: var(--white); // background: var(--white);
color: var(--black); // color: var(--black);
padding: 0 10px; // padding: 0 10px;
max-width: 50%; // max-width: 50%;
font-family: inherit; // font-family: inherit;
} // }
div.math { div.math {
overflow-x: auto; overflow-x: auto;

View File

@ -19,6 +19,7 @@
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@hello-pangea/dnd": "^16.5.0", "@hello-pangea/dnd": "^16.5.0",
"@next/third-parties": "^14.1.0", "@next/third-parties": "^14.1.0",
"@radix-ui/react-switch": "^1.0.3",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2", "@vercel/speed-insights": "^1.0.2",

View File

@ -16,10 +16,12 @@ module.exports = {
}, },
fontWeight: { fontWeight: {
'setting-title': '700', 'setting-title': '700',
'setting-card-title': '600',
}, },
fontFamily: { fontFamily: {
'common': ['Satoshi Variable', 'Variable'], 'common': ['Satoshi Variable', 'Variable'],
'time': ['Hind', 'Variable'] 'time': ['Hind', 'Variable'],
'setting-card-title': ['PingFang HK', 'PingFang']
}, },
screens: { screens: {
sm: '480px', sm: '480px',
@ -29,6 +31,10 @@ module.exports = {
'2xl': '1980px' '2xl': '1980px'
}, },
extend: { extend: {
lineHeight: {
'slide-btn': "17px",
'input': '22px',
},
backdropBlur: { backdropBlur: {
'chat-header': '20px', 'chat-header': '20px',
}, },
@ -36,6 +42,14 @@ module.exports = {
'chat-input-mobile': '19px', 'chat-input-mobile': '19px',
'chat-input': '60px', '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: { width: {
'md': '15rem', 'md': '15rem',
'lg': '21.25rem', 'lg': '21.25rem',
@ -43,6 +57,7 @@ module.exports = {
'page': 'calc(100% - var(--menu-width))', 'page': 'calc(100% - var(--menu-width))',
'thumbnail': '5rem', 'thumbnail': '5rem',
'actions-popover': '203px', 'actions-popover': '203px',
'switch': '2.25rem',
}, },
height: { height: {
mobile: 'var(--siderbar-mobile-height)', mobile: 'var(--siderbar-mobile-height)',
@ -53,6 +68,8 @@ module.exports = {
'chat-input': '60px', 'chat-input': '60px',
'chat-panel-mobile': '- var(--siderbar-mobile-height)', 'chat-panel-mobile': '- var(--siderbar-mobile-height)',
'setting-panel-mobile': 'calc(100vh - var(--siderbar-mobile-height))', 'setting-panel-mobile': 'calc(100vh - var(--siderbar-mobile-height))',
'slide-btn': '18px',
'switch': '1rem',
}, },
flexBasis: { flexBasis: {
'sidebar': 'var(--menu-width)', 'sidebar': 'var(--menu-width)',
@ -75,12 +92,14 @@ module.exports = {
maxHeight: {}, maxHeight: {},
maxWidth: { maxWidth: {
'message-width': 'var(--max-message-width)', 'message-width': 'var(--max-message-width)',
'setting-list': '710px',
}, },
backgroundColor: { backgroundColor: {
'select-btn': 'rgba(0, 0, 0, 0.05)', 'select-btn': 'rgba(0, 0, 0, 0.05)',
'chat-actions-popover-color': 'var(--tip-popover-color)', 'chat-actions-popover-color': 'var(--tip-popover-color)',
'chat-panel': 'var(--chat-panel-bg)', 'chat-panel': 'var(--chat-panel-bg)',
'global': '#E3E3ED', 'global': '#E3E3ED',
'switch-checked': '#2E42F3',
}, },
boxShadow: { boxShadow: {
'btn': '0px 4px 10px 0px rgba(60, 68, 255, 0.14)', '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-popover': '0px 14px 40px 0px rgba(0, 0, 0, 0.12)',
'actions-bar': '0px 4px 30px 0px rgba(0, 0, 0, 0.10)', '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)' 'prompt-hint-container': 'inset 0 4px 8px 0 rgba(0, 0, 0, 0.1)'
} },
colors: {
'text-hint': '#A5A5B3',
'text-danger': '#FF5454',
},
}, },
borderRadius: { borderRadius: {
'none': '0', 'none': '0',
@ -102,6 +125,7 @@ module.exports = {
'actions-bar-btn': '0.375rem', 'actions-bar-btn': '0.375rem',
'chat-input': '0.5rem', 'chat-input': '0.5rem',
'chat-img': '0.5rem', 'chat-img': '0.5rem',
'slide': '0.625rem',
}, },
borderWidth: { borderWidth: {
DEFAULT: '1px', DEFAULT: '1px',

View File

@ -1040,6 +1040,13 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" 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": "@babel/template@^7.18.10", "@babel/template@^7.20.7":
version "7.20.7" version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" 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" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.0.tgz#7d8dacb7fdef0e4387caf7396cbd77f179867d06"
integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ== 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": "@remix-run/router@1.8.0":
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"