feat: update main chat and system chat UI
This commit is contained in:
252
src/app/main/SharedAppSettingsPage.tsx
Normal file
252
src/app/main/SharedAppSettingsPage.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import { ReloadOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Alert, App, Button, Card, Checkbox, Flex, Form, Input, InputNumber, Select, Space, Spin, Typography } from 'antd';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
DEFAULT_APP_CONFIG,
|
||||
getWeeklyScheduleOptions,
|
||||
saveAppConfigToServer,
|
||||
type AppConfig,
|
||||
type PlanCostTimeUnit,
|
||||
type WeeklyScheduleDay,
|
||||
} from './appConfig';
|
||||
import './SharedAppSettingsPage.css';
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
const PLAN_COST_TIME_UNIT_OPTIONS: Array<{ value: PlanCostTimeUnit; label: string }> = [
|
||||
{ value: 'hour', label: '시간' },
|
||||
{ value: 'minute', label: '분' },
|
||||
{ value: 'second', label: '초' },
|
||||
];
|
||||
|
||||
type SharedAppSettingsPageProps = {
|
||||
shareToken: string;
|
||||
};
|
||||
|
||||
type SharedAppSettingsFormValue = AppConfig;
|
||||
|
||||
export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps) {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<SharedAppSettingsFormValue>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [savedConfig, setSavedConfig] = useState<AppConfig>(DEFAULT_APP_CONFIG);
|
||||
|
||||
const loadConfig = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/app-config', {
|
||||
headers: {
|
||||
'X-Chat-Share-Token': shareToken,
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = (await response.json().catch(() => null)) as { message?: string } | null;
|
||||
throw new Error(payload?.message || '앱 설정을 불러오지 못했습니다.');
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as { config?: AppConfig };
|
||||
const nextConfig = payload.config ?? DEFAULT_APP_CONFIG;
|
||||
setSavedConfig(nextConfig);
|
||||
form.setFieldsValue(nextConfig);
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '앱 설정을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [form, shareToken]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfig();
|
||||
}, [loadConfig]);
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (values: SharedAppSettingsFormValue) => {
|
||||
setIsSaving(true);
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const saved = await saveAppConfigToServer(values, {
|
||||
shareToken,
|
||||
skipAutomationNotifications: true,
|
||||
});
|
||||
setSavedConfig(saved);
|
||||
form.setFieldsValue(saved);
|
||||
message.success('앱 설정을 저장했습니다.');
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '앱 설정 저장에 실패했습니다.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[form, message, shareToken],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="shared-app-settings-page shared-app-settings-page--loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shared-app-settings-page">
|
||||
<Flex align="center" justify="space-between" gap={12} wrap>
|
||||
<div>
|
||||
<Title level={4}>앱 설정</Title>
|
||||
<Paragraph type="secondary">
|
||||
공유 링크에서 허용된 핵심 앱 설정만 바로 수정합니다.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadConfig()} disabled={isSaving}>
|
||||
새로고침
|
||||
</Button>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={() => void form.submit()} loading={isSaving}>
|
||||
저장
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
|
||||
|
||||
<Form<SharedAppSettingsFormValue>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={savedConfig}
|
||||
onFinish={(values) => void handleSave(values)}
|
||||
>
|
||||
<div className="shared-app-settings-page__grid">
|
||||
<Card size="small" title="채팅 문맥 설정">
|
||||
<Form.Item label="최근 메시지 수" name={['chat', 'maxContextMessages']}>
|
||||
<InputNumber min={1} max={50} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="최대 문자 수" name={['chat', 'maxContextChars']}>
|
||||
<InputNumber min={500} max={20000} step={100} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Codex Live 최대 실행 시간(초)" name={['chat', 'codexLiveMaxExecutionSeconds']}>
|
||||
<InputNumber min={60} max={7200} step={30} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="무출력 실패 시간(초)" name={['chat', 'codexLiveIdleTimeoutSeconds']}>
|
||||
<InputNumber min={30} max={3600} step={10} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="재기동 완료 자동 실행 대기(초)" name={['chat', 'restartReservationCompletionDelaySeconds']}>
|
||||
<InputNumber min={1} max={300} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name={['chat', 'receiveRoomNotifications']} valuePropName="checked">
|
||||
<Checkbox>채팅방 알림 수신</Checkbox>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="자동접수 / 주기">
|
||||
<Form.Item name={['automation', 'autoRefreshEnabled']} valuePropName="checked">
|
||||
<Checkbox>자동 새로고침 사용</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item label="자동 새로고침 간격(초)" name={['automation', 'autoRefreshIntervalSeconds']}>
|
||||
<InputNumber min={1} max={3600} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="자동접수 방식" name={['automation', 'autoReceiveScheduleType']}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'interval', label: '간격' },
|
||||
{ value: 'daily', label: '매일' },
|
||||
{ value: 'weekly', label: '매주' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="간격(초)" name={['automation', 'autoReceiveIntervalSeconds']}>
|
||||
<InputNumber min={1} max={3600} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="매일 시각" name={['automation', 'autoReceiveDailyTime']}>
|
||||
<Input placeholder="09:00" />
|
||||
</Form.Item>
|
||||
<Form.Item label="매주 요일" name={['automation', 'autoReceiveWeeklyDay']}>
|
||||
<Select options={getWeeklyScheduleOptions()} />
|
||||
</Form.Item>
|
||||
<Form.Item label="매주 시각" name={['automation', 'autoReceiveWeeklyTime']}>
|
||||
<Input placeholder="09:00" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="자동화 기본값">
|
||||
<Form.Item name={['planDefaults', 'jangsingProcessingRequired']} valuePropName="checked">
|
||||
<Checkbox>장싱 처리 필수</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name={['planDefaults', 'autoDeployToMain']} valuePropName="checked">
|
||||
<Checkbox>main 자동 반영</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name={['planDefaults', 'openEditorAfterCreate']} valuePropName="checked">
|
||||
<Checkbox>생성 후 에디터 열기</Checkbox>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="업무일지 자동화">
|
||||
<Form.Item name={['worklogAutomation', 'autoCreateDailyWorklog']} valuePropName="checked">
|
||||
<Checkbox>일일 업무일지 자동 생성</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item label="생성 시각" name={['worklogAutomation', 'dailyCreateTime']}>
|
||||
<Input placeholder="18:00" />
|
||||
</Form.Item>
|
||||
<Form.Item name={['worklogAutomation', 'includeScreenshots']} valuePropName="checked">
|
||||
<Checkbox>스크린샷 포함</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name={['worklogAutomation', 'includeChangedFiles']} valuePropName="checked">
|
||||
<Checkbox>변경 파일 포함</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name={['worklogAutomation', 'includeCommandLogs']} valuePropName="checked">
|
||||
<Checkbox>명령 로그 포함</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item label="템플릿" name={['worklogAutomation', 'template']}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'simple', label: '간단' },
|
||||
{ value: 'detailed', label: '상세' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="비용 표시 / 단축키">
|
||||
<Form.Item label="백만 토큰당 기본 비용" name={['planCost', 'baseCostPerMillionTokens']}>
|
||||
<InputNumber min={100} max={1000000} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="재시도 비용 배수(%)" name={['planCost', 'retryCostMultiplierPercent']}>
|
||||
<InputNumber min={0} max={500} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="시간 비용 배수(%)" name={['planCost', 'hourlyCostMultiplierPercent']}>
|
||||
<InputNumber min={0} max={500} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="시간 단위" name={['planCost', 'timeCostUnit']}>
|
||||
<Select options={PLAN_COST_TIME_UNIT_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item label="주의 배수" name={['planCost', 'attentionCostThresholdMultiplier']}>
|
||||
<InputNumber min={0.1} max={100} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="경고 배수" name={['planCost', 'warningCostThresholdMultiplier']}>
|
||||
<InputNumber min={0.1} max={100} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="고비용 배수" name={['planCost', 'highCostThresholdMultiplier']}>
|
||||
<InputNumber min={0.1} max={100} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="검색 단축키" name={['gestureShortcuts', 'openSearch']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="시스템 채팅 단축키" name={['gestureShortcuts', 'openWindowSearch']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<Text type="secondary">
|
||||
알림 토큰 등록과 업데이트 확인처럼 현재 기기 상태가 필요한 항목은 공유 링크에서 제외했습니다.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user