mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
refactor: update styling and split the component into smaller components
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IoClose, IoChevronDown, IoChevronForward } from "react-icons/io5";
|
import { IoClose } from "react-icons/io5";
|
||||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||||
import { I18nKey } from "#/i18n/declaration";
|
import { I18nKey } from "#/i18n/declaration";
|
||||||
import { PluginSpec } from "#/api/conversation-service/v1-conversation-service.types";
|
import { PluginSpec } from "#/api/conversation-service/v1-conversation-service.types";
|
||||||
import { Typography } from "#/ui/typography";
|
import { Typography } from "#/ui/typography";
|
||||||
import { cn } from "#/utils/utils";
|
import { cn } from "#/utils/utils";
|
||||||
|
import { PluginLaunchPluginSection } from "./plugin-launch-plugin-section";
|
||||||
|
|
||||||
interface PluginLaunchModalProps {
|
interface PluginLaunchModalProps {
|
||||||
plugins: PluginSpec[];
|
plugins: PluginSpec[];
|
||||||
@@ -116,132 +117,6 @@ export function PluginLaunchModal({
|
|||||||
onStartConversation(pluginConfigs, message);
|
onStartConversation(pluginConfigs, message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderParameterInput = (
|
|
||||||
pluginIndex: number,
|
|
||||||
paramKey: string,
|
|
||||||
paramValue: unknown,
|
|
||||||
) => {
|
|
||||||
const inputId = `plugin-${pluginIndex}-param-${paramKey}`;
|
|
||||||
const inputClasses =
|
|
||||||
"rounded-md border border-tertiary bg-base-secondary px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary";
|
|
||||||
|
|
||||||
if (typeof paramValue === "boolean") {
|
|
||||||
return (
|
|
||||||
<div key={paramKey} className="flex items-center gap-3 py-2">
|
|
||||||
<input
|
|
||||||
id={inputId}
|
|
||||||
data-testid={inputId}
|
|
||||||
type="checkbox"
|
|
||||||
checked={paramValue}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateParameter(pluginIndex, paramKey, e.target.checked)
|
|
||||||
}
|
|
||||||
className="h-4 w-4 rounded border-tertiary bg-base-secondary accent-primary"
|
|
||||||
/>
|
|
||||||
<label htmlFor={inputId} className="text-sm">
|
|
||||||
{paramKey}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof paramValue === "number") {
|
|
||||||
return (
|
|
||||||
<div key={paramKey} className="flex flex-col gap-1 py-2">
|
|
||||||
<label htmlFor={inputId} className="text-sm text-white">
|
|
||||||
{paramKey}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={inputId}
|
|
||||||
data-testid={inputId}
|
|
||||||
type="number"
|
|
||||||
value={paramValue}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateParameter(
|
|
||||||
pluginIndex,
|
|
||||||
paramKey,
|
|
||||||
parseFloat(e.target.value) || 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={inputClasses}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: string input
|
|
||||||
return (
|
|
||||||
<div key={paramKey} className="flex flex-col gap-1 py-2">
|
|
||||||
<label htmlFor={inputId} className="text-sm text-white">
|
|
||||||
{paramKey}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={inputId}
|
|
||||||
data-testid={inputId}
|
|
||||||
type="text"
|
|
||||||
value={String(paramValue ?? "")}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateParameter(pluginIndex, paramKey, e.target.value)
|
|
||||||
}
|
|
||||||
className={inputClasses}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPluginSection = (plugin: PluginSpec, originalIndex: number) => {
|
|
||||||
const isExpanded = expandedSections[originalIndex];
|
|
||||||
const hasParams =
|
|
||||||
plugin.parameters && Object.keys(plugin.parameters).length > 0;
|
|
||||||
|
|
||||||
if (!hasParams) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`plugin-${originalIndex}`}
|
|
||||||
className="rounded-lg border border-tertiary bg-tertiary"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggleSection(originalIndex)}
|
|
||||||
className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-base-tertiary rounded-t-lg"
|
|
||||||
data-testid={`plugin-section-${originalIndex}`}
|
|
||||||
>
|
|
||||||
<Typography.Text className="font-medium">
|
|
||||||
{getPluginDisplayName(plugin)}
|
|
||||||
</Typography.Text>
|
|
||||||
{isExpanded ? (
|
|
||||||
<IoChevronDown className="h-5 w-5 text-white" />
|
|
||||||
) : (
|
|
||||||
<IoChevronForward className="h-5 w-5 text-white" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="border-t border-tertiary px-4 py-3">
|
|
||||||
{plugin.ref && (
|
|
||||||
<div className="mb-2 text-xs text-white">
|
|
||||||
{t(I18nKey.LAUNCH$PLUGIN_REF)} {plugin.ref}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{plugin.repo_path && (
|
|
||||||
<div className="mb-2 text-xs text-white">
|
|
||||||
{t(I18nKey.LAUNCH$PLUGIN_PATH)} {plugin.repo_path}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-1">
|
|
||||||
{Object.entries(plugin.parameters || {}).map(([key, value]) =>
|
|
||||||
renderParameterInput(originalIndex, key, value),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const modalTitle =
|
const modalTitle =
|
||||||
pluginConfigs.length === 1
|
pluginConfigs.length === 1
|
||||||
? getPluginDisplayName(pluginConfigs[0])
|
? getPluginDisplayName(pluginConfigs[0])
|
||||||
@@ -260,7 +135,7 @@ export function PluginLaunchModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-md p-1 text-white hover:text-secondary hover:bg-tertiary"
|
className="rounded-md p-1 text-white hover:bg-tertiary cursor-pointer"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
data-testid="close-button"
|
data-testid="close-button"
|
||||||
>
|
>
|
||||||
@@ -273,9 +148,17 @@ export function PluginLaunchModal({
|
|||||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||||
{pluginsWithParams.length > 0 && (
|
{pluginsWithParams.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{pluginConfigs.map((plugin, index) =>
|
{pluginConfigs.map((plugin, index) => (
|
||||||
renderPluginSection(plugin, index),
|
<PluginLaunchPluginSection
|
||||||
)}
|
key={`plugin-${index}`}
|
||||||
|
plugin={plugin}
|
||||||
|
originalIndex={index}
|
||||||
|
isExpanded={!!expandedSections[index]}
|
||||||
|
onToggle={() => toggleSection(index)}
|
||||||
|
getPluginDisplayName={getPluginDisplayName}
|
||||||
|
onParameterChange={updateParameter}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -319,15 +202,16 @@ export function PluginLaunchModal({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={trustConfirmed}
|
checked={trustConfirmed}
|
||||||
onChange={(e) => setTrustConfirmed(e.target.checked)}
|
onChange={(e) => setTrustConfirmed(e.target.checked)}
|
||||||
className="mt-1 h-4 w-4 rounded border-tertiary bg-base-secondary accent-primary flex-shrink-0"
|
className="mt-1 h-4 w-4 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="trust-checkbox" className="text-sm text-white">
|
<label htmlFor="trust-checkbox" className="text-sm text-white">
|
||||||
{t(I18nKey.LAUNCH$TRUST_SKILL_CHECKBOX, {
|
{t(I18nKey.LAUNCH$TRUST_SKILL_CHECKBOX, {
|
||||||
sources: getUniqueSources().join(", "),
|
sources: getUniqueSources().join(", "),
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
})}
|
})}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-end gap-2">
|
<div className="flex w-full justify-end mt-8">
|
||||||
<BrandButton
|
<BrandButton
|
||||||
testId="start-conversation-button"
|
testId="start-conversation-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||||
|
|
||||||
|
export interface PluginLaunchParameterInputProps {
|
||||||
|
pluginIndex: number;
|
||||||
|
paramKey: string;
|
||||||
|
paramValue: unknown;
|
||||||
|
onParameterChange: (
|
||||||
|
pluginIndex: number,
|
||||||
|
paramKey: string,
|
||||||
|
value: unknown,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PluginLaunchParameterInput({
|
||||||
|
pluginIndex,
|
||||||
|
paramKey,
|
||||||
|
paramValue,
|
||||||
|
onParameterChange,
|
||||||
|
}: PluginLaunchParameterInputProps) {
|
||||||
|
const inputId = `plugin-${pluginIndex}-param-${paramKey}`;
|
||||||
|
|
||||||
|
if (typeof paramValue === "boolean") {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
htmlFor={inputId}
|
||||||
|
className="flex w-full cursor-pointer items-center gap-2.5"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={inputId}
|
||||||
|
data-testid={inputId}
|
||||||
|
type="checkbox"
|
||||||
|
checked={paramValue}
|
||||||
|
onChange={(e) =>
|
||||||
|
onParameterChange(pluginIndex, paramKey, e.target.checked)
|
||||||
|
}
|
||||||
|
className="h-4 w-4 shrink-0 rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{paramKey}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof paramValue === "number") {
|
||||||
|
return (
|
||||||
|
<SettingsInput
|
||||||
|
testId={inputId}
|
||||||
|
name={`plugin-${pluginIndex}-param-${paramKey}`}
|
||||||
|
type="number"
|
||||||
|
label={paramKey}
|
||||||
|
value={String(paramValue)}
|
||||||
|
className="w-full"
|
||||||
|
onChange={(value) =>
|
||||||
|
onParameterChange(
|
||||||
|
pluginIndex,
|
||||||
|
paramKey,
|
||||||
|
value === "" ? 0 : parseFloat(value) || 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsInput
|
||||||
|
testId={inputId}
|
||||||
|
name={`plugin-${pluginIndex}-param-${paramKey}`}
|
||||||
|
type="text"
|
||||||
|
label={paramKey}
|
||||||
|
value={String(paramValue ?? "")}
|
||||||
|
className="w-full"
|
||||||
|
onChange={(value) => onParameterChange(pluginIndex, paramKey, value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { IoChevronDown, IoChevronForward } from "react-icons/io5";
|
||||||
|
import { I18nKey } from "#/i18n/declaration";
|
||||||
|
import { PluginSpec } from "#/api/conversation-service/v1-conversation-service.types";
|
||||||
|
import { Typography } from "#/ui/typography";
|
||||||
|
import { PluginLaunchParameterInput } from "./plugin-launch-parameter-input";
|
||||||
|
|
||||||
|
export interface PluginLaunchPluginSectionProps {
|
||||||
|
plugin: PluginSpec;
|
||||||
|
originalIndex: number;
|
||||||
|
isExpanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
getPluginDisplayName: (plugin: PluginSpec) => string;
|
||||||
|
onParameterChange: (
|
||||||
|
pluginIndex: number,
|
||||||
|
paramKey: string,
|
||||||
|
value: unknown,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PluginLaunchPluginSection({
|
||||||
|
plugin,
|
||||||
|
originalIndex,
|
||||||
|
isExpanded,
|
||||||
|
onToggle,
|
||||||
|
getPluginDisplayName,
|
||||||
|
onParameterChange,
|
||||||
|
}: PluginLaunchPluginSectionProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const hasParams =
|
||||||
|
plugin.parameters && Object.keys(plugin.parameters).length > 0;
|
||||||
|
|
||||||
|
if (!hasParams) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-tertiary bg-tertiary">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-base-tertiary rounded-t-lg cursor-pointer"
|
||||||
|
data-testid={`plugin-section-${originalIndex}`}
|
||||||
|
>
|
||||||
|
<Typography.Text className="text-base font-medium">
|
||||||
|
{getPluginDisplayName(plugin)}
|
||||||
|
</Typography.Text>
|
||||||
|
{isExpanded ? (
|
||||||
|
<IoChevronDown className="h-5 w-5 text-white" />
|
||||||
|
) : (
|
||||||
|
<IoChevronForward className="h-5 w-5 text-white" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="border-t border-tertiary px-4 pb-3">
|
||||||
|
{plugin.ref && (
|
||||||
|
<div className="mb-2 text-sm text-white">
|
||||||
|
{t(I18nKey.LAUNCH$PLUGIN_REF)} {plugin.ref}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{plugin.repo_path && (
|
||||||
|
<div className="mb-2 text-sm text-white">
|
||||||
|
{t(I18nKey.LAUNCH$PLUGIN_PATH)} {plugin.repo_path}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{Object.entries(plugin.parameters || {}).map(([key, value]) => (
|
||||||
|
<PluginLaunchParameterInput
|
||||||
|
key={key}
|
||||||
|
pluginIndex={originalIndex}
|
||||||
|
paramKey={key}
|
||||||
|
paramValue={value}
|
||||||
|
onParameterChange={onParameterChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user