feat: update main chat and system chat UI

This commit is contained in:
2026-05-25 17:26:37 +09:00
parent fb5ec649cd
commit f59522ffc4
120 changed files with 43262 additions and 3325 deletions

View File

@@ -0,0 +1,740 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, UnorderedListOutlined } from '@ant-design/icons';
import { Alert, App, Button, Card, Checkbox, Descriptions, Empty, Form, Input, InputNumber, List, Modal, Space, Switch, Tabs, Tag, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { getReadyPlayAppEntries } from '../../views/play/apps/apps/appsRegistry';
import { confirmWithKeyboard } from './modalKeyboard';
import {
deleteTokenSetting,
type TokenSettingRecord,
upsertTokenSetting,
useTokenSettingRegistry,
} from './tokenSettingAccess';
import { useTokenAccess } from './tokenAccess';
import './TokenSettingManagementPage.css';
const { Paragraph, Text, Title } = Typography;
type TokenSettingFormValue = {
originalId?: string;
id: string;
name: string;
description: string;
defaultExpiresInMinutes: number;
maxTokensPer30Days: number;
maxTokensPer7Days: number;
maxTokensPer5Hours: number;
oneTimeTokenLimit: number;
allowedAppIds: string[];
enabled: boolean;
};
type AppOption = {
value: string;
label: string;
description: string;
category: '관리' | 'Play';
};
type SharedTokenSettingPreview = {
id: string;
name: string;
defaultExpiresInMinutes: number;
maxTokensPer30Days: number;
maxTokensPer7Days: number;
maxTokensPer5Hours: number;
oneTimeTokenLimit: number;
allowedAppIds: string[];
};
type SharedTokenSettingAccess = {
shareToken: string;
canManage: boolean;
};
const MANAGEMENT_APP_OPTIONS: AppOption[] = [
{ value: 'chat-live', label: 'Codex Live', description: '채팅방 조회와 응답 흐름 진입', category: '관리' },
{ value: 'chat-rooms', label: '시스템 채팅방', description: '메뉴별 시스템 채팅방 화면 접근', category: '관리' },
{ value: 'chat-room-settings', label: '채팅방 설정', description: 'Codex Live 채팅방 Context 설정 편집 접근', category: '관리' },
{ value: 'token-setting', label: '토큰관리 설정', description: '토큰 설정 목록과 상세 편집 접근', category: '관리' },
{ value: 'shared-resource', label: '공유 리소스 관리', description: '공유 링크와 권한, 활동 이력 관리', category: '관리' },
{ value: 'plan-board', label: '자동화 현황', description: '작업 현황과 요청 보드 접근', category: '관리' },
{ value: 'app-settings', label: '앱 설정', description: '헤더 설정, 알림, 업데이트 화면 접근', category: '관리' },
{ value: 'resource-manager', label: '리소스 관리', description: '세션 리소스와 파일 미리보기 접근', category: '관리' },
{ value: 'error-log', label: '에러 로그', description: '앱 로그와 장애 이력 조회', category: '관리' },
];
const PLAY_APP_OPTIONS: AppOption[] = getReadyPlayAppEntries().map((entry) => ({
value: entry.id,
label: entry.name,
description: entry.searchDescription ?? `${entry.name} 앱 실행`,
category: 'Play',
}));
const APP_OPTIONS: AppOption[] = [...MANAGEMENT_APP_OPTIONS, ...PLAY_APP_OPTIONS];
const APP_OPTION_LABEL_MAP = new Map(APP_OPTIONS.map((item) => [item.value, item.label] as const));
const EMPTY_FORM_VALUE: TokenSettingFormValue = {
id: '',
name: '',
description: '',
defaultExpiresInMinutes: 60,
maxTokensPer30Days: 0,
maxTokensPer7Days: 100_000,
maxTokensPer5Hours: 100_000,
oneTimeTokenLimit: 0,
allowedAppIds: [],
enabled: true,
};
function normalizeSettingId(value: string | null | undefined) {
return String(value ?? '')
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9._-]/g, '');
}
function toFormValue(setting: TokenSettingRecord | null): TokenSettingFormValue {
if (!setting) {
return EMPTY_FORM_VALUE;
}
return {
originalId: setting.id,
id: setting.id,
name: setting.name,
description: setting.description,
defaultExpiresInMinutes: setting.defaultExpiresInMinutes,
maxTokensPer30Days: setting.maxTokensPer30Days,
maxTokensPer7Days: setting.maxTokensPer7Days,
maxTokensPer5Hours: setting.maxTokensPer5Hours,
oneTimeTokenLimit: setting.oneTimeTokenLimit,
allowedAppIds: setting.allowedAppIds,
enabled: setting.enabled,
};
}
function formatDuration(minutes: number) {
if (minutes <= 0) {
return '무제한';
}
if (minutes % (60 * 24) === 0) {
return `${minutes / (60 * 24)}`;
}
if (minutes % 60 === 0) {
return `${minutes / 60}시간`;
}
return `${minutes}`;
}
function formatTokenLimit(value: number) {
if (value <= 0) {
return '무제한';
}
return `${value.toLocaleString('ko-KR')} 토큰`;
}
function formatUnlimitedNumberInput(value: string | number | null | undefined, unit: string) {
if (value === null || value === undefined || value === '') {
return '';
}
const normalized = Number(String(value).replace(/[^\d.-]/g, ''));
if (!Number.isFinite(normalized)) {
return String(value);
}
if (normalized <= 0) {
return '무제한';
}
return `${normalized.toLocaleString('ko-KR')} ${unit}`;
}
function parseUnlimitedNumberInput(value: string | undefined) {
if (!value) {
return '';
}
if (value.includes('무제한')) {
return '0';
}
return value.replace(/[^\d]/g, '');
}
function formatQuotaSummary(setting: TokenSettingRecord) {
return [`7일 ${formatTokenLimit(setting.maxTokensPer7Days)}`, `5시간 ${formatTokenLimit(setting.maxTokensPer5Hours)}`].join(' / ');
}
function resolveAppLabels(appIds: string[]) {
return appIds.map((item) => APP_OPTION_LABEL_MAP.get(item) ?? item);
}
export function TokenSettingManagementPage({
sharedPreviewTokenSetting = null,
sharedAccess = null,
}: {
sharedPreviewTokenSetting?: SharedTokenSettingPreview | null;
sharedAccess?: SharedTokenSettingAccess | null;
}) {
const { message } = App.useApp();
const { hasAccess } = useTokenAccess();
const isSharedManageMode = !hasAccess && Boolean(sharedAccess?.canManage && sharedAccess.shareToken);
const isSharedPreviewMode = !hasAccess && Boolean(sharedPreviewTokenSetting) && !isSharedManageMode;
const sharedPreviewRecord = useMemo<TokenSettingRecord | null>(
() =>
sharedPreviewTokenSetting
? {
id: sharedPreviewTokenSetting.id,
name: sharedPreviewTokenSetting.name,
description: '',
defaultExpiresInMinutes: sharedPreviewTokenSetting.defaultExpiresInMinutes,
maxExpiresInMinutes: sharedPreviewTokenSetting.defaultExpiresInMinutes,
maxTokensPer30Days: sharedPreviewTokenSetting.maxTokensPer30Days,
maxTokensPer7Days: sharedPreviewTokenSetting.maxTokensPer7Days,
maxTokensPer5Hours: sharedPreviewTokenSetting.maxTokensPer5Hours,
oneTimeTokenLimit: sharedPreviewTokenSetting.oneTimeTokenLimit,
allowedAppIds: sharedPreviewTokenSetting.allowedAppIds,
enabled: true,
updatedAt: '',
}
: null,
[sharedPreviewTokenSetting],
);
const { tokenSettings, setTokenSettings, isLoading, errorMessage } = useTokenSettingRegistry(
hasAccess || isSharedManageMode,
isSharedManageMode ? { shareToken: sharedAccess?.shareToken } : undefined,
);
const [selectedTokenSettingId, setSelectedTokenSettingId] = useState<string | null>(
sharedPreviewTokenSetting?.id ?? tokenSettings[0]?.id ?? null,
);
const [detailMode, setDetailMode] = useState<'list' | 'detail'>(sharedPreviewTokenSetting ? 'detail' : 'list');
const [isCreating, setIsCreating] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [saveErrorMessage, setSaveErrorMessage] = useState('');
const [saveSuccessMessage, setSaveSuccessMessage] = useState('');
const [activeDetailTab, setActiveDetailTab] = useState<'basic' | 'quota' | 'apps'>('basic');
const [form] = Form.useForm<TokenSettingFormValue>();
const [modalApi, modalContextHolder] = Modal.useModal();
const lastHydratedFormKeyRef = useRef('');
const selectedTokenSetting = useMemo(
() => tokenSettings.find((item) => item.id === selectedTokenSettingId) ?? null,
[selectedTokenSettingId, tokenSettings],
);
const effectiveSelectedTokenSetting =
selectedTokenSetting ?? (isSharedPreviewMode || isSharedManageMode ? sharedPreviewRecord : null);
useEffect(() => {
if (isSharedPreviewMode || isSharedManageMode) {
setSelectedTokenSettingId(sharedPreviewTokenSetting?.id ?? null);
if (detailMode !== 'detail') {
setDetailMode('detail');
}
return;
}
if (selectedTokenSettingId && tokenSettings.some((item) => item.id === selectedTokenSettingId)) {
return;
}
setSelectedTokenSettingId(tokenSettings[0]?.id ?? null);
}, [detailMode, isSharedManageMode, isSharedPreviewMode, selectedTokenSettingId, sharedPreviewTokenSetting?.id, tokenSettings]);
useEffect(() => {
if (detailMode !== 'detail') {
lastHydratedFormKeyRef.current = '';
return;
}
const nextFormKey = isCreating ? '__create__' : effectiveSelectedTokenSetting?.id ?? '__empty__';
if (lastHydratedFormKeyRef.current === nextFormKey) {
return;
}
lastHydratedFormKeyRef.current = nextFormKey;
form.resetFields();
form.setFieldsValue(toFormValue(isCreating ? null : effectiveSelectedTokenSetting));
}, [detailMode, effectiveSelectedTokenSetting?.id, form, isCreating]);
const openCreateForm = () => {
setIsCreating(true);
setSelectedTokenSettingId(null);
setDetailMode('detail');
setSaveErrorMessage('');
setSaveSuccessMessage('');
setActiveDetailTab('basic');
form.resetFields();
form.setFieldsValue(EMPTY_FORM_VALUE);
};
const openDetail = (tokenSettingId: string) => {
setIsCreating(false);
setSelectedTokenSettingId(tokenSettingId);
setDetailMode('detail');
setSaveErrorMessage('');
setSaveSuccessMessage('');
setActiveDetailTab('basic');
};
const closeDetail = () => {
setIsCreating(false);
setDetailMode('list');
setSaveErrorMessage('');
setSaveSuccessMessage('');
};
const handleDelete = async () => {
if (!selectedTokenSetting) {
return;
}
const confirmed = await confirmWithKeyboard(modalApi, {
title: `"${selectedTokenSetting.name}" 토큰 설정을 삭제할까요?`,
okText: '삭제',
cancelText: '취소',
okButtonProps: { danger: true },
});
if (!confirmed) {
return;
}
const nextTokenSettings = deleteTokenSetting(tokenSettings, selectedTokenSetting.id);
setIsSaving(true);
setSaveErrorMessage('');
setSaveSuccessMessage('');
try {
const savedTokenSettings = await setTokenSettings(nextTokenSettings);
setSelectedTokenSettingId(savedTokenSettings[0]?.id ?? null);
setIsCreating(false);
setDetailMode('list');
form.resetFields();
form.setFieldsValue(EMPTY_FORM_VALUE);
message.success('토큰 설정을 삭제했습니다.');
} catch (error) {
setSaveErrorMessage(error instanceof Error ? error.message : '토큰 설정 삭제에 실패했습니다.');
} finally {
setIsSaving(false);
}
};
if (!hasAccess) {
if (!isSharedPreviewMode && !isSharedManageMode) {
return (
<>
{modalContextHolder}
<Card title="토큰 설정" className="chat-type-management-page">
<Alert
showIcon
type="warning"
message="관리 페이지는 토큰 등록 사용자만 사용할 수 있습니다."
description="설정 > 토큰 관리에서 권한 토큰을 등록한 뒤 토큰 설정을 관리하세요."
/>
</Card>
</>
);
}
}
return (
<div className={`chat-type-management-page${detailMode === 'detail' ? ' chat-type-management-page--detail' : ''}`}>
{modalContextHolder}
{detailMode === 'list' ? (
<Card
title="토큰 설정"
className="chat-type-management-page__card"
extra={
isSharedPreviewMode || isSharedManageMode ? null : (
<Button icon={<PlusOutlined />} onClick={openCreateForm}>
</Button>
)
}
>
<div className="chat-type-management-page__list">
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
<div className="chat-type-management-page__list-header">
<Title level={5}> </Title>
<Text type="secondary">{isLoading ? '불러오는 중' : `${tokenSettings.length}`}</Text>
</div>
<Paragraph type="secondary" className="token-setting-management-page__helper">
ID를 , , 7/5 .
</Paragraph>
{tokenSettings.length > 0 ? (
<List
dataSource={tokenSettings}
renderItem={(item) => (
<List.Item
className={
item.id === selectedTokenSettingId
? 'chat-type-management-page__item chat-type-management-page__item--active'
: 'chat-type-management-page__item'
}
onClick={() => openDetail(item.id)}
actions={[
<Button
key="edit"
type="text"
icon={<EditOutlined />}
disabled={isSaving}
onClick={(event) => {
event.stopPropagation();
openDetail(item.id);
}}
/>,
]}
>
<div className="chat-type-management-page__item-main">
<Space size={[8, 8]} wrap>
<Text strong>{item.name}</Text>
<Text type="secondary">{item.id}</Text>
<Tag color={item.enabled ? 'green' : 'default'}>{item.enabled ? '사용' : '중지'}</Tag>
</Space>
<div className="token-setting-management-page__stats">
<Tag>{`유효시간 ${formatDuration(item.defaultExpiresInMinutes)}`}</Tag>
<Tag>{formatQuotaSummary(item)}</Tag>
<Tag>{`${item.allowedAppIds.length}`}</Tag>
</div>
<div className="chat-type-management-page__item-description">
{item.description || '설명 없음'}
</div>
<Space size={[6, 6]} wrap>
{resolveAppLabels(item.allowedAppIds).map((label) => (
<Tag key={`${item.id}-${label}`}>{label}</Tag>
))}
</Space>
</div>
</List.Item>
)}
/>
) : (
<Empty description="등록된 토큰 설정이 없습니다." />
)}
</div>
</Card>
) : (
<Card
title={isCreating ? '토큰 설정 등록' : '토큰 설정 상세'}
className="chat-type-management-page__card"
extra={
isSharedPreviewMode ? (
<Tag color="blue"> </Tag>
) : isSharedManageMode ? (
<Tag color="cyan"> </Tag>
) : (
<Space size={6} className="chat-type-management-page__header-actions" wrap>
<Button
type="primary"
shape="circle"
icon={<SaveOutlined />}
loading={isSaving}
aria-label={isCreating ? '등록' : '수정 저장'}
onClick={() => {
void form.submit();
}}
/>
<Button shape="circle" icon={<PlusOutlined />} disabled={isSaving} aria-label="새 입력" onClick={openCreateForm} />
{!isCreating && selectedTokenSetting ? (
<Button
danger
shape="circle"
icon={<DeleteOutlined />}
loading={isSaving}
aria-label="삭제"
onClick={() => void handleDelete()}
/>
) : null}
<Button shape="circle" icon={<UnorderedListOutlined />} aria-label="목록 가기" onClick={closeDetail} />
</Space>
)
}
>
<div className="chat-type-management-page__editor">
{isSharedPreviewMode ? (
<Alert
showIcon
type="info"
message="현재 공유 링크에 연결된 토큰 설정입니다."
description="허용된 앱과 한도를 이 화면에서 바로 확인할 수 있습니다."
/>
) : null}
{isSharedManageMode ? (
<Alert
showIcon
type="info"
message="현재 공유 링크에 연결된 토큰 설정을 관리 중입니다."
description="이 공유 링크에 연결된 설정 1건만 수정할 수 있습니다."
/>
) : null}
{(!isSharedPreviewMode || isSharedManageMode) && errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
{saveSuccessMessage ? <Alert showIcon type="success" message={saveSuccessMessage} /> : null}
<Form
className="chat-type-management-page__editor-form"
disabled={isSharedPreviewMode}
layout="vertical"
form={form}
initialValues={EMPTY_FORM_VALUE}
scrollToFirstError
onFinishFailed={() => {
setSaveSuccessMessage('');
setSaveErrorMessage('필수 입력값과 권한 앱 선택을 확인해 주세요.');
}}
onFinish={async (values) => {
const nextTokenSettings = upsertTokenSetting(tokenSettings, values);
const isNewSetting = isCreating;
setIsSaving(true);
setSaveErrorMessage('');
setSaveSuccessMessage('');
try {
const savedTokenSettings = await setTokenSettings(nextTokenSettings);
const normalizedSavedId = normalizeSettingId(values.id);
const savedTokenSetting =
savedTokenSettings.find((item) => item.id === normalizedSavedId) ??
savedTokenSettings.find((item) => item.id === normalizeSettingId(values.originalId));
setIsCreating(false);
setSelectedTokenSettingId(savedTokenSetting?.id ?? null);
setDetailMode('detail');
const nextSuccessMessage = isNewSetting ? '토큰 설정을 등록했습니다.' : '토큰 설정을 저장했습니다.';
setSaveSuccessMessage(nextSuccessMessage);
message.success(nextSuccessMessage);
} catch (error) {
setSaveErrorMessage(error instanceof Error ? error.message : '토큰 설정 저장에 실패했습니다.');
} finally {
setIsSaving(false);
}
}}
>
<Form.Item name="originalId" hidden>
<Input />
</Form.Item>
<div className="chat-type-management-page__editor-scroll">
<Tabs
activeKey={activeDetailTab}
onChange={(key) => setActiveDetailTab(key as 'basic' | 'quota' | 'apps')}
className="token-setting-management-page__detail-tabs"
items={[
{
key: 'basic',
label: '기본 정보',
children: (
<div className="token-setting-management-page__section-scroll">
{isSharedPreviewMode && sharedPreviewRecord ? (
<Descriptions column={1} bordered size="small" className="token-setting-management-page__summary-descriptions">
<Descriptions.Item label="설정 ID">{sharedPreviewRecord.id}</Descriptions.Item>
<Descriptions.Item label="설정명">{sharedPreviewRecord.name}</Descriptions.Item>
<Descriptions.Item label="유효시간">{formatDuration(sharedPreviewRecord.defaultExpiresInMinutes)}</Descriptions.Item>
<Descriptions.Item label="앱 개수">{`${sharedPreviewRecord.allowedAppIds.length}`}</Descriptions.Item>
</Descriptions>
) : (
<div className="chat-type-management-page__meta-grid">
<Form.Item
className="chat-type-management-page__meta-item"
label="설정 ID"
name="id"
extra={isSharedManageMode ? '공유 링크 관리 모드에서는 설정 ID를 변경할 수 없습니다.' : undefined}
rules={[
{ required: true, message: '설정 ID를 입력하세요.' },
{
validator: async (_rule, value) => {
const normalized = normalizeSettingId(value);
if (!normalized) {
throw new Error('영문, 숫자, `-`, `_`, `.` 조합으로 입력하세요.');
}
},
},
]}
>
<Input placeholder="예: photoprism-basic" disabled={isSharedManageMode} />
</Form.Item>
<Form.Item
className="chat-type-management-page__meta-item chat-type-management-page__meta-item--enabled"
label="사용"
name="enabled"
valuePropName="checked"
>
<Switch checkedChildren="사용" unCheckedChildren="중지" />
</Form.Item>
<Form.Item
className="chat-type-management-page__meta-item chat-type-management-page__meta-item--name"
label="설정명"
name="name"
rules={[{ required: true, message: '설정명을 입력하세요.' }]}
>
<Input placeholder="예: PhotoPrism 읽기 전용" />
</Form.Item>
<Form.Item
className="chat-type-management-page__meta-item chat-type-management-page__meta-item--name"
label="설명"
name="description"
>
<Input.TextArea
autoSize={{ minRows: 4, maxRows: 10 }}
placeholder="이 설정으로 발급할 토큰의 용도와 제한을 적어 두세요."
/>
</Form.Item>
</div>
)}
</div>
),
},
{
key: 'quota',
label: '만료·한도',
children: (
<div className="token-setting-management-page__section-scroll">
<div className="token-setting-management-page__quota-grid">
<Form.Item
label="유효시간(분)"
name="defaultExpiresInMinutes"
extra="0이면 만료 없이 계속 사용할 수 있습니다."
rules={[{ required: true, message: '기본 유효시간을 입력하세요.' }]}
>
<InputNumber
min={0}
style={{ width: '100%' }}
formatter={(value) => formatUnlimitedNumberInput(value, '분')}
parser={((value: string | undefined) => Number(parseUnlimitedNumberInput(value) || 0)) as unknown as (displayValue: string | undefined) => 0}
/>
</Form.Item>
<Form.Item
label="7일 사용 가능 토큰"
name="maxTokensPer7Days"
extra="최근 7일 동안 누적 사용할 수 있는 총 토큰입니다."
rules={[{ required: true, message: '7일 토큰 한도를 입력하세요.' }]}
>
<InputNumber
min={0}
style={{ width: '100%' }}
formatter={(value) => formatUnlimitedNumberInput(value, '토큰')}
parser={((value: string | undefined) => Number(parseUnlimitedNumberInput(value) || 0)) as unknown as (displayValue: string | undefined) => 0}
/>
</Form.Item>
<Form.Item
label="5시간 사용 가능 토큰"
name="maxTokensPer5Hours"
extra="최근 5시간 동안 누적 사용할 수 있는 총 토큰입니다."
rules={[{ required: true, message: '5시간 토큰 한도를 입력하세요.' }]}
>
<InputNumber
min={0}
style={{ width: '100%' }}
formatter={(value) => formatUnlimitedNumberInput(value, '토큰')}
parser={((value: string | undefined) => Number(parseUnlimitedNumberInput(value) || 0)) as unknown as (displayValue: string | undefined) => 0}
/>
</Form.Item>
</div>
</div>
),
},
{
key: 'apps',
label: `앱 권한 ${APP_OPTIONS.length}`,
children: (
<div className="token-setting-management-page__section-scroll">
<Form.Item
label="실행 가능 앱"
name="allowedAppIds"
rules={[
{
validator: async (_rule, value) => {
if (Array.isArray(value) && value.length > 0) {
return;
}
throw new Error('최소 1개 이상의 앱 권한을 선택하세요.');
},
},
]}
>
<Checkbox.Group style={{ width: '100%' }}>
<div className="token-setting-management-page__app-grid">
{APP_OPTIONS.map((option) => (
<div key={option.value} className="token-setting-management-page__app-card">
<div className="token-setting-management-page__app-card-header">
<div className="token-setting-management-page__app-card-title">
<Text strong>{option.label}</Text>
<Text type="secondary">{option.value}</Text>
</div>
<Tag>{option.category}</Tag>
</div>
<Text type="secondary">{option.description}</Text>
<Checkbox value={option.value}> </Checkbox>
</div>
))}
</div>
</Checkbox.Group>
</Form.Item>
</div>
),
},
]}
/>
</div>
{isSharedPreviewMode ? (
<div className="token-setting-management-page__form-actions">
<Text type="secondary"> 1 .</Text>
</div>
) : isSharedManageMode ? (
<div className="token-setting-management-page__form-actions">
<Space size={8} wrap>
<Button
type="primary"
icon={<SaveOutlined />}
loading={isSaving}
onClick={() => {
void form.submit();
}}
>
</Button>
</Space>
<Text type="secondary"> 1 .</Text>
</div>
) : (
<div className="token-setting-management-page__form-actions">
<Space size={8} wrap>
<Button
type="primary"
icon={<SaveOutlined />}
loading={isSaving}
onClick={() => {
void form.submit();
}}
>
{isCreating ? '등록' : '저장'}
</Button>
<Button icon={<PlusOutlined />} disabled={isSaving} onClick={openCreateForm}>
</Button>
{!isCreating && selectedTokenSetting ? (
<Button danger icon={<DeleteOutlined />} loading={isSaving} onClick={() => void handleDelete()}>
</Button>
) : null}
<Button icon={<UnorderedListOutlined />} onClick={closeDetail}>
</Button>
</Space>
<Text type="secondary"> .</Text>
</div>
)}
</Form>
</div>
</Card>
)}
</div>
);
}