Toggle switch component (#4890)

* replace all toggle switches with toggle component

* add variant, label, and description support for toggle component

* refactor Toggle with subcomponents and JSDoc

* use checked value from Toggle onChange callback

* replace missed inline toggles with Toggle component

* fix jsdoc to use optional props

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2026-01-28 16:58:45 -08:00 committed by GitHub
parent fdeb7b9acf
commit 49a2b8f6f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 448 additions and 481 deletions

View File

@ -6,6 +6,7 @@ import { TagsInput } from "react-tag-input-component";
import { Info, Warning } from "@phosphor-icons/react"; import { Info, Warning } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
const DEFAULT_BRANCHES = ["main", "master"]; const DEFAULT_BRANCHES = ["main", "master"];
export default function GitlabOptions() { export default function GitlabOptions() {
@ -128,32 +129,18 @@ export default function GitlabOptions() {
</p> </p>
</div> </div>
<div className="flex items-center gap-x-2 mb-3"> <div className="flex items-center gap-x-2 mb-3">
<label className="relative inline-flex cursor-pointer items-center"> <Toggle
<input name="fetchIssues"
type="checkbox" size="md"
name="fetchIssues" label={t("connectors.gitlab.fetch_issues")}
value={true} />
className="peer sr-only"
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
<span className="ml-3 text-sm font-medium text-white">
{t("connectors.gitlab.fetch_issues")}
</span>
</label>
</div> </div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<label className="relative inline-flex cursor-pointer items-center"> <Toggle
<input name="fetchWikis"
type="checkbox" size="md"
name="fetchWikis" label="Fetch Wikis as Documents"
value={true} />
className="peer sr-only"
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
<span className="ml-3 text-sm font-medium text-white">
Fetch Wikis as Documents
</span>
</label>
</div> </div>
</div> </div>
<GitLabBranchSelection <GitLabBranchSelection

View File

@ -11,6 +11,7 @@ import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import { safeJsonParse } from "@/utils/request"; import { safeJsonParse } from "@/utils/request";
import Toggle from "@/components/lib/Toggle";
import { import {
USERNAME_MIN_LENGTH, USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH, USERNAME_MAX_LENGTH,
@ -294,10 +295,9 @@ function AutoSubmitPreference() {
setAutoSubmitSttInput(settings.autoSubmitSttInput ?? true); setAutoSubmitSttInput(settings.autoSubmitSttInput ?? true);
}, []); }, []);
const handleChange = (e) => { const handleChange = (checked) => {
const newValue = e.target.checked; setAutoSubmitSttInput(checked);
setAutoSubmitSttInput(newValue); Appearance.updateSettings({ autoSubmitSttInput: checked });
Appearance.updateSettings({ autoSubmitSttInput: newValue });
}; };
return ( return (
@ -317,19 +317,7 @@ function AutoSubmitPreference() {
<Info size={16} weight="bold" className="text-white" /> <Info size={16} weight="bold" className="text-white" />
</div> </div>
</div> </div>
<div className="flex items-center gap-x-4"> <Toggle size="lg" enabled={autoSubmitSttInput} onChange={handleChange} />
<label className="relative inline-flex cursor-pointer items-center">
<input
id="autoSubmit"
type="checkbox"
name="autoSubmit"
checked={autoSubmitSttInput}
onChange={handleChange}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
<Tooltip <Tooltip
id="auto-submit-info" id="auto-submit-info"
place="bottom" place="bottom"
@ -352,10 +340,9 @@ function AutoSpeakPreference() {
); );
}, []); }, []);
const handleChange = (e) => { const handleChange = (checked) => {
const newValue = e.target.checked; setAutoPlayAssistantTtsResponse(checked);
setAutoPlayAssistantTtsResponse(newValue); Appearance.updateSettings({ autoPlayAssistantTtsResponse: checked });
Appearance.updateSettings({ autoPlayAssistantTtsResponse: newValue });
}; };
return ( return (
@ -375,19 +362,11 @@ function AutoSpeakPreference() {
<Info size={16} weight="bold" className="text-white" /> <Info size={16} weight="bold" className="text-white" />
</div> </div>
</div> </div>
<div className="flex items-center gap-x-4"> <Toggle
<label className="relative inline-flex cursor-pointer items-center"> size="lg"
<input enabled={autoPlayAssistantTtsResponse}
id="autoSpeak" onChange={handleChange}
type="checkbox" />
name="autoSpeak"
checked={autoPlayAssistantTtsResponse}
onChange={handleChange}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
<Tooltip <Tooltip
id="auto-speak-info" id="auto-speak-info"
place="bottom" place="bottom"

View File

@ -0,0 +1,163 @@
import { Info } from "@phosphor-icons/react";
const TOGGLE_STYLES = {
sm: "h-[12px] w-[20px] after:h-[8px] after:w-[8px] after:top-[2px] after:left-[2px] peer-checked:after:translate-x-full",
md: "h-[16px] w-[28px] after:h-[12px] after:w-[12px] after:top-[2px] after:left-[2px] peer-checked:after:translate-x-full",
lg: "h-[19px] w-[36px] after:h-[15px] after:w-[15px] after:top-[2px] after:left-[2px] peer-checked:after:translate-x-[17px]",
};
const LABEL_STYLES = {
sm: {
label: "text-[12px] leading-[10px] font-medium mt-[1.5px]",
description: "text-[10px] leading-[16px] font-normal",
gap: "gap-[2px]",
},
md: {
label: "text-[14px] leading-[18px] font-medium -mt-[2px]",
description: "text-[12px] leading-[16px] font-normal",
gap: "gap-[2px]",
},
lg: {
label: "text-[16px] leading-[14px] font-medium mt-[2.5px]",
description: "text-[14px] leading-[24px] font-normal",
gap: "gap-[2px]",
},
};
/**
* @param {Object} props - Component props
* @param {string} [props.className] - Additional CSS classes
* @param {boolean} [props.enabled] - Controlled checked state
* @param {(checked: boolean) => void} [props.onChange] - Change handler receiving new checked state
* @param {boolean} [props.disabled=false] - Whether toggle is disabled
* @param {"sm" | "md" | "lg"} [props.size="sm"] - Toggle size
* @param {string} [props.name] - Input name for form submission
* @param {string} [props.label] - Label text next to toggle
* @param {string} [props.description] - Description text below label
* @param {"default" | "horizontal"} [props.variant="default"] - Layout variant
* @param {string} [props.hint] - Tooltip ID for info icon hint next to label
* @param {string} [props.value] - Input value for form submission
*/
export default function Toggle({
className,
enabled,
onChange,
disabled = false,
size = "sm",
name,
label,
description,
variant = "default",
hint,
value,
}) {
const inputProps =
enabled !== undefined
? { checked: enabled, onChange: (e) => onChange?.(e.target.checked) }
: { defaultChecked: false };
const labelStyles = LABEL_STYLES[size] || LABEL_STYLES.sm;
if (variant === "horizontal") {
return (
<label
className={`flex items-start justify-between max-w-[700px] ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"} ${className ?? ""}`}
>
<TextContent
label={label}
description={description}
labelStyles={labelStyles}
hint={hint}
/>
<div className="shrink-0 ml-4">
<ToggleSwitch
name={name}
disabled={disabled}
size={size}
inputProps={inputProps}
value={value}
/>
</div>
</label>
);
}
return (
<label
className={`inline-flex items-start ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"} ${className ?? ""}`}
>
<ToggleSwitch
name={name}
disabled={disabled}
size={size}
inputProps={inputProps}
value={value}
/>
{(label || description) && (
<div className="ml-3">
<TextContent
label={label}
description={description}
labelStyles={labelStyles}
hint={hint}
/>
</div>
)}
</label>
);
}
function ToggleSwitch({ name, disabled, size, inputProps, value }) {
return (
<>
<input
type="checkbox"
name={name}
disabled={disabled}
className="peer sr-only"
value={value}
{...inputProps}
/>
<div
className={`
relative shrink-0 peer pointer-events-none rounded-full
${TOGGLE_STYLES[size] || TOGGLE_STYLES.sm}
after:absolute after:rounded-full after:bg-white
after:transition-all after:content-['']
peer-focus:ring-2
bg-zinc-500 light:bg-zinc-300 peer-focus:ring-zinc-700 light:peer-focus:bg-green-100 light:peer-focus:ring-green-200
peer-checked:bg-green-400 peer-checked:peer-focus:bg-green-300 peer-checked:peer-focus:ring-green-900 light:peer-checked:peer-focus:bg-green-300 light:peer-checked:peer-focus:ring-green-200
`}
/>
</>
);
}
function TextContent({ label, description, labelStyles = {}, hint }) {
if (!label && !description) return null;
return (
<div className={`flex flex-col ${labelStyles.gap}`}>
{label && (
<span
className={`flex items-center gap-x-1 text-white light:text-slate-950 ${labelStyles.label}`}
>
{label}
{hint && (
<Info
size={14}
className="text-theme-text-secondary cursor-pointer"
data-tooltip-id={hint}
/>
)}
</span>
)}
{description && (
<span
className={`text-zinc-400 light:text-zinc-600 ${labelStyles.description}`}
>
{description}
</span>
)}
</div>
);
}

View File

@ -11,6 +11,7 @@ import {
BracketsCurly, BracketsCurly,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip"; import { Tooltip } from "react-tooltip";
import Toggle from "@/components/lib/Toggle";
import StartNode from "../nodes/StartNode"; import StartNode from "../nodes/StartNode";
import ApiCallNode from "../nodes/ApiCallNode"; import ApiCallNode from "../nodes/ApiCallNode";
import WebsiteNode from "../nodes/WebsiteNode"; import WebsiteNode from "../nodes/WebsiteNode";
@ -172,32 +173,20 @@ export default function BlockList({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{renderBlockConfigContent(block, props)} {renderBlockConfigContent(block, props)}
<div className="flex justify-between items-center pt-4 border-t border-white/10"> <div className="pt-4 border-t border-white/10">
<div> <Toggle
<label className="block text-sm font-medium text-theme-text-primary"> size="md"
Direct Output variant="horizontal"
</label> label="Direct Output"
<p className="text-xs text-theme-text-secondary"> description="The output of this block will be returned directly to the chat. This will prevent any further tool calls from being executed."
The output of this block will be returned directly to the chat. enabled={props.config.directOutput || false}
<br /> onChange={(checked) =>
This will prevent any further tool calls from being executed. props.onConfigChange({
</p> ...props.config,
</div> directOutput: checked,
<label className="relative inline-flex cursor-pointer items-center"> })
<input }
type="checkbox" />
checked={props.config.directOutput || false}
onChange={(e) =>
props.onConfigChange({
...props.config,
directOutput: e.target.checked,
})
}
className="peer sr-only"
aria-label="Toggle direct output"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div> </div>
</div> </div>
); );

View File

@ -1,5 +1,4 @@
import { Info } from "@phosphor-icons/react"; import Toggle from "@/components/lib/Toggle";
import React from "react";
export default function WebScrapingNode({ export default function WebScrapingNode({
config, config,
@ -72,35 +71,16 @@ export default function WebScrapingNode({
</div> </div>
)} )}
<div className="flex justify-between items-center"> <Toggle
<div className="flex flex-row items-center gap-x-1 mb-2"> size="md"
<label className="block text-sm font-medium text-theme-text-primary"> variant="horizontal"
Content Summarization label="Content Summarization"
</label> hint="content-summarization-tooltip"
<Info enabled={config.enableSummarization ?? true}
size={16} onChange={(checked) =>
className="text-theme-text-secondary cursor-pointer" onConfigChange({ ...config, enableSummarization: checked })
data-tooltip-id="content-summarization-tooltip" }
/> />
</div>
<div className="flex items-center gap-2 mb-2">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={config.enableSummarization ?? true}
onChange={(e) =>
onConfigChange({
...config,
enableSummarization: e.target.checked,
})
}
className="sr-only peer"
aria-label="Toggle content summarization"
/>
<div className="w-11 h-6 bg-theme-settings-input-bg peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-button/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-button"></div>
</label>
</div>
</div>
<div> <div>
<label className="block text-sm font-medium text-theme-text-primary mb-2"> <label className="block text-sm font-medium text-theme-text-primary mb-2">
Result Variable Result Variable

View File

@ -4,6 +4,7 @@ import showToast from "@/utils/toast";
import { FlowArrow, Gear } from "@phosphor-icons/react"; import { FlowArrow, Gear } from "@phosphor-icons/react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import Toggle from "@/components/lib/Toggle";
function ManageFlowMenu({ flow, onDelete }) { function ManageFlowMenu({ flow, onDelete }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -97,22 +98,17 @@ export default function FlowPanel({ flow, toggleFlow, onDelete }) {
<> <>
<div className="p-2"> <div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex items-center gap-x-2"> <div className="flex w-full justify-between items-center">
<FlowArrow size={24} weight="bold" className="text-white" /> <div className="flex items-center gap-x-2">
<label htmlFor="name" className="text-white text-md font-bold"> <FlowArrow size={24} weight="bold" className="text-white" />
{flow.name} <label htmlFor="name" className="text-white text-md font-bold">
</label> {flow.name}
<label className="border-none relative inline-flex items-center ml-auto cursor-pointer"> </label>
<input </div>
type="checkbox" <div className="flex items-center gap-x-2">
className="peer sr-only" <Toggle size="lg" enabled={isActive} onChange={handleToggle} />
checked={isActive} <ManageFlowMenu flow={flow} onDelete={onDelete} />
onChange={handleToggle} </div>
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
<span className="ml-3 text-sm font-medium"></span>
</label>
<ManageFlowMenu flow={flow} onDelete={onDelete} />
</div> </div>
<p className="whitespace-pre-wrap text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="whitespace-pre-wrap text-white text-opacity-60 text-xs font-medium py-1.5">
{flow.description || "No description provided"} {flow.description || "No description provided"}

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { DefaultBadge } from "../Badges/default"; import { DefaultBadge } from "../Badges/default";
import Toggle from "@/components/lib/Toggle";
export default function DefaultSkillPanel({ export default function DefaultSkillPanel({
title, title,
@ -29,18 +30,11 @@ export default function DefaultSkillPanel({
</label> </label>
<DefaultBadge title={title} /> <DefaultBadge title={title} />
</div> </div>
<label <Toggle
className={`border-none relative inline-flex items-center ml-auto cursor-pointer`} size="lg"
> enabled={enabled}
<input onChange={() => toggleSkill(skill)}
type="checkbox" />
className="peer sr-only"
checked={enabled}
onChange={() => toggleSkill(skill)}
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
<span className="ml-3 text-sm font-medium"></span>
</label>
</div> </div>
<img src={image} alt={title} className="w-full rounded-md" /> <img src={image} alt={title} className="w-full rounded-md" />
<p className="text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5"> <p className="text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5">

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import Toggle from "@/components/lib/Toggle";
export default function GenericSkillPanel({ export default function GenericSkillPanel({
title, title,
@ -13,34 +14,27 @@ export default function GenericSkillPanel({
return ( return (
<div className="p-2"> <div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex items-center gap-x-2"> <div className="flex w-full justify-between items-center">
{icon && <div className="flex items-center gap-x-2">
React.createElement(icon, { {icon &&
size: 24, React.createElement(icon, {
color: "var(--theme-text-primary)", size: 24,
weight: "bold", color: "var(--theme-text-primary)",
})} weight: "bold",
<label })}
htmlFor="name" <label
className="text-theme-text-primary text-md font-bold" htmlFor="name"
> className="text-theme-text-primary text-md font-bold"
{title} >
</label> {title}
<label </label>
className={`border-none relative inline-flex items-center ml-auto ${ </div>
disabled ? "cursor-not-allowed" : "cursor-pointer" <Toggle
}`} size="lg"
> enabled={enabled}
<input disabled={disabled}
type="checkbox" onChange={() => toggleSkill(skill)}
disabled={disabled} />
className="peer sr-only"
checked={enabled}
onChange={() => toggleSkill(skill)}
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
<span className="ml-3 text-sm font-medium"></span>
</label>
</div> </div>
<img src={image} alt={title} className="w-full rounded-md" /> <img src={image} alt={title} className="w-full rounded-md" />
<p className="text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5"> <p className="text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5">

View File

@ -3,6 +3,7 @@ import showToast from "@/utils/toast";
import { Gear, Plug } from "@phosphor-icons/react"; import { Gear, Plug } from "@phosphor-icons/react";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { sentenceCase } from "text-case"; import { sentenceCase } from "text-case";
import Toggle from "@/components/lib/Toggle";
/** /**
* Converts setup_args to inputs for the form builder * Converts setup_args to inputs for the form builder
@ -110,25 +111,24 @@ export default function ImportedSkillConfig({
<> <>
<div className="p-2"> <div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex items-center gap-x-2"> <div className="flex w-full justify-between items-center">
<Plug size={24} weight="bold" className="text-white" /> <div className="flex items-center gap-x-2">
<label htmlFor="name" className="text-white text-md font-bold"> <Plug size={24} weight="bold" className="text-white" />
{sentenceCase(config.name)} <label htmlFor="name" className="text-white text-md font-bold">
</label> {sentenceCase(config.name)}
<label className="border-none relative inline-flex items-center ml-auto cursor-pointer"> </label>
<input </div>
type="checkbox" <div className="flex items-center gap-x-2">
className="peer sr-only" <Toggle
checked={config.active} size="lg"
onChange={() => toggleSkill()} enabled={config.active}
onChange={toggleSkill}
/> />
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div> <ManageSkillMenu
<span className="ml-3 text-sm font-medium"></span> config={config}
</label> setImportedSkills={setImportedSkills}
<ManageSkillMenu />
config={config} </div>
setImportedSkills={setImportedSkills}
/>
</div> </div>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5"> <p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{config.description} by{" "} {config.description} by{" "}

View File

@ -5,6 +5,7 @@ import { WarningOctagon, X } from "@phosphor-icons/react";
import { DB_LOGOS } from "./DBConnection"; import { DB_LOGOS } from "./DBConnection";
import System from "@/models/system"; import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import Toggle from "@/components/lib/Toggle";
function assembleConnectionString({ function assembleConnectionString({
engine, engine,
@ -288,21 +289,13 @@ export default function NewSQLConnection({
)} )}
{engine === "sql-server" && ( {engine === "sql-server" && (
<div className="flex items-center justify-between"> <Toggle
<label className="relative inline-flex items-center cursor-pointer"> name="encrypt"
<input value="true"
type="checkbox" size="md"
name="encrypt" label="Enable Encryption"
value="true" enabled={config.encrypt}
className="sr-only peer" />
checked={config.encrypt}
/>
<div className="w-11 h-6 bg-theme-settings-input-bg peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-white">
Enable Encryption
</span>
</label>
</div>
)} )}
<p className="text-theme-text-secondary text-sm"> <p className="text-theme-text-secondary text-sm">

View File

@ -5,6 +5,7 @@ import NewSQLConnection from "./NewConnectionModal";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import SQLAgentImage from "@/media/agents/sql-agent.png"; import SQLAgentImage from "@/media/agents/sql-agent.png";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import Toggle from "@/components/lib/Toggle";
export default function AgentSQLConnectorSelection({ export default function AgentSQLConnectorSelection({
skill, skill,
@ -36,28 +37,25 @@ export default function AgentSQLConnectorSelection({
<> <>
<div className="p-2"> <div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex items-center gap-x-2"> <div className="flex w-full justify-between items-center">
<Database <div className="flex items-center gap-x-2">
size={24} <Database
color="var(--theme-text-primary)" size={24}
weight="bold" color="var(--theme-text-primary)"
/> weight="bold"
<label
htmlFor="name"
className="text-theme-text-primary text-md font-bold"
>
SQL Agent
</label>
<label className="border-none relative inline-flex items-center ml-auto cursor-pointer">
<input
type="checkbox"
className="peer sr-only"
checked={enabled}
onChange={() => toggleSkill(skill)}
/> />
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div> <label
<span className="ml-3 text-sm font-medium"></span> htmlFor="name"
</label> className="text-theme-text-primary text-md font-bold"
>
SQL Agent
</label>
</div>
<Toggle
size="lg"
enabled={enabled}
onChange={() => toggleSkill(skill)}
/>
</div> </div>
<img <img
src={SQLAgentImage} src={SQLAgentImage}

View File

@ -17,6 +17,7 @@ import {
X, X,
ListMagnifyingGlass, ListMagnifyingGlass,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import Toggle from "@/components/lib/Toggle";
import SearchProviderItem from "./SearchProviderItem"; import SearchProviderItem from "./SearchProviderItem";
import WebSearchImage from "@/media/agents/scrape-websites.png"; import WebSearchImage from "@/media/agents/scrape-websites.png";
import { import {
@ -170,28 +171,25 @@ export default function AgentWebSearchSelection({
return ( return (
<div className="p-2"> <div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[500px]"> <div className="flex flex-col gap-y-[18px] max-w-[500px]">
<div className="flex items-center gap-x-2"> <div className="flex w-full justify-between items-center">
<ListMagnifyingGlass <div className="flex items-center gap-x-2">
size={24} <ListMagnifyingGlass
color="var(--theme-text-primary)" size={24}
weight="bold" color="var(--theme-text-primary)"
/> weight="bold"
<label
htmlFor="name"
className="text-theme-text-primary text-md font-bold"
>
Live web search and browsing
</label>
<label className="border-none relative inline-flex items-center ml-auto cursor-pointer">
<input
type="checkbox"
className="peer sr-only"
checked={enabled}
onChange={() => toggleSkill(skill)}
/> />
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div> <label
<span className="ml-3 text-sm font-medium"></span> htmlFor="name"
</label> className="text-theme-text-primary text-md font-bold"
>
Live web search and browsing
</label>
</div>
<Toggle
size="lg"
enabled={enabled}
onChange={() => toggleSkill(skill)}
/>
</div> </div>
<img <img
src={WebSearchImage} src={WebSearchImage}

View File

@ -4,6 +4,7 @@ import showToast from "@/utils/toast";
import { ArrowSquareOut } from "@phosphor-icons/react"; import { ArrowSquareOut } from "@phosphor-icons/react";
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import Toggle from "@/components/lib/Toggle";
export default function LiveSyncToggle({ enabled = false, onToggle }) { export default function LiveSyncToggle({ enabled = false, onToggle }) {
const [status, setStatus] = useState(enabled); const [status, setStatus] = useState(enabled);
@ -36,15 +37,7 @@ export default function LiveSyncToggle({ enabled = false, onToggle }) {
<h2 className="text-theme-text-primary text-md font-bold"> <h2 className="text-theme-text-primary text-md font-bold">
Automatic Document Content Sync Automatic Document Content Sync
</h2> </h2>
<label className="relative inline-flex cursor-pointer items-center"> <Toggle size="lg" enabled={status} onChange={toggleFeatureFlag} />
<input
type="checkbox"
onClick={toggleFeatureFlag}
checked={status}
className="peer sr-only pointer-events-none"
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div> </div>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<p className="text-theme-text-secondary text-sm"> <p className="text-theme-text-secondary text-sm">

View File

@ -11,6 +11,7 @@ import NewUserModal from "./NewUserModal";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import Toggle from "@/components/lib/Toggle";
export default function AdminUsers() { export default function AdminUsers() {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
@ -147,31 +148,19 @@ export function MessageLimitInput({ enabled, limit, updateState, role }) {
if (role === "admin") return null; if (role === "admin") return null;
return ( return (
<div className="mt-4 mb-8"> <div className="mt-4 mb-8">
<div className="flex flex-col gap-y-1"> <Toggle
<div className="flex items-center gap-x-2"> size="md"
<h2 className="text-base leading-6 font-bold text-white"> variant="horizontal"
Limit messages per day label="Limit messages per day"
</h2> description="Restrict this user to a number of successful queries or chats within a 24 hour window."
<label className="relative inline-flex cursor-pointer items-center"> enabled={enabled}
<input onChange={(checked) => {
type="checkbox" updateState((prev) => ({
checked={enabled} ...prev,
onChange={(e) => { enabled: checked,
updateState((prev) => ({ }));
...prev, }}
enabled: e.target.checked, />
}));
}}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
<p className="text-xs leading-[18px] font-base text-white/60">
Restrict this user to a number of successful queries or chats within a
24 hour window.
</p>
</div>
{enabled && ( {enabled && (
<div className="mt-4"> <div className="mt-4">
<label className="text-white text-sm font-semibold block mb-4"> <label className="text-white text-sm font-semibold block mb-4">

View File

@ -3,6 +3,7 @@ import { X } from "@phosphor-icons/react";
import Workspace from "@/models/workspace"; import Workspace from "@/models/workspace";
import { TagsInput } from "react-tag-input-component"; import { TagsInput } from "react-tag-input-component";
import Embed from "@/models/embed"; import Embed from "@/models/embed";
import Toggle from "@/components/lib/Toggle";
export function enforceSubmissionSchema(form) { export function enforceSubmissionSchema(form) {
const data = {}; const data = {};
@ -343,23 +344,14 @@ export const BooleanInput = ({ name, title, hint, defaultValue = null }) => {
const [status, setStatus] = useState(defaultValue ?? false); const [status, setStatus] = useState(defaultValue ?? false);
return ( return (
<div> <Toggle
<div className="flex flex-col mb-2"> name={name}
<label htmlFor={name} className="block text-sm font-medium text-white"> size="md"
{title} variant="horizontal"
</label> label={title}
<p className="text-theme-text-secondary text-xs">{hint}</p> description={hint}
</div> enabled={status}
<label className="relative inline-flex cursor-pointer items-center"> onChange={(checked) => setStatus(checked)}
<input />
name={name}
type="checkbox"
onClick={() => setStatus(!status)}
checked={status}
className="peer sr-only pointer-events-none"
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
); );
}; };

View File

@ -6,6 +6,7 @@ import System from "@/models/system";
import PreLoader from "@/components/Preloader"; import PreLoader from "@/components/Preloader";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ProviderPrivacy from "@/components/ProviderPrivacy"; import ProviderPrivacy from "@/components/ProviderPrivacy";
import Toggle from "@/components/lib/Toggle";
export default function PrivacyAndDataHandling() { export default function PrivacyAndDataHandling() {
const [settings, setSettings] = useState({}); const [settings, setSettings] = useState({});
@ -80,18 +81,13 @@ function TelemetryLogs({ settings }) {
<div className="space-y-6 flex h-full w-full"> <div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-4">
<div className=""> <div className="">
<label className="mb-2.5 block font-medium text-theme-text-primary"> <Toggle
{t("privacy.anonymous")} size="lg"
</label> className="mb-4"
<label className="relative inline-flex cursor-pointer items-center"> label={t("privacy.anonymous")}
<input enabled={telemetry}
type="checkbox" onChange={toggleTelemetry}
onClick={toggleTelemetry} />
checked={telemetry}
className="peer sr-only pointer-events-none"
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
import PreLoader from "@/components/Preloader"; import PreLoader from "@/components/Preloader";
import CTAButton from "@/components/lib/CTAButton"; import CTAButton from "@/components/lib/CTAButton";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
import { import {
USERNAME_MIN_LENGTH, USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH, USERNAME_MAX_LENGTH,
@ -125,26 +126,19 @@ function MultiUserMode() {
<div className="flex items-start justify-between px-6 py-4"></div> <div className="flex items-start justify-between px-6 py-4"></div>
<div className="space-y-6 flex h-full w-full"> <div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-4">
<div className=""> {multiUserModeEnabled ? (
<label className="text-white text-sm font-semibold block mb-3"> <p className="text-white text-sm font-semibold">
{multiUserModeEnabled {t("security.multiuser.enable.is-enable")}
? t("security.multiuser.enable.is-enable") </p>
: t("security.multiuser.enable.enable")} ) : (
</label> <Toggle
size="lg"
<label className="relative inline-flex cursor-pointer items-center"> className="mb-4"
<input label={t("security.multiuser.enable.enable")}
type="checkbox" enabled={useMultiUserMode}
onClick={() => setUseMultiUserMode(!useMultiUserMode)} onChange={(checked) => setUseMultiUserMode(checked)}
defaultChecked={useMultiUserMode} />
className="peer sr-only pointer-events-none" )}
/>
<div
hidden={multiUserModeEnabled}
className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"
/>
</label>
</div>
{useMultiUserMode && ( {useMultiUserMode && (
<div className="w-full flex flex-col gap-y-2 my-5"> <div className="w-full flex flex-col gap-y-2 my-5">
<div className="w-80"> <div className="w-80">
@ -307,21 +301,13 @@ function PasswordProtection() {
<div className="flex items-start justify-between px-6 py-4"></div> <div className="flex items-start justify-between px-6 py-4"></div>
<div className="space-y-6 flex h-full w-full"> <div className="space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4"> <div className="w-full flex flex-col gap-y-4">
<div className=""> <Toggle
<label className="text-white text-sm font-semibold block mb-3"> size="lg"
{t("security.password.title")} className="mb-4"
</label> label={t("security.password.title")}
enabled={usePassword}
<label className="relative inline-flex cursor-pointer items-center"> onChange={(checked) => setUsePassword(checked)}
<input />
type="checkbox"
onClick={() => setUsePassword(!usePassword)}
defaultChecked={usePassword}
className="peer sr-only pointer-events-none"
/>
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent" />
</label>
</div>
{usePassword && ( {usePassword && (
<div className="w-full flex flex-col gap-y-2 my-5"> <div className="w-full flex flex-col gap-y-2 my-5">
<div className="mt-4 w-80"> <div className="mt-4 w-80">

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
export default function AutoSpeak() { export default function AutoSpeak() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -8,15 +9,14 @@ export default function AutoSpeak() {
useState(false); useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = async (e) => { const handleChange = async (checked) => {
const newValue = e.target.checked; setAutoPlayAssistantTtsResponse(checked);
setAutoPlayAssistantTtsResponse(newValue);
setSaving(true); setSaving(true);
try { try {
Appearance.updateSettings({ autoPlayAssistantTtsResponse: newValue }); Appearance.updateSettings({ autoPlayAssistantTtsResponse: checked });
} catch (error) { } catch (error) {
console.error("Failed to update appearance settings:", error); console.error("Failed to update appearance settings:", error);
setAutoPlayAssistantTtsResponse(!newValue); setAutoPlayAssistantTtsResponse(!checked);
} }
setSaving(false); setSaving(false);
}; };
@ -32,28 +32,16 @@ export default function AutoSpeak() {
}, []); }, []);
return ( return (
<div className="flex flex-col gap-y-0.5 my-4"> <div className="my-4">
<p className="text-sm leading-6 font-semibold text-white"> <Toggle
{t("customization.chat.auto_speak.title")} size="md"
</p> variant="horizontal"
<p className="text-xs text-white/60"> enabled={autoPlayAssistantTtsResponse}
{t("customization.chat.auto_speak.description")} onChange={handleChange}
</p> disabled={saving}
<div className="flex items-center gap-x-4"> label={t("customization.chat.auto_speak.title")}
<label className="relative inline-flex cursor-pointer items-center"> description={t("customization.chat.auto_speak.description")}
<input />
id="auto_speak"
type="checkbox"
name="auto_speak"
value="yes"
checked={autoPlayAssistantTtsResponse}
onChange={handleChange}
disabled={saving}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
</div> </div>
); );
} }

View File

@ -1,21 +1,21 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
export default function AutoSubmit() { export default function AutoSubmit() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [autoSubmitSttInput, setAutoSubmitSttInput] = useState(true); const [autoSubmitSttInput, setAutoSubmitSttInput] = useState(true);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = async (e) => { const handleChange = async (checked) => {
const newValue = e.target.checked; setAutoSubmitSttInput(checked);
setAutoSubmitSttInput(newValue);
setSaving(true); setSaving(true);
try { try {
Appearance.updateSettings({ autoSubmitSttInput: newValue }); Appearance.updateSettings({ autoSubmitSttInput: checked });
} catch (error) { } catch (error) {
console.error("Failed to update appearance settings:", error); console.error("Failed to update appearance settings:", error);
setAutoSubmitSttInput(!newValue); setAutoSubmitSttInput(!checked);
} }
setSaving(false); setSaving(false);
}; };
@ -29,28 +29,16 @@ export default function AutoSubmit() {
}, []); }, []);
return ( return (
<div className="flex flex-col gap-y-0.5 my-4"> <div className="my-4">
<p className="text-sm leading-6 font-semibold text-white"> <Toggle
{t("customization.chat.auto_submit.title")} size="md"
</p> variant="horizontal"
<p className="text-xs text-white/60"> enabled={autoSubmitSttInput}
{t("customization.chat.auto_submit.description")} onChange={handleChange}
</p> disabled={saving}
<div className="flex items-center gap-x-4"> label={t("customization.chat.auto_submit.title")}
<label className="relative inline-flex cursor-pointer items-center"> description={t("customization.chat.auto_submit.description")}
<input />
id="auto_submit"
type="checkbox"
name="auto_submit"
value="yes"
checked={autoSubmitSttInput}
onChange={handleChange}
disabled={saving}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
</div> </div>
); );
} }

View File

@ -1,21 +1,21 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
export default function ChatRenderHTML() { export default function ChatRenderHTML() {
const { t } = useTranslation(); const { t } = useTranslation();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [renderHTML, setRenderHTML] = useState(false); const [renderHTML, setRenderHTML] = useState(false);
const handleChange = async (e) => { const handleChange = async (checked) => {
const newValue = e.target.checked; setRenderHTML(checked);
setRenderHTML(newValue);
setSaving(true); setSaving(true);
try { try {
Appearance.updateSettings({ renderHTML: newValue }); Appearance.updateSettings({ renderHTML: checked });
} catch (error) { } catch (error) {
console.error("Failed to update appearance settings:", error); console.error("Failed to update appearance settings:", error);
setRenderHTML(!newValue); setRenderHTML(!checked);
} }
setSaving(false); setSaving(false);
}; };
@ -29,28 +29,16 @@ export default function ChatRenderHTML() {
}, []); }, []);
return ( return (
<div className="flex flex-col gap-y-0.5 my-4"> <div className="my-4">
<p className="text-sm leading-6 font-semibold text-white"> <Toggle
{t("customization.items.render-html.title")} size="md"
</p> variant="horizontal"
<p className="text-xs text-white/60 w-1/2 whitespace-pre-line"> enabled={renderHTML}
{t("customization.items.render-html.description")} onChange={handleChange}
</p> disabled={saving}
<div className="flex items-center gap-x-4 pt-1"> label={t("customization.items.render-html.title")}
<label className="relative inline-flex cursor-pointer items-center"> description={t("customization.items.render-html.description")}
<input />
id="render_html"
type="checkbox"
name="render_html"
value="yes"
checked={renderHTML}
onChange={handleChange}
disabled={saving}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
</div> </div>
); );
} }

View File

@ -1,21 +1,21 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
export default function ShowScrollbar() { export default function ShowScrollbar() {
const { t } = useTranslation(); const { t } = useTranslation();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [showScrollbar, setShowScrollbar] = useState(false); const [showScrollbar, setShowScrollbar] = useState(false);
const handleChange = async (e) => { const handleChange = async (checked) => {
const newValue = e.target.checked; setShowScrollbar(checked);
setShowScrollbar(newValue);
setSaving(true); setSaving(true);
try { try {
Appearance.updateSettings({ showScrollbar: newValue }); Appearance.updateSettings({ showScrollbar: checked });
} catch (error) { } catch (error) {
console.error("Failed to update appearance settings:", error); console.error("Failed to update appearance settings:", error);
setShowScrollbar(!newValue); setShowScrollbar(!checked);
} }
setSaving(false); setSaving(false);
}; };
@ -29,28 +29,16 @@ export default function ShowScrollbar() {
}, []); }, []);
return ( return (
<div className="flex flex-col gap-y-0.5 my-4"> <div className="my-4">
<p className="text-sm leading-6 font-semibold text-white"> <Toggle
{t("customization.items.show-scrollbar.title")} size="md"
</p> variant="horizontal"
<p className="text-xs text-white/60"> enabled={showScrollbar}
{t("customization.items.show-scrollbar.description")} onChange={handleChange}
</p> disabled={saving}
<div className="flex items-center gap-x-4"> label={t("customization.items.show-scrollbar.title")}
<label className="relative inline-flex cursor-pointer items-center"> description={t("customization.items.show-scrollbar.description")}
<input />
id="show_scrollbar"
type="checkbox"
name="show_scrollbar"
value="yes"
checked={showScrollbar}
onChange={handleChange}
disabled={saving}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
</div> </div>
); );
} }

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState } from "react";
import Appearance from "@/models/appearance"; import Appearance from "@/models/appearance";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Toggle from "@/components/lib/Toggle";
export default function SpellCheck() { export default function SpellCheck() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -9,42 +10,29 @@ export default function SpellCheck() {
Appearance.get("enableSpellCheck") Appearance.get("enableSpellCheck")
); );
const handleChange = async (e) => { const handleChange = async (checked) => {
const newValue = e.target.checked; setEnableSpellCheck(checked);
setEnableSpellCheck(newValue);
setSaving(true); setSaving(true);
try { try {
Appearance.set("enableSpellCheck", newValue); Appearance.set("enableSpellCheck", checked);
} catch (error) { } catch (error) {
console.error("Failed to update appearance settings:", error); console.error("Failed to update appearance settings:", error);
setEnableSpellCheck(!newValue); setEnableSpellCheck(!checked);
} }
setSaving(false); setSaving(false);
}; };
return ( return (
<div className="flex flex-col gap-y-0.5 my-4"> <div className="my-4">
<p className="text-sm leading-6 font-semibold text-white"> <Toggle
{t("customization.chat.spellcheck.title")} size="md"
</p> variant="horizontal"
<p className="text-xs text-white/60"> enabled={enableSpellCheck}
{t("customization.chat.spellcheck.description")} onChange={handleChange}
</p> disabled={saving}
<div className="flex items-center gap-x-4"> label={t("customization.chat.spellcheck.title")}
<label className="relative inline-flex cursor-pointer items-center"> description={t("customization.chat.spellcheck.description")}
<input />
id="spellcheck"
type="checkbox"
name="spellcheck"
value="yes"
checked={enableSpellCheck}
onChange={handleChange}
disabled={saving}
className="peer sr-only"
/>
<div className="pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
</label>
</div>
</div> </div>
); );
} }