feat: expand live chat and work server tools

This commit is contained in:
2026-04-30 11:40:02 +09:00
parent 42ae640470
commit 2df0ba30cb
112 changed files with 15241 additions and 996 deletions

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