feat: expand live chat and work server tools
This commit is contained in:
284
src/app/main/AutomationContextManagementPage.tsx
Normal file
284
src/app/main/AutomationContextManagementPage.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import { Alert, Button, Card, Empty, Form, Input, List, Space, Switch, Typography } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { MarkdownPreviewContent } from '../../components/markdownPreview';
|
||||
import {
|
||||
deleteAutomationContext,
|
||||
type AutomationContextRecord,
|
||||
upsertAutomationContext,
|
||||
useAutomationContextRegistry,
|
||||
} from './automationContextAccess';
|
||||
import { useTokenAccess } from './tokenAccess';
|
||||
import './ChatTypeManagementPage.css';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
type AutomationContextFormValue = {
|
||||
id?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
defaultSelected: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_FORM_VALUE: AutomationContextFormValue = {
|
||||
title: '',
|
||||
content: '',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
};
|
||||
|
||||
function toFormValue(context: AutomationContextRecord | null): AutomationContextFormValue {
|
||||
if (!context) {
|
||||
return EMPTY_FORM_VALUE;
|
||||
}
|
||||
|
||||
return {
|
||||
id: context.id,
|
||||
title: context.title,
|
||||
content: context.content,
|
||||
enabled: context.enabled,
|
||||
defaultSelected: context.defaultSelected,
|
||||
};
|
||||
}
|
||||
|
||||
export function AutomationContextManagementPage() {
|
||||
const { hasAccess } = useTokenAccess();
|
||||
const { automationContexts, setAutomationContexts, isLoading, errorMessage } = useAutomationContextRegistry();
|
||||
const [selectedAutomationContextId, setSelectedAutomationContextId] = useState<string | null>(automationContexts[0]?.id ?? null);
|
||||
const [detailMode, setDetailMode] = useState<'list' | 'detail'>('list');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState('');
|
||||
const [form] = Form.useForm<AutomationContextFormValue>();
|
||||
|
||||
const selectedAutomationContext = useMemo(
|
||||
() => automationContexts.find((item) => item.id === selectedAutomationContextId) ?? null,
|
||||
[automationContexts, selectedAutomationContextId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAutomationContextId && automationContexts.some((item) => item.id === selectedAutomationContextId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedAutomationContextId(automationContexts[0]?.id ?? null);
|
||||
}, [automationContexts, selectedAutomationContextId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (detailMode !== 'detail') {
|
||||
return;
|
||||
}
|
||||
|
||||
form.resetFields();
|
||||
form.setFieldsValue(toFormValue(isCreating ? null : selectedAutomationContext));
|
||||
}, [detailMode, form, isCreating, selectedAutomationContext]);
|
||||
|
||||
const openCreateForm = () => {
|
||||
setIsCreating(true);
|
||||
setSelectedAutomationContextId(null);
|
||||
setDetailMode('detail');
|
||||
form.resetFields();
|
||||
form.setFieldsValue(EMPTY_FORM_VALUE);
|
||||
};
|
||||
|
||||
const openDetail = (automationContextId: string) => {
|
||||
setIsCreating(false);
|
||||
setSelectedAutomationContextId(automationContextId);
|
||||
setDetailMode('detail');
|
||||
};
|
||||
|
||||
const closeDetail = () => {
|
||||
setIsCreating(false);
|
||||
setDetailMode('list');
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedAutomationContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`"${selectedAutomationContext.title}" Context를 삭제할까요?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextAutomationContexts = deleteAutomationContext(automationContexts, selectedAutomationContext.id);
|
||||
setIsSaving(true);
|
||||
setSaveErrorMessage('');
|
||||
|
||||
try {
|
||||
const savedAutomationContexts = await setAutomationContexts(nextAutomationContexts);
|
||||
setSelectedAutomationContextId(savedAutomationContexts[0]?.id ?? null);
|
||||
setIsCreating(false);
|
||||
setDetailMode('list');
|
||||
form.resetFields();
|
||||
form.setFieldsValue(EMPTY_FORM_VALUE);
|
||||
} catch (error) {
|
||||
setSaveErrorMessage(error instanceof Error ? error.message : '자동화 Context 삭제에 실패했습니다.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<Card title="Context 관리" className="chat-type-management-page">
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message="관리 페이지는 토큰 등록 사용자만 사용할 수 있습니다."
|
||||
description="설정 > 토큰 관리에서 권한 토큰을 등록한 뒤 Context를 관리하세요."
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`chat-type-management-page${detailMode === 'detail' ? ' chat-type-management-page--detail' : ''}`}>
|
||||
{detailMode === 'list' ? (
|
||||
<Card
|
||||
title="Context 관리"
|
||||
className="chat-type-management-page__card"
|
||||
extra={
|
||||
<Button icon={<PlusOutlined />} onClick={openCreateForm}>
|
||||
신규 Context
|
||||
</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}>등록 Context</Title>
|
||||
<Text type="secondary">{isLoading ? '불러오는 중' : `${automationContexts.length}건`}</Text>
|
||||
</div>
|
||||
{automationContexts.length > 0 ? (
|
||||
<List
|
||||
dataSource={automationContexts}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
className={
|
||||
item.id === selectedAutomationContextId
|
||||
? '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.title}</Text>
|
||||
<Text type="secondary">{item.id}</Text>
|
||||
</Space>
|
||||
<Space size={[8, 8]} wrap style={{ marginTop: 6 }}>
|
||||
<Text type={item.enabled ? undefined : 'secondary'}>{item.enabled ? '사용' : '중지'}</Text>
|
||||
<Text type={item.defaultSelected ? undefined : 'secondary'}>
|
||||
{item.defaultSelected ? '기본 선택' : '기본 해제'}
|
||||
</Text>
|
||||
</Space>
|
||||
<div className="chat-type-management-page__item-description">
|
||||
{item.content ? <MarkdownPreviewContent content={item.content} maxBlocks={3} /> : '본문 없음'}
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="등록된 Context가 없습니다." />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
title={isCreating ? 'Context 등록' : 'Context 상세'}
|
||||
className="chat-type-management-page__card"
|
||||
extra={
|
||||
<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 && selectedAutomationContext ? (
|
||||
<Button
|
||||
danger
|
||||
shape="circle"
|
||||
icon={<DeleteOutlined />}
|
||||
loading={isSaving}
|
||||
aria-label="삭제"
|
||||
onClick={() => void handleDelete()}
|
||||
/>
|
||||
) : null}
|
||||
<Button shape="circle" icon={<UnorderedListOutlined />} aria-label="목록 가기" onClick={closeDetail} />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
|
||||
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={EMPTY_FORM_VALUE}
|
||||
onFinish={async (values) => {
|
||||
const nextAutomationContexts = upsertAutomationContext(automationContexts, values);
|
||||
setIsSaving(true);
|
||||
setSaveErrorMessage('');
|
||||
|
||||
try {
|
||||
const savedAutomationContexts = await setAutomationContexts(nextAutomationContexts);
|
||||
const savedAutomationContext = savedAutomationContexts.find(
|
||||
(item) => item.id === values.id || item.title === values.title,
|
||||
);
|
||||
setIsCreating(false);
|
||||
setSelectedAutomationContextId(savedAutomationContext?.id ?? null);
|
||||
setDetailMode('detail');
|
||||
} catch (error) {
|
||||
setSaveErrorMessage(error instanceof Error ? error.message : '자동화 Context 저장에 실패했습니다.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item name="id" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="제목" name="title" rules={[{ required: true, message: '제목을 입력하세요.' }]}>
|
||||
<Input placeholder="예: 기본 처리" />
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Form.Item label="사용" name="enabled" valuePropName="checked">
|
||||
<Switch checkedChildren="사용" unCheckedChildren="중지" />
|
||||
</Form.Item>
|
||||
<Form.Item label="기본 선택" name="defaultSelected" valuePropName="checked">
|
||||
<Switch checkedChildren="기본" unCheckedChildren="해제" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item label="Context 본문" name="content">
|
||||
<Input.TextArea
|
||||
autoSize={{ minRows: 10, maxRows: 18 }}
|
||||
placeholder={'## 처리 기준\n- 이 Context에서 적용할 규칙을 Markdown으로 정리하세요.'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user