chore: sync local workspace changes
This commit is contained in:
@@ -5,18 +5,20 @@ import { ChatPage } from './pages/ChatPage';
|
||||
import { DocsPage } from './pages/DocsPage';
|
||||
import { PlansPage } from './pages/PlansPage';
|
||||
import { PlayPage } from './pages/PlayPage';
|
||||
import { buildDocsPath, buildPlansPath } from './routes';
|
||||
import { buildChatPath, buildDocsPath } from './routes';
|
||||
|
||||
export function AppShell() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<Navigate to={buildPlansPath('all')} replace />} />
|
||||
<Route index element={<Navigate to={buildChatPath('live')} replace />} />
|
||||
<Route path="docs/:folder" element={<DocsPage />} />
|
||||
<Route path="apis/:section" element={<ApisPage />} />
|
||||
<Route path="plans/:section" element={<PlansPage />} />
|
||||
<Route path="chat/:section" element={<ChatPage />} />
|
||||
<Route path="play/layout" element={<PlayPage />} />
|
||||
<Route path="play/test" element={<PlayPage />} />
|
||||
<Route path="play/cbt" element={<PlayPage />} />
|
||||
<Route path="play/layout-record/:layoutId" element={<PlayPage />} />
|
||||
<Route path="*" element={<Navigate to={buildDocsPath()} replace />} />
|
||||
</Route>
|
||||
|
||||
426
src/app/main/ChatDefaultContextManagementPage.tsx
Normal file
426
src/app/main/ChatDefaultContextManagementPage.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
ArrowsAltOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
SaveOutlined,
|
||||
ShrinkOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Alert, Button, Card, Empty, Form, Input, List, Segmented, Space, Switch, Tag, Typography } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { MarkdownPreviewContent } from '../../components/markdownPreview';
|
||||
import {
|
||||
deleteChatDefaultContext,
|
||||
pruneChatRoomContextSettings,
|
||||
pruneChatTypeDefaultSelections,
|
||||
upsertChatDefaultContext,
|
||||
useChatContextSettingsRegistry,
|
||||
type ChatDefaultContextRecord,
|
||||
} from './chatContextSettingsAccess';
|
||||
import { useTokenAccess } from './tokenAccess';
|
||||
import './ChatTypeManagementPage.css';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
type ChatDefaultContextFormValue = {
|
||||
id?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
const EMPTY_FORM_VALUE: ChatDefaultContextFormValue = {
|
||||
title: '',
|
||||
content: '',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
function toFormValue(record: ChatDefaultContextRecord | null): ChatDefaultContextFormValue {
|
||||
if (!record) {
|
||||
return EMPTY_FORM_VALUE;
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
title: record.title,
|
||||
content: record.content,
|
||||
enabled: record.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
export function ChatDefaultContextManagementPage() {
|
||||
const { hasAccess } = useTokenAccess();
|
||||
const {
|
||||
defaultContexts,
|
||||
chatTypeDefaults,
|
||||
roomContexts,
|
||||
errorMessage: contextSettingsErrorMessage,
|
||||
setStore,
|
||||
} = useChatContextSettingsRegistry();
|
||||
const [selectedContextId, setSelectedContextId] = useState<string | null>(defaultContexts[0]?.id ?? null);
|
||||
const [isMobileViewport, setIsMobileViewport] = useState(false);
|
||||
const [mobileView, setMobileView] = useState<'edit' | 'preview'>('edit');
|
||||
const [detailMode, setDetailMode] = useState<'list' | 'detail'>('list');
|
||||
const [maximizedPane, setMaximizedPane] = useState<'none' | 'edit' | 'preview'>('none');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState('');
|
||||
const [form] = Form.useForm<ChatDefaultContextFormValue>();
|
||||
|
||||
const selectedContext = useMemo(
|
||||
() => defaultContexts.find((item) => item.id === selectedContextId) ?? null,
|
||||
[defaultContexts, selectedContextId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedContextId && defaultContexts.some((item) => item.id === selectedContextId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedContextId(defaultContexts[0]?.id ?? null);
|
||||
}, [defaultContexts, selectedContextId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (detailMode !== 'detail') {
|
||||
return;
|
||||
}
|
||||
|
||||
form.resetFields();
|
||||
form.setFieldsValue(toFormValue(isCreating ? null : selectedContext));
|
||||
}, [detailMode, form, isCreating, selectedContext]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia('(max-width: 960px)');
|
||||
const update = () => {
|
||||
setIsMobileViewport(mediaQuery.matches);
|
||||
if (!mediaQuery.matches) {
|
||||
setMobileView('edit');
|
||||
}
|
||||
};
|
||||
|
||||
update();
|
||||
mediaQuery.addEventListener('change', update);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openCreateForm = () => {
|
||||
setIsCreating(true);
|
||||
setSelectedContextId(null);
|
||||
setDetailMode('detail');
|
||||
setMaximizedPane('none');
|
||||
form.resetFields();
|
||||
form.setFieldsValue(EMPTY_FORM_VALUE);
|
||||
};
|
||||
|
||||
const openDetail = (contextId: string) => {
|
||||
setIsCreating(false);
|
||||
setSelectedContextId(contextId);
|
||||
setDetailMode('detail');
|
||||
setMaximizedPane('none');
|
||||
};
|
||||
|
||||
const closeDetail = () => {
|
||||
setIsCreating(false);
|
||||
setDetailMode('list');
|
||||
setMaximizedPane('none');
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`"${selectedContext.title}" 기본 유형을 삭제할까요?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDefaultContexts = deleteChatDefaultContext(defaultContexts, selectedContext.id);
|
||||
const nextChatTypeDefaults = pruneChatTypeDefaultSelections(chatTypeDefaults, selectedContext.id);
|
||||
const nextRoomContexts = pruneChatRoomContextSettings(roomContexts, selectedContext.id);
|
||||
|
||||
await setStore({
|
||||
defaultContexts: nextDefaultContexts,
|
||||
chatTypeDefaults: nextChatTypeDefaults,
|
||||
roomContexts: nextRoomContexts,
|
||||
});
|
||||
setSelectedContextId(nextDefaultContexts[0]?.id ?? null);
|
||||
setIsCreating(false);
|
||||
setDetailMode('list');
|
||||
form.resetFields();
|
||||
form.setFieldsValue(EMPTY_FORM_VALUE);
|
||||
};
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<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' : ''}${
|
||||
maximizedPane !== 'none' ? ' chat-type-management-page--pane-maximized' : ''
|
||||
}`}
|
||||
>
|
||||
{detailMode === 'list' ? (
|
||||
<Card
|
||||
title="기본 유형 관리"
|
||||
className="chat-type-management-page__card"
|
||||
extra={
|
||||
<Button icon={<PlusOutlined />} onClick={openCreateForm}>
|
||||
신규 기본 유형
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="chat-type-management-page__list">
|
||||
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
|
||||
{contextSettingsErrorMessage ? <Alert showIcon type="error" message={contextSettingsErrorMessage} /> : null}
|
||||
<div className="chat-type-management-page__list-header">
|
||||
<Title level={5}>등록 기본 유형</Title>
|
||||
<Text type="secondary">{`${defaultContexts.length}건`}</Text>
|
||||
</div>
|
||||
{defaultContexts.length > 0 ? (
|
||||
<List
|
||||
dataSource={defaultContexts}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
className={
|
||||
item.id === selectedContextId
|
||||
? '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 />}
|
||||
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>
|
||||
<Tag color={item.enabled ? 'green' : 'default'}>{item.enabled ? '사용' : '중지'}</Tag>
|
||||
</Space>
|
||||
<div className="chat-type-management-page__item-description">
|
||||
{item.content ? <MarkdownPreviewContent content={item.content} maxBlocks={3} /> : '본문 없음'}
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="등록된 기본 유형이 없습니다." />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
title={isCreating ? '기본 유형 등록' : '기본 유형 상세'}
|
||||
className={`chat-type-management-page__card${
|
||||
maximizedPane !== 'none' ? ' chat-type-management-page__card--pane-maximized' : ''
|
||||
}`}
|
||||
extra={
|
||||
<Space size={6} className="chat-type-management-page__header-actions" wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
icon={<SaveOutlined />}
|
||||
aria-label={isCreating ? '등록' : '수정 저장'}
|
||||
onClick={() => {
|
||||
void form.submit();
|
||||
}}
|
||||
/>
|
||||
<Button shape="circle" icon={<PlusOutlined />} aria-label="새 입력" onClick={openCreateForm} />
|
||||
{!isCreating && selectedContext ? (
|
||||
<Button
|
||||
danger
|
||||
shape="circle"
|
||||
icon={<DeleteOutlined />}
|
||||
aria-label="삭제"
|
||||
onClick={() => {
|
||||
void handleDelete();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Button shape="circle" icon={<UnorderedListOutlined />} aria-label="목록 가기" onClick={closeDetail} />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="chat-type-management-page__editor">
|
||||
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
|
||||
{contextSettingsErrorMessage ? <Alert showIcon type="error" message={contextSettingsErrorMessage} /> : null}
|
||||
<Form
|
||||
className="chat-type-management-page__editor-form"
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={EMPTY_FORM_VALUE}
|
||||
onFinish={async (values) => {
|
||||
setSaveErrorMessage('');
|
||||
|
||||
try {
|
||||
const nextDefaultContexts = upsertChatDefaultContext(defaultContexts, values);
|
||||
const savedContext = nextDefaultContexts.find((item) => item.id === values.id || item.title === values.title) ?? null;
|
||||
|
||||
await setStore({
|
||||
defaultContexts: nextDefaultContexts,
|
||||
chatTypeDefaults,
|
||||
roomContexts,
|
||||
});
|
||||
setIsCreating(false);
|
||||
setSelectedContextId(savedContext?.id ?? null);
|
||||
setDetailMode('detail');
|
||||
} catch (error) {
|
||||
setSaveErrorMessage(error instanceof Error ? error.message : '기본 유형 저장에 실패했습니다.');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item name="id" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="chat-type-management-page__editor-scroll">
|
||||
<div className={`chat-type-management-page__meta-grid${maximizedPane !== 'none' ? ' chat-type-management-page__meta-grid--hidden' : ''}`}>
|
||||
<Form.Item
|
||||
className="chat-type-management-page__meta-item chat-type-management-page__meta-item--name"
|
||||
label="기본 유형명"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '기본 유형명을 입력하세요.' }]}
|
||||
>
|
||||
<Input placeholder="예: 모바일 검증 공통 규칙" />
|
||||
</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>
|
||||
</div>
|
||||
<div className="chat-type-management-page__markdown-field">
|
||||
<Text strong className="chat-type-management-page__field-label">
|
||||
기본 유형 본문
|
||||
</Text>
|
||||
<div className="chat-type-management-page__markdown-editor">
|
||||
<div className="chat-type-management-page__editor-toolbar">
|
||||
{isMobileViewport ? (
|
||||
<Segmented
|
||||
className="chat-type-management-page__mobile-toggle"
|
||||
options={[
|
||||
{ label: '입력', value: 'edit' },
|
||||
{ label: '미리보기', value: 'preview' },
|
||||
]}
|
||||
value={mobileView}
|
||||
onChange={(value) => {
|
||||
setMobileView(value as 'edit' | 'preview');
|
||||
setMaximizedPane('none');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Space size={8} wrap>
|
||||
<Button
|
||||
type={maximizedPane === 'edit' ? 'primary' : 'default'}
|
||||
icon={maximizedPane === 'edit' ? <ShrinkOutlined /> : <ArrowsAltOutlined />}
|
||||
onClick={() => {
|
||||
setMaximizedPane((current) => (current === 'edit' ? 'none' : 'edit'));
|
||||
}}
|
||||
>
|
||||
{maximizedPane === 'edit' ? '입력 축소' : '입력 최대화'}
|
||||
</Button>
|
||||
<Button
|
||||
type={maximizedPane === 'preview' ? 'primary' : 'default'}
|
||||
icon={maximizedPane === 'preview' ? <ShrinkOutlined /> : <ArrowsAltOutlined />}
|
||||
onClick={() => {
|
||||
setMaximizedPane((current) => (current === 'preview' ? 'none' : 'preview'));
|
||||
}}
|
||||
>
|
||||
{maximizedPane === 'preview' ? '미리보기 축소' : '미리보기 최대화'}
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`chat-type-management-page__markdown-grid${
|
||||
maximizedPane !== 'none' ? ' chat-type-management-page__markdown-grid--maximized' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`chat-type-management-page__markdown-pane${
|
||||
isMobileViewport && mobileView === 'preview'
|
||||
? ' chat-type-management-page__markdown-pane--mobile-hidden'
|
||||
: ''
|
||||
}${
|
||||
maximizedPane === 'preview' ? ' chat-type-management-page__markdown-pane--desktop-hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="chat-type-management-page__markdown-pane-header">
|
||||
<Text type="secondary">입력</Text>
|
||||
</div>
|
||||
<Form.Item name="content" noStyle>
|
||||
<Input.TextArea
|
||||
autoSize={isMobileViewport ? false : { minRows: 10, maxRows: 18 }}
|
||||
className="chat-type-management-page__markdown-textarea"
|
||||
placeholder={'## 적용 기준\n- 기본 유형의 공통 규칙을 Markdown으로 정의하세요.'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div
|
||||
className={`chat-type-management-page__markdown-pane${
|
||||
isMobileViewport && mobileView === 'edit'
|
||||
? ' chat-type-management-page__markdown-pane--mobile-hidden'
|
||||
: ''
|
||||
}${
|
||||
maximizedPane === 'edit' ? ' chat-type-management-page__markdown-pane--desktop-hidden' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="chat-type-management-page__markdown-preview">
|
||||
<div className="chat-type-management-page__markdown-pane-header">
|
||||
<Text type="secondary">미리보기</Text>
|
||||
</div>
|
||||
<div className="chat-type-management-page__markdown-preview-body">
|
||||
<Form.Item noStyle shouldUpdate={(prev, next) => prev.content !== next.content}>
|
||||
{({ getFieldValue }) => {
|
||||
const content = String(getFieldValue('content') ?? '').trim();
|
||||
|
||||
return content ? (
|
||||
<MarkdownPreviewContent content={content} />
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="미리보기할 기본 유형 본문이 없습니다." />
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAppStore } from '../../store';
|
||||
import { getChatClientSessionId } from './mainChatPanel';
|
||||
import { chatConnectionGateway, chatGateway } from './chatV2';
|
||||
import type { ChatMessage, ChatViewContext } from './mainChatPanel/types';
|
||||
|
||||
@@ -17,8 +17,26 @@ function isStandaloneDisplayMode() {
|
||||
|
||||
export function ChatRuntimeBridgeV2() {
|
||||
const { currentPage, focusedComponentId } = useAppStore();
|
||||
const [sessionId] = useState(() => getChatClientSessionId());
|
||||
const location = useLocation();
|
||||
const [, setMessages] = useState<ChatMessage[]>([]);
|
||||
const sessionId = useMemo(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (currentPage.topMenu !== 'chat') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const pathname = currentUrl.pathname.replace(/\/+$/, '') || '/';
|
||||
|
||||
if (pathname !== '/chat/live') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return currentUrl.searchParams.get('sessionId')?.trim() || '';
|
||||
}, [currentPage.topMenu, location.pathname, location.search]);
|
||||
|
||||
const currentContext: ChatViewContext = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-type-management-page--detail {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.chat-type-management-page .ant-card,
|
||||
.chat-type-management-page .ant-card-body,
|
||||
.chat-type-management-page__card {
|
||||
@@ -61,6 +57,7 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.chat-type-management-page__editor-form {
|
||||
@@ -81,7 +78,11 @@
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: auto;
|
||||
padding: 0 0 8px;
|
||||
padding: 0 0 calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.chat-type-management-page__editor-scroll:has(.chat-type-management-page__markdown-grid--maximized) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-type-management-page__editor-form .ant-form-item {
|
||||
@@ -128,6 +129,55 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 250, 252, 0.82);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-field--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-options {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-option-copy {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chat-type-management-page__markdown-field {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
@@ -190,6 +240,7 @@
|
||||
|
||||
.chat-type-management-page__markdown-grid--maximized {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: min(720px, calc(100dvh - 236px));
|
||||
}
|
||||
|
||||
.chat-type-management-page__markdown-pane {
|
||||
@@ -236,13 +287,13 @@
|
||||
|
||||
.chat-type-management-page__markdown-textarea {
|
||||
height: 100% !important;
|
||||
min-height: 360px;
|
||||
min-height: clamp(360px, calc(100dvh - 360px), 720px);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.chat-type-management-page__markdown-textarea textarea {
|
||||
height: 100% !important;
|
||||
min-height: 360px;
|
||||
min-height: clamp(360px, calc(100dvh - 360px), 720px);
|
||||
overflow: auto !important;
|
||||
resize: none;
|
||||
}
|
||||
@@ -325,12 +376,17 @@
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-form,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-scroll,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-field,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-editor,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-grid {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__field-label {
|
||||
display: none;
|
||||
}
|
||||
@@ -414,6 +470,10 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.chat-type-management-page__default-context-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-type-management-page__meta-item--enabled .ant-form-item-control-input-content {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -421,6 +481,8 @@
|
||||
.chat-type-management-page__markdown-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -465,6 +527,110 @@
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-pane
|
||||
.ant-form-item,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-preview {
|
||||
min-height: clamp(220px, calc(100dvh - 560px), 320px);
|
||||
}
|
||||
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-field,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-editor,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-grid,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-pane,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-preview {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-pane
|
||||
.ant-form-item,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-pane
|
||||
.ant-form-item-control,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-pane
|
||||
.ant-form-item-control-input,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-pane
|
||||
.ant-form-item-control-input-content {
|
||||
flex: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-textarea,
|
||||
.chat-type-management-page:not(.chat-type-management-page--pane-maximized)
|
||||
.chat-type-management-page__markdown-textarea textarea {
|
||||
height: auto !important;
|
||||
min-height: clamp(320px, calc(100dvh - 430px), 520px) !important;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized {
|
||||
height: calc(100dvh - 52px);
|
||||
max-height: calc(100dvh - 52px);
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .ant-card-head {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .ant-card-head-title,
|
||||
.chat-type-management-page--pane-maximized .ant-card-extra {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .ant-card-body {
|
||||
padding: 4px 8px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__card,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-form,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-scroll,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-field,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-editor,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-grid,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-pane,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-preview,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-textarea,
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-textarea textarea {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-scroll {
|
||||
gap: 0;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__editor-toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-grid {
|
||||
height: calc(100dvh - 124px - env(safe-area-inset-bottom, 0px));
|
||||
min-height: calc(100dvh - 124px - env(safe-area-inset-bottom, 0px));
|
||||
max-height: calc(100dvh - 124px - env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized .chat-type-management-page__markdown-pane {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-type-management-page--pane-maximized
|
||||
.chat-type-management-page__markdown-pane
|
||||
.ant-form-item-control-input-content {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.chat-type-management-page__markdown-preview {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
type ChatPermissionRole,
|
||||
type ChatTypeRecord,
|
||||
} from './chatTypeAccess';
|
||||
import {
|
||||
resolveChatTypeDefaultContextIds,
|
||||
upsertChatTypeDefaultContextSelection,
|
||||
useChatContextSettingsRegistry,
|
||||
} from './chatContextSettingsAccess';
|
||||
import { useTokenAccess } from './tokenAccess';
|
||||
import './ChatTypeManagementPage.css';
|
||||
|
||||
@@ -57,6 +62,13 @@ function toFormValue(chatType: ChatTypeRecord | null): ChatTypeFormValue {
|
||||
export function ChatTypeManagementPage() {
|
||||
const { hasAccess } = useTokenAccess();
|
||||
const { chatTypes, setChatTypes, isLoading, errorMessage } = useChatTypeRegistry();
|
||||
const {
|
||||
defaultContexts,
|
||||
chatTypeDefaults,
|
||||
roomContexts,
|
||||
errorMessage: contextSettingsErrorMessage,
|
||||
setStore,
|
||||
} = useChatContextSettingsRegistry();
|
||||
const [selectedChatTypeId, setSelectedChatTypeId] = useState<string | null>(chatTypes[0]?.id ?? null);
|
||||
const [isMobileViewport, setIsMobileViewport] = useState(false);
|
||||
const [mobileView, setMobileView] = useState<'edit' | 'preview'>('edit');
|
||||
@@ -65,6 +77,7 @@ export function ChatTypeManagementPage() {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState('');
|
||||
const [selectedDefaultContextIds, setSelectedDefaultContextIds] = useState<string[]>([]);
|
||||
const [form] = Form.useForm<ChatTypeFormValue>();
|
||||
const userRoles = useMemo(() => resolveCurrentChatPermissionRoles(hasAccess), [hasAccess]);
|
||||
const isPaneMaximized = maximizedPane !== 'none';
|
||||
@@ -89,7 +102,14 @@ export function ChatTypeManagementPage() {
|
||||
|
||||
form.resetFields();
|
||||
form.setFieldsValue(toFormValue(isCreating ? null : selectedChatType));
|
||||
}, [detailMode, form, isCreating, selectedChatType]);
|
||||
setSelectedDefaultContextIds(
|
||||
isCreating
|
||||
? []
|
||||
: resolveChatTypeDefaultContextIds(chatTypeDefaults, selectedChatType?.id).filter((contextId) =>
|
||||
defaultContexts.some((context) => context.id === contextId && context.enabled),
|
||||
),
|
||||
);
|
||||
}, [chatTypeDefaults, defaultContexts, detailMode, form, isCreating, selectedChatType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (detailMode !== 'detail') {
|
||||
@@ -268,6 +288,7 @@ export function ChatTypeManagementPage() {
|
||||
>
|
||||
<div className="chat-type-management-page__list">
|
||||
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
|
||||
{contextSettingsErrorMessage ? <Alert showIcon type="error" message={contextSettingsErrorMessage} /> : null}
|
||||
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
|
||||
<div className="chat-type-management-page__list-header">
|
||||
<Title level={5}>등록 컨텍스트</Title>
|
||||
@@ -279,6 +300,9 @@ export function ChatTypeManagementPage() {
|
||||
dataSource={chatTypes}
|
||||
renderItem={(item) => {
|
||||
const isCurrentUserAllowed = canUseChatType(item, userRoles);
|
||||
const linkedDefaultContexts = resolveChatTypeDefaultContextIds(chatTypeDefaults, item.id)
|
||||
.map((contextId) => defaultContexts.find((context) => context.id === contextId))
|
||||
.filter((context): context is NonNullable<typeof context> => Boolean(context));
|
||||
const itemClassName =
|
||||
item.id === selectedChatTypeId
|
||||
? 'chat-type-management-page__item chat-type-management-page__item--active'
|
||||
@@ -322,6 +346,11 @@ export function ChatTypeManagementPage() {
|
||||
{item.permissions.map((permission) => (
|
||||
<Tag key={`${item.id}-${permission}`}>{CHAT_PERMISSION_ROLE_LABELS[permission]}</Tag>
|
||||
))}
|
||||
{linkedDefaultContexts.map((context) => (
|
||||
<Tag key={`${item.id}-${context.id}`} color="gold">
|
||||
{context.title}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
@@ -341,6 +370,7 @@ export function ChatTypeManagementPage() {
|
||||
>
|
||||
<div className="chat-type-management-page__editor">
|
||||
{errorMessage ? <Alert showIcon type="error" message={errorMessage} /> : null}
|
||||
{contextSettingsErrorMessage ? <Alert showIcon type="error" message={contextSettingsErrorMessage} /> : null}
|
||||
{saveErrorMessage ? <Alert showIcon type="error" message={saveErrorMessage} /> : null}
|
||||
|
||||
<Form
|
||||
@@ -356,6 +386,14 @@ export function ChatTypeManagementPage() {
|
||||
try {
|
||||
const savedChatTypes = await setChatTypes(nextChatTypes);
|
||||
const savedChatType = savedChatTypes.find((item) => item.id === values.id || item.name === values.name);
|
||||
const nextChatTypeDefaults = savedChatType
|
||||
? upsertChatTypeDefaultContextSelection(chatTypeDefaults, savedChatType.id, selectedDefaultContextIds)
|
||||
: chatTypeDefaults;
|
||||
await setStore({
|
||||
defaultContexts,
|
||||
chatTypeDefaults: nextChatTypeDefaults,
|
||||
roomContexts,
|
||||
});
|
||||
setIsCreating(false);
|
||||
setSelectedChatTypeId(savedChatType?.id ?? null);
|
||||
setDetailMode('detail');
|
||||
@@ -400,6 +438,60 @@ export function ChatTypeManagementPage() {
|
||||
<Switch checkedChildren="사용" unCheckedChildren="중지" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className={`chat-type-management-page__default-context-field${isPaneMaximized ? ' chat-type-management-page__default-context-field--hidden' : ''}`}>
|
||||
<div className="chat-type-management-page__default-context-header">
|
||||
<Text strong>기본 유형 연결</Text>
|
||||
<Text type="secondary">여러 개를 선택하면 채팅 요청마다 함께 참조됩니다.</Text>
|
||||
</div>
|
||||
{defaultContexts.filter((context) => context.enabled).length > 0 ? (
|
||||
<>
|
||||
<Checkbox.Group
|
||||
className="chat-type-management-page__default-context-options"
|
||||
value={selectedDefaultContextIds}
|
||||
onChange={(checkedValues) => {
|
||||
setSelectedDefaultContextIds(
|
||||
checkedValues
|
||||
.map((value) => String(value).trim())
|
||||
.filter((value) => defaultContexts.some((context) => context.id === value && context.enabled)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={10} className="chat-type-management-page__default-context-space">
|
||||
{defaultContexts
|
||||
.filter((context) => context.enabled)
|
||||
.map((context) => (
|
||||
<label key={context.id} className="chat-type-management-page__default-context-option">
|
||||
<Checkbox value={context.id}>{context.title}</Checkbox>
|
||||
<Text type="secondary" className="chat-type-management-page__default-context-option-copy">
|
||||
{context.content.split('\n')[0]?.replace(/^#+\s*/, '') || '설명 없음'}
|
||||
</Text>
|
||||
</label>
|
||||
))}
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
{selectedDefaultContextIds.length > 0 ? (
|
||||
<div className="chat-type-management-page__default-context-preview">
|
||||
{selectedDefaultContextIds.map((contextId) => {
|
||||
const context = defaultContexts.find((item) => item.id === contextId);
|
||||
|
||||
return context ? (
|
||||
<Tag key={`preview-${context.id}`} color="gold">
|
||||
{context.title}
|
||||
</Tag>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
showIcon
|
||||
type="info"
|
||||
message="등록된 기본 유형이 없습니다."
|
||||
description="채팅 관리 > 기본 유형 관리에서 Markdown 스타일 기본 유형을 먼저 등록하세요."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="chat-type-management-page__markdown-field">
|
||||
<Text strong className="chat-type-management-page__field-label">
|
||||
기본 문맥 설명
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: calc(100dvh - 128px);
|
||||
max-height: calc(100dvh - 128px);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(59, 130, 246, 0.18), transparent 30%),
|
||||
radial-gradient(circle at bottom left, rgba(14, 165, 233, 0.12), transparent 34%),
|
||||
@@ -186,6 +186,147 @@
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.app-chat-panel__action-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.app-chat-panel__action-group .ant-btn {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.app-chat-panel__action-group--mobile {
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.app-chat-panel__mobile-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-nav {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-content-holder,
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-tabpane-active {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-tabpane-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-section {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.92));
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-section--editor {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-section-head {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-space,
|
||||
.app-chat-panel__context-drawer-radio,
|
||||
.app-chat-panel__context-drawer-checkbox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid rgba(226, 232, 240, 0.96);
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-card--readonly {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-card-copy {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-card-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-textarea-shell {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-textarea {
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 220px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.app-chat-panel__error-layout {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -661,15 +802,15 @@
|
||||
@media (max-width: 1080px) {
|
||||
.app-chat-panel {
|
||||
position: static;
|
||||
height: calc(100dvh - 112px);
|
||||
max-height: calc(100dvh - 112px);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-chat-panel {
|
||||
height: calc(100dvh - 76px);
|
||||
max-height: calc(100dvh - 76px);
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -749,4 +890,44 @@
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-chat-panel__action-group {
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.app-chat-panel__action-group--mobile {
|
||||
gap: 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer {
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-nav {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-tabs .ant-tabs-tab {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-section {
|
||||
padding: 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-section--editor {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-textarea-shell {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__context-drawer-textarea {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
.app-chat-panel--tablet-app .app-chat-panel__conversation-empty,
|
||||
.app-chat-panel--tablet-app .app-chat-panel__conversation-empty-list {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -245,6 +245,12 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -253,6 +259,25 @@
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section--reorderable {
|
||||
position: relative;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-header--reorderable,
|
||||
.app-chat-panel__conversation-section-toggle--reorderable {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-header-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-header--muted {
|
||||
margin-top: 6px;
|
||||
}
|
||||
@@ -316,6 +341,120 @@
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.12);
|
||||
color: #0f172a;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-mobile-header .app-chat-panel__conversation-section-toggle {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle-main {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle-actions,
|
||||
.app-chat-panel__conversation-section-move-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-move-controls {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-move-activator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(226, 232, 240, 0.9);
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-move-activator.is-active {
|
||||
background: rgba(191, 219, 254, 0.95);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-move-button {
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(226, 232, 240, 0.9);
|
||||
color: #334155;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-move-button:disabled {
|
||||
background: rgba(226, 232, 240, 0.52);
|
||||
color: rgba(100, 116, 139, 0.72);
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-move-button:not(:disabled):active {
|
||||
background: rgba(191, 219, 254, 0.95);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle-icon,
|
||||
.app-chat-panel__conversation-section-toggle-caret {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle.is-open {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(59, 130, 246, 0.18),
|
||||
0 10px 24px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle--processing .app-chat-panel__conversation-section-toggle-icon,
|
||||
.app-chat-panel__conversation-section-toggle--processing .app-chat-panel__conversation-section-title {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle--failed .app-chat-panel__conversation-section-toggle-icon,
|
||||
.app-chat-panel__conversation-section-toggle--failed .app-chat-panel__conversation-section-title {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-section-toggle--unread .app-chat-panel__conversation-section-toggle-icon,
|
||||
.app-chat-panel__conversation-section-toggle--unread .app-chat-panel__conversation-section-title {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -649,16 +788,52 @@
|
||||
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.18);
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-item-delete.ant-btn {
|
||||
.app-chat-panel__conversation-item-flag--section {
|
||||
color: #475569;
|
||||
background: rgba(226, 232, 240, 0.76);
|
||||
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-item-flag--request {
|
||||
color: #0f766e;
|
||||
background: rgba(204, 251, 241, 0.96);
|
||||
box-shadow: inset 0 0 0 1px rgba(13, 148, 136, 0.16);
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 4px 4px 4px 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-item-folder.ant-btn,
|
||||
.app-chat-panel__conversation-item-delete.ant-btn {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: auto;
|
||||
margin-right: 4px;
|
||||
height: 28px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-item-delete.ant-btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__general-section-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-chat-panel__general-section-presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-main {
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
@@ -948,6 +1123,46 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.app-chat-panel__resource-chip-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.app-chat-panel__resource-chip-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex: 0 0 22px;
|
||||
border-radius: 8px;
|
||||
background: rgba(226, 232, 240, 0.9);
|
||||
color: #1e293b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-chat-panel__resource-chip-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-chat-panel__resource-chip-meta {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
color: #334155;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-chat-panel__title-input {
|
||||
width: min(240px, 48vw);
|
||||
}
|
||||
@@ -1246,6 +1461,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 820px) and (max-width: 1366px) {
|
||||
.app-chat-panel--ipad-readable .app-chat-message__body,
|
||||
.app-chat-panel--ipad-readable .app-chat-message__body.ant-typography,
|
||||
.app-chat-panel--ipad-readable .app-chat-message__block,
|
||||
.app-chat-panel--ipad-readable .app-chat-message__block .ant-typography,
|
||||
.app-chat-panel--ipad-readable .app-chat-message__block span,
|
||||
.app-chat-panel--ipad-readable .app-chat-message__block a {
|
||||
font-size: 22px !important;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (pointer: fine) {
|
||||
.app-chat-panel .app-chat-message__body,
|
||||
.app-chat-panel .app-chat-message__body.ant-typography {
|
||||
font-size: 19px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1366px) and (pointer: fine) {
|
||||
.app-chat-panel .app-chat-message__body,
|
||||
.app-chat-panel .app-chat-message__body.ant-typography {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app-chat-panel__conversation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1634,11 +1875,15 @@
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 8px 0 10px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
padding: 8px 1px 12px;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.92));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(148, 163, 184, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
box-sizing: border-box;
|
||||
overflow: clip;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -1682,9 +1927,12 @@
|
||||
}
|
||||
|
||||
.app-chat-preview-card--collapsed .app-chat-preview-card__header {
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.92));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(148, 163, 184, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.app-chat-preview-card__meta {
|
||||
@@ -1884,8 +2132,9 @@
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||
padding-top: 8px;
|
||||
padding: 8px 0 1px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-chat-preview-card--fullscreen {
|
||||
@@ -1969,6 +2218,8 @@
|
||||
.app-chat-panel__preview-rich {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-bottom: 1px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-rich .previewer-ui__editor,
|
||||
@@ -2030,6 +2281,66 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-scroll {
|
||||
overflow: auto;
|
||||
max-height: min(420px, 70vh);
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-grid {
|
||||
width: 100%;
|
||||
min-width: max-content;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-grid th,
|
||||
.app-chat-panel__preview-table-grid td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid rgba(226, 232, 240, 0.92);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-grid th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: #eff6ff;
|
||||
color: #1e3a8a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-grid tbody tr:nth-child(even) td {
|
||||
background: rgba(248, 250, 252, 0.82);
|
||||
}
|
||||
|
||||
.app-chat-panel__preview-table-grid tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.app-chat-message__preview-image,
|
||||
.app-chat-message__preview-video,
|
||||
.app-chat-message__preview-frame {
|
||||
@@ -2923,6 +3234,7 @@
|
||||
.app-chat-panel__conversation-list,
|
||||
.app-chat-panel__conversation-main {
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2949,6 +3261,17 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.app-chat-panel input,
|
||||
.app-chat-panel textarea,
|
||||
.app-chat-panel .ant-input,
|
||||
.app-chat-panel .ant-input-affix-wrapper input,
|
||||
.app-chat-panel .ant-select-selection-item,
|
||||
.app-chat-panel .ant-select-selection-placeholder,
|
||||
.app-chat-panel .ant-select-selector,
|
||||
.app-chat-panel .ant-input-textarea textarea.ant-input {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.app-chat-panel__messages,
|
||||
.app-chat-panel__composer,
|
||||
.app-chat-panel__resource-strip {
|
||||
@@ -2977,15 +3300,21 @@
|
||||
.app-chat-panel__messages,
|
||||
.app-chat-panel__preview-stage,
|
||||
.app-chat-panel__resource-strip {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-chat-panel__composer {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: max(2px, min(env(safe-area-inset-bottom, 0px), 8px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-chat-panel__composer textarea.ant-input {
|
||||
@@ -2993,6 +3322,7 @@
|
||||
min-height: clamp(104px, 16dvh, 136px);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.app-chat-panel__composer-input-shell {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@ import { useAppStore } from '../../store';
|
||||
import { LayoutPlaygroundView } from '../../views/play/LayoutPlaygroundView';
|
||||
import { AutomationTypeManagementPage } from './AutomationTypeManagementPage';
|
||||
import { AutomationContextManagementPage } from './AutomationContextManagementPage';
|
||||
import { ChatDefaultContextManagementPage } from './ChatDefaultContextManagementPage';
|
||||
import { ResourceManagementPage } from './ResourceManagementPage';
|
||||
import { ChatTypeManagementPage } from './ChatTypeManagementPage';
|
||||
import { ChatSourceChangesPage } from './ChatSourceChangesPage';
|
||||
import { MainChatPanel } from './MainChatPanel';
|
||||
@@ -30,6 +32,7 @@ const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
export function MainContent({
|
||||
contentExpanded,
|
||||
sidebarOverlayActive = false,
|
||||
onToggleContentExpanded,
|
||||
children,
|
||||
}: MainContentProps) {
|
||||
@@ -204,10 +207,18 @@ export function MainContent({
|
||||
return <ChatSourceChangesPage />;
|
||||
}
|
||||
|
||||
if (selectionId === 'page:chat:resources') {
|
||||
return <ResourceManagementPage />;
|
||||
}
|
||||
|
||||
if (selectionId === 'page:chat:manage') {
|
||||
return <ChatTypeManagementPage />;
|
||||
}
|
||||
|
||||
if (selectionId === 'page:chat:manage-defaults') {
|
||||
return <ChatDefaultContextManagementPage />;
|
||||
}
|
||||
|
||||
if (selectionId === 'page:play:layout') {
|
||||
return <LayoutPlaygroundView />;
|
||||
}
|
||||
@@ -217,7 +228,13 @@ export function MainContent({
|
||||
|
||||
return (
|
||||
<Content
|
||||
className={contentExpanded ? 'app-main-content app-main-content--expanded' : 'app-main-content'}
|
||||
className={[
|
||||
'app-main-content',
|
||||
contentExpanded ? 'app-main-content--expanded' : '',
|
||||
sidebarOverlayActive ? 'app-main-content--under-sidebar-overlay' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClickCapture={(event) => {
|
||||
handleFocusCapture(event.target);
|
||||
}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,17 @@
|
||||
.app-shell {
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: var(--app-viewport-height);
|
||||
min-height: var(--app-viewport-height);
|
||||
max-height: var(--app-viewport-height);
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-shell:has(.app-chat-panel) {
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell:has(.app-main-panel--play-saved) {
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell:has(.app-chat-panel) > .ant-layout {
|
||||
.app-shell__body.ant-layout {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: calc(100dvh - 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell:has(.app-main-panel--play-saved) > .ant-layout {
|
||||
min-height: 0;
|
||||
height: calc(100dvh - 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -200,6 +186,7 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 132px;
|
||||
max-width: min(100%, 240px);
|
||||
padding: 10px 12px;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
@@ -304,6 +291,130 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-header__settings-copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.app-header__settings-meta {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(59, 130, 246, 0.2), transparent 28%),
|
||||
linear-gradient(135deg, rgba(2, 6, 23, 0.84), rgba(15, 23, 42, 0.92));
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-card {
|
||||
width: min(100%, 420px);
|
||||
padding: 24px 22px;
|
||||
border: 1px solid rgba(96, 165, 250, 0.2);
|
||||
border-radius: 26px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.94), rgba(2, 6, 23, 0.98)),
|
||||
rgba(15, 23, 42, 0.96);
|
||||
box-shadow:
|
||||
0 26px 60px rgba(15, 23, 42, 0.42),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(147, 197, 253, 0.88);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-title {
|
||||
display: block;
|
||||
margin-bottom: 14px;
|
||||
color: #f8fafc;
|
||||
font-size: clamp(22px, 4vw, 28px);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
color: #bfdbfe;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-detail {
|
||||
margin: 0 0 16px;
|
||||
color: #cbd5e1;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-steps {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: 14px;
|
||||
background: rgba(15, 23, 42, 0.52);
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-step-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-step--done {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-step--active {
|
||||
color: #f8fafc;
|
||||
border-color: rgba(96, 165, 250, 0.28);
|
||||
background: rgba(30, 41, 59, 0.86);
|
||||
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.14);
|
||||
}
|
||||
|
||||
.app-header__restart-overlay-step--pending {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.app-header__settings-group-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -432,6 +543,9 @@
|
||||
}
|
||||
|
||||
.app-sider.ant-layout-sider {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-right: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
@@ -444,7 +558,7 @@
|
||||
min-width: 100vw !important;
|
||||
max-width: 100vw;
|
||||
flex: 0 0 100vw !important;
|
||||
height: calc(100vh - 72px);
|
||||
height: calc(var(--app-viewport-height) - 72px);
|
||||
border-right: 0;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
transition: none !important;
|
||||
@@ -479,36 +593,32 @@
|
||||
.app-main-content.ant-layout-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: calc(100dvh - 60px);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.app-main-content.ant-layout-content:has(.app-chat-panel) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-content.ant-layout-content:has(.app-main-panel--play-saved) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
.app-main-content--under-sidebar-overlay.ant-layout-content {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-main-content--expanded.ant-layout-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
min-height: var(--app-viewport-height);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-main-panel {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -518,7 +628,7 @@
|
||||
|
||||
.app-main-panel--play-saved {
|
||||
height: 100%;
|
||||
min-height: calc(100dvh - 60px);
|
||||
min-height: calc(var(--app-viewport-height) - 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -535,18 +645,21 @@
|
||||
}
|
||||
|
||||
.app-main-content--expanded.ant-layout-content:has(.app-main-panel--play-saved) {
|
||||
min-height: calc(100dvh - 60px);
|
||||
min-height: calc(var(--app-viewport-height) - 60px);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-panel:has(.app-chat-panel) {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.app-chat-panel) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
@@ -557,10 +670,16 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.chat-type-management-page) {
|
||||
@@ -569,13 +688,124 @@
|
||||
padding: 4px 12px 12px;
|
||||
}
|
||||
|
||||
.app-main-panel:has(.board-page),
|
||||
.app-main-panel:has(.history-page),
|
||||
.app-main-panel:has(.chat-source-changes-page),
|
||||
.app-main-panel:has(.docs-page) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.board-page),
|
||||
.app-main-layout:has(.history-page),
|
||||
.app-main-layout:has(.chat-source-changes-page),
|
||||
.app-main-layout:has(.docs-page) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 4px 12px 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.docs-page) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.docs-page {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.docs-page__card,
|
||||
.docs-page__card.ant-card,
|
||||
.docs-page__card.ant-card .ant-card-body {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.docs-page__card.ant-card {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.docs-page__card.ant-card .ant-card-body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.docs-page__scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 0 0 calc(16px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.docs-page__stack {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-main-panel:has(.resource-management-page) {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.resource-management-page) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 4px 12px 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-panel:has(.plan-board-page),
|
||||
.app-main-panel:has(.plan-schedule-page),
|
||||
.app-main-panel:has(.release-review-page),
|
||||
.app-main-panel:has(.server-command-page),
|
||||
.app-main-panel:has(.test-play-app),
|
||||
.app-main-panel:has(.layout-playground__editor-card) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.plan-board-page),
|
||||
.app-main-layout:has(.plan-schedule-page),
|
||||
.app-main-layout:has(.release-review-page),
|
||||
.app-main-layout:has(.server-command-page),
|
||||
.app-main-layout:has(.test-play-app),
|
||||
.app-main-layout:has(.layout-playground__editor-card) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-panel--play:has(.test-play-app),
|
||||
.app-main-panel--play:has(.layout-playground__editor-card) {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100dvh;
|
||||
height: var(--app-viewport-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
html:has(.chat-type-management-page),
|
||||
@@ -595,12 +825,16 @@
|
||||
.app-shell,
|
||||
.app-main-content.ant-layout-content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.app-main-panel,
|
||||
.app-main-layout {
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-main-panel:has(.app-chat-panel),
|
||||
@@ -609,12 +843,128 @@
|
||||
}
|
||||
|
||||
.app-shell:has(.chat-type-management-page),
|
||||
.app-shell:has(.chat-type-management-page) > .ant-layout {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
height: var(--app-viewport-height);
|
||||
min-height: var(--app-viewport-height);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-content.ant-layout-content:has(.chat-type-management-page),
|
||||
.app-main-panel:has(.chat-type-management-page),
|
||||
.app-main-layout:has(.chat-type-management-page),
|
||||
.chat-type-management-page,
|
||||
.chat-type-management-page__card {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-main-content.ant-layout-content:has(.chat-type-management-page),
|
||||
.app-main-panel:has(.chat-type-management-page),
|
||||
.app-main-layout:has(.chat-type-management-page) {
|
||||
height: calc(var(--app-viewport-height) - 52px);
|
||||
min-height: calc(var(--app-viewport-height) - 52px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell:has(.resource-management-page),
|
||||
.app-shell:has(.resource-management-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.resource-management-page),
|
||||
.app-main-panel:has(.resource-management-page),
|
||||
.app-main-layout:has(.resource-management-page) {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-shell:has(.resource-management-page),
|
||||
.app-shell:has(.resource-management-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.resource-management-page),
|
||||
.app-main-panel:has(.resource-management-page),
|
||||
.app-main-layout:has(.resource-management-page) {
|
||||
height: calc(var(--app-viewport-height) - 52px);
|
||||
min-height: calc(var(--app-viewport-height) - 52px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell:has(.chat-type-management-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.chat-type-management-page),
|
||||
.app-main-panel:has(.chat-type-management-page),
|
||||
.app-main-layout:has(.chat-type-management-page) {
|
||||
height: calc(100dvh - 52px);
|
||||
min-height: calc(100dvh - 52px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-shell:has(.board-page),
|
||||
.app-shell:has(.board-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.board-page),
|
||||
.app-main-panel:has(.board-page),
|
||||
.app-main-layout:has(.board-page),
|
||||
.app-shell:has(.history-page),
|
||||
.app-shell:has(.history-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.history-page),
|
||||
.app-main-panel:has(.history-page),
|
||||
.app-main-layout:has(.history-page),
|
||||
.app-shell:has(.chat-source-changes-page),
|
||||
.app-shell:has(.chat-source-changes-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.chat-source-changes-page),
|
||||
.app-main-panel:has(.chat-source-changes-page),
|
||||
.app-main-layout:has(.chat-source-changes-page),
|
||||
.app-shell:has(.docs-page),
|
||||
.app-shell:has(.docs-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.docs-page),
|
||||
.app-main-panel:has(.docs-page),
|
||||
.app-main-layout:has(.docs-page) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-shell:has(.docs-page),
|
||||
.app-shell:has(.docs-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.docs-page),
|
||||
.app-main-panel:has(.docs-page) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.docs-page) {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.app-shell:has(.plan-board-page),
|
||||
.app-shell:has(.plan-board-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.plan-board-page),
|
||||
.app-main-panel:has(.plan-board-page),
|
||||
.app-main-layout:has(.plan-board-page),
|
||||
.app-shell:has(.plan-schedule-page),
|
||||
.app-shell:has(.plan-schedule-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.plan-schedule-page),
|
||||
.app-main-panel:has(.plan-schedule-page),
|
||||
.app-main-layout:has(.plan-schedule-page),
|
||||
.app-shell:has(.release-review-page),
|
||||
.app-shell:has(.release-review-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.release-review-page),
|
||||
.app-main-panel:has(.release-review-page),
|
||||
.app-main-layout:has(.release-review-page),
|
||||
.app-shell:has(.server-command-page),
|
||||
.app-shell:has(.server-command-page) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.server-command-page),
|
||||
.app-main-panel:has(.server-command-page),
|
||||
.app-main-layout:has(.server-command-page),
|
||||
.app-shell:has(.test-play-app),
|
||||
.app-shell:has(.test-play-app) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.test-play-app),
|
||||
.app-main-panel:has(.test-play-app),
|
||||
.app-main-layout:has(.test-play-app),
|
||||
.app-shell:has(.layout-playground__editor-card),
|
||||
.app-shell:has(.layout-playground__editor-card) > .ant-layout,
|
||||
.app-main-content.ant-layout-content:has(.layout-playground__editor-card),
|
||||
.app-main-panel:has(.layout-playground__editor-card),
|
||||
.app-main-layout:has(.layout-playground__editor-card) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -623,8 +973,8 @@
|
||||
.app-main-content.ant-layout-content:has(.app-main-panel--play-saved),
|
||||
.app-main-layout:has(.app-main-panel--play-saved),
|
||||
.app-main-panel--play-saved {
|
||||
height: calc(100dvh - 52px);
|
||||
min-height: calc(100dvh - 52px);
|
||||
height: calc(var(--app-viewport-height) - 52px);
|
||||
min-height: calc(var(--app-viewport-height) - 52px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -718,7 +1068,7 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100dvh - 92px);
|
||||
min-height: calc(var(--app-viewport-height) - 92px);
|
||||
overflow: hidden;
|
||||
border-radius: 24px;
|
||||
}
|
||||
@@ -806,14 +1156,14 @@
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.app-main-panel:has(.app-chat-panel) {
|
||||
height: calc(100dvh - 60px);
|
||||
min-height: calc(100dvh - 60px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.app-chat-panel) {
|
||||
height: calc(100dvh - 60px);
|
||||
min-height: calc(100dvh - 60px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
@@ -821,10 +1171,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-shell:has(.app-chat-panel) > .ant-layout {
|
||||
height: calc(100dvh - 52px);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding: 6px 10px;
|
||||
height: 52px;
|
||||
@@ -874,7 +1220,7 @@
|
||||
.app-sider--mobile.ant-layout-sider {
|
||||
position: fixed;
|
||||
inset: 52px 0 0;
|
||||
height: calc(100vh - 52px);
|
||||
height: calc(var(--app-viewport-height) - 52px);
|
||||
}
|
||||
|
||||
.app-sider--mobile-inline.ant-layout-sider {
|
||||
@@ -884,21 +1230,33 @@
|
||||
|
||||
.app-main-content.ant-layout-content {
|
||||
padding: 0;
|
||||
min-height: calc(100dvh - 52px);
|
||||
}
|
||||
|
||||
.app-main-layout {
|
||||
min-height: calc(100dvh - 52px);
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
.app-main-layout,
|
||||
.app-main-layout:has(.chat-type-management-page),
|
||||
.app-main-layout:has(.docs-page),
|
||||
.app-main-layout:has(.resource-management-page),
|
||||
.app-main-layout:has(.board-page),
|
||||
.app-main-layout:has(.history-page),
|
||||
.app-main-layout:has(.chat-source-changes-page),
|
||||
.app-main-layout:has(.plan-board-page),
|
||||
.app-main-layout:has(.plan-schedule-page),
|
||||
.app-main-layout:has(.release-review-page),
|
||||
.app-main-layout:has(.server-command-page),
|
||||
.app-main-layout:has(.test-play-app),
|
||||
.app-main-layout:has(.layout-playground__editor-card),
|
||||
.app-main-layout:has(.app-main-panel--play-saved) {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.app-main-panel--play-saved {
|
||||
min-height: calc(100dvh - 52px);
|
||||
min-height: calc(var(--app-viewport-height) - 52px);
|
||||
}
|
||||
|
||||
.app-main-layout:has(.app-main-panel--play-saved) {
|
||||
min-height: calc(100dvh - 52px);
|
||||
min-height: calc(var(--app-viewport-height) - 52px);
|
||||
}
|
||||
|
||||
.app-main-layout:has(.chat-type-management-page) {
|
||||
@@ -906,12 +1264,17 @@
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.docs-page) {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.app-main-window-layer {
|
||||
inset: 8px;
|
||||
}
|
||||
|
||||
.app-main-window-layer__stage {
|
||||
min-height: calc(100dvh - 68px);
|
||||
min-height: calc(var(--app-viewport-height) - 68px);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
@@ -935,8 +1298,28 @@
|
||||
padding: 10px 16px 16px;
|
||||
}
|
||||
|
||||
.docs-page__scroll {
|
||||
padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.app-main-panel:has(.app-chat-panel) {
|
||||
height: calc(100dvh - 76px);
|
||||
min-height: calc(100dvh - 76px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.app-main-layout:has(.plan-board-page),
|
||||
.app-main-layout:has(.plan-schedule-page),
|
||||
.app-main-layout:has(.release-review-page),
|
||||
.app-main-layout:has(.server-command-page),
|
||||
.app-main-layout:has(.test-play-app),
|
||||
.app-main-layout:has(.layout-playground__editor-card) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main-panel:has(.test-play-app),
|
||||
.app-main-panel:has(.layout-playground__editor-card) {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export function MainSidebar({
|
||||
: [...(docsMenuItems ?? []), ...(apiMenuItems ?? [])]
|
||||
: effectiveTopMenu === 'play'
|
||||
? [...(playMenuItems ?? [])]
|
||||
: [...(planMenuItems ?? []), ...(chatMenuItems ?? [])];
|
||||
: [...(chatMenuItems ?? []), ...(planMenuItems ?? [])];
|
||||
const rootKeys = sidebarItems.flatMap((item) =>
|
||||
item && typeof item === 'object' && 'key' in item && typeof item.key === 'string' ? [item.key] : [],
|
||||
);
|
||||
|
||||
657
src/app/main/ResourceManagementPage.css
Normal file
657
src/app/main/ResourceManagementPage.css
Normal file
@@ -0,0 +1,657 @@
|
||||
.resource-management-page {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 320px) minmax(320px, 1fr) minmax(360px, 1.08fr);
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.resource-management-page__mobile-nav,
|
||||
.resource-management-page__mobile-card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resource-management-page__sidebar,
|
||||
.resource-management-page__content,
|
||||
.resource-management-page__preview-card {
|
||||
min-height: 0;
|
||||
border-radius: 22px;
|
||||
overflow: clip;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resource-management-page__sidebar.ant-card,
|
||||
.resource-management-page__content.ant-card,
|
||||
.resource-management-page__preview-card.ant-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(191, 204, 229, 0.92),
|
||||
0 16px 40px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.resource-management-page__sidebar .ant-card-head,
|
||||
.resource-management-page__content .ant-card-head,
|
||||
.resource-management-page__preview-card .ant-card-head {
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.resource-management-page__sidebar .ant-card-body,
|
||||
.resource-management-page__content .ant-card-body,
|
||||
.resource-management-page__preview-card .ant-card-body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.resource-management-page__scope-copy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.resource-management-page__tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.resource-management-page__tree .ant-tree-node-content-wrapper {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.resource-management-page__tree-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.resource-management-page__content {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__workspace {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.resource-management-page__toolbar-main,
|
||||
.resource-management-page__toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.resource-management-page__guide {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__list-shell {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(249, 251, 255, 0.96), rgba(241, 246, 255, 0.92)),
|
||||
#fff;
|
||||
box-shadow: inset 0 0 0 1px #dfe6f4;
|
||||
overflow: clip;
|
||||
box-sizing: border-box;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.resource-management-page__list-header,
|
||||
.resource-management-page__list-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 160px 88px 76px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.resource-management-page__list-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e9eef8;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.resource-management-page__list-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.resource-management-page__list-row {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eef2fa;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.18s ease, transform 0.18s ease;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.resource-management-page__list-row:hover {
|
||||
background: rgba(222, 234, 255, 0.42);
|
||||
}
|
||||
|
||||
.resource-management-page__list-row--selected {
|
||||
background: rgba(186, 209, 255, 0.34);
|
||||
}
|
||||
|
||||
.resource-management-page__list-row--parent {
|
||||
background: rgba(241, 245, 255, 0.92);
|
||||
}
|
||||
|
||||
.resource-management-page__list-name,
|
||||
.resource-management-page__entry-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__list-meta {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.resource-management-page__entry-name-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-card .ant-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-frame {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px #d9e1f2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resource-management-page__text-preview {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px #d9e1f2;
|
||||
box-sizing: border-box;
|
||||
color: #111827;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.resource-management-page__image-preview {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px #d9e1f2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resource-management-page__zoom-shell,
|
||||
.resource-management-page__preview-modal-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__zoom-shell {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: clip;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
background: #0b1220;
|
||||
box-shadow: inset 0 0 0 1px #d9e1f2;
|
||||
box-sizing: border-box;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
.resource-management-page__zoom-shell--touch-zoom {
|
||||
touch-action: none;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.resource-management-page__zoom-shell--image {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.resource-management-page__image-preview--zoomable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.resource-management-page__frame-zoom-shell {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
transform-origin: center center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-frame--zoomable {
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
transform-origin: center center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
gap: 12px;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
border-top: 1px solid rgba(217, 225, 242, 0.9);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
backdrop-filter: blur(14px);
|
||||
flex: 0 0 auto;
|
||||
min-height: 72px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-toolbar-slider {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-toolbar-slider .ant-slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-toolbar-button {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
padding-inline: 0;
|
||||
flex: 0 0 44px;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-toolbar-value {
|
||||
flex: 0 0 56px;
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-body {
|
||||
overflow: hidden;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-shell .resource-management-page__preview-modal-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__editor {
|
||||
min-height: 260px;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
.resource-management-page__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu {
|
||||
position: fixed;
|
||||
z-index: 1200;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 208px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.16);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1199;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: #111827;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu button:hover {
|
||||
background: rgba(222, 234, 255, 0.42);
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu button.danger {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu button.danger:hover {
|
||||
background: rgba(254, 226, 226, 0.72);
|
||||
}
|
||||
|
||||
.resource-management-page__file-input {
|
||||
position: fixed;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
inset: auto auto -100vh -100vw;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.resource-management-page {
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.resource-management-page__preview-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.resource-management-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.resource-management-page--mobile {
|
||||
min-height: 0;
|
||||
padding-inline: 1px;
|
||||
padding-bottom: max(10px, calc(env(safe-area-inset-bottom, 0px) + 6px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resource-management-page--mobile > .resource-management-page__sidebar,
|
||||
.resource-management-page--mobile > .resource-management-page__content,
|
||||
.resource-management-page--mobile > .resource-management-page__preview-card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resource-management-page__mobile-nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.resource-management-page__mobile-card {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__mobile-card > .ant-card {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.resource-management-page__sidebar .ant-card-body,
|
||||
.resource-management-page__content .ant-card-body,
|
||||
.resource-management-page__preview-card .ant-card-body {
|
||||
padding: 14px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.resource-management-page__list-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.resource-management-page__list-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-areas:
|
||||
'name actions'
|
||||
'meta meta';
|
||||
row-gap: 6px;
|
||||
}
|
||||
|
||||
.resource-management-page__list-name {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
.resource-management-page__list-meta {
|
||||
grid-area: meta;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.resource-management-page__list-meta span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.resource-management-page__list-row > .ant-space {
|
||||
grid-area: actions;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.resource-management-page__context-menu {
|
||||
min-width: min(208px, calc(100vw - 24px));
|
||||
}
|
||||
|
||||
.resource-management-page__toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.resource-management-page__toolbar-main,
|
||||
.resource-management-page__toolbar-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.resource-management-page__toolbar-actions .ant-btn {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-frame,
|
||||
.resource-management-page__image-preview,
|
||||
.resource-management-page__text-preview {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.resource-management-page__editor {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-card .ant-tabs,
|
||||
.resource-management-page__preview-card .ant-tabs-content-holder,
|
||||
.resource-management-page__preview-card .ant-tabs-content,
|
||||
.resource-management-page__preview-card .ant-tabs-tabpane {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-card .ant-tabs-content-holder {
|
||||
overflow: hidden;
|
||||
padding-bottom: 1px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-card .ant-tabs-tabpane-active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resource-management-page--panel-preview .resource-management-page__preview-card .ant-card-body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-content {
|
||||
border-radius: 0;
|
||||
min-height: 100dvh;
|
||||
background: #0b1220;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-header {
|
||||
padding: calc(12px + env(safe-area-inset-top, 0px)) 64px 12px 16px;
|
||||
margin-bottom: 0;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-title {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-close {
|
||||
top: calc(8px + env(safe-area-inset-top, 0px));
|
||||
right: 8px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
color: #f8fafc;
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
border: 1px solid rgba(226, 232, 240, 0.22);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-close:hover,
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-close:focus-visible {
|
||||
color: #ffffff;
|
||||
background: rgba(30, 41, 59, 0.98);
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .ant-modal-body {
|
||||
height: calc(100dvh - 56px) !important;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__preview-modal-shell .resource-management-page__preview-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__zoom-shell {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__zoom-shell--image {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__preview-frame,
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__image-preview {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__preview-modal-toolbar {
|
||||
gap: 10px;
|
||||
padding-inline: 12px;
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__preview-modal-toolbar .ant-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.resource-management-page__preview-modal-wrap--mobile .resource-management-page__preview-modal-toolbar-value {
|
||||
flex-basis: 52px;
|
||||
width: 52px;
|
||||
}
|
||||
}
|
||||
1840
src/app/main/ResourceManagementPage.tsx
Normal file
1840
src/app/main/ResourceManagementPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
641
src/app/main/appConfig.js
Normal file
641
src/app/main/appConfig.js
Normal file
@@ -0,0 +1,641 @@
|
||||
"use strict";
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
if (typeof b !== "function" && b !== null)
|
||||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DEFAULT_APP_CONFIG = exports.APP_CONFIG_STORAGE_KEY = void 0;
|
||||
exports.fetchAppConfigFromServer = fetchAppConfigFromServer;
|
||||
exports.saveAppConfigToServer = saveAppConfigToServer;
|
||||
exports.saveAutomationNotificationPreferenceToServer = saveAutomationNotificationPreferenceToServer;
|
||||
exports.syncAppConfigFromServer = syncAppConfigFromServer;
|
||||
exports.getStoredAppConfig = getStoredAppConfig;
|
||||
exports.setStoredAppConfig = setStoredAppConfig;
|
||||
exports.updateStoredAppConfig = updateStoredAppConfig;
|
||||
exports.useAppConfig = useAppConfig;
|
||||
exports.describeAutoReceiveSchedule = describeAutoReceiveSchedule;
|
||||
exports.getWeeklyScheduleOptions = getWeeklyScheduleOptions;
|
||||
var react_1 = require("react");
|
||||
var clientIdentity_1 = require("./clientIdentity");
|
||||
var notificationIdentity_1 = require("./notificationIdentity");
|
||||
exports.APP_CONFIG_STORAGE_KEY = 'work-server.app-config';
|
||||
var APP_CONFIG_EVENT = 'work-server:app-config';
|
||||
var APP_CONFIG_API_PATH = '/app-config';
|
||||
var AUTOMATION_NOTIFICATION_PREFERENCE_API_PATH = '/notifications/preferences/automation';
|
||||
var APP_CONFIG_REQUEST_TIMEOUT_MS = 8000;
|
||||
var cachedConfig = null;
|
||||
var cachedRawConfig = null;
|
||||
exports.DEFAULT_APP_CONFIG = {
|
||||
chat: {
|
||||
maxContextMessages: 12,
|
||||
maxContextChars: 3200,
|
||||
codexLiveMaxExecutionSeconds: 600,
|
||||
codexLiveIdleTimeoutSeconds: 180,
|
||||
receiveRoomNotifications: true,
|
||||
},
|
||||
automation: {
|
||||
autoRefreshEnabled: true,
|
||||
autoRefreshIntervalSeconds: 5,
|
||||
autoReceiveScheduleType: 'interval',
|
||||
autoReceiveIntervalSeconds: 30,
|
||||
autoReceiveDailyTime: '09:00',
|
||||
autoReceiveWeeklyDay: 'mon',
|
||||
autoReceiveWeeklyTime: '09:00',
|
||||
notifyOnAutomationStart: true,
|
||||
notifyOnAutomationProgress: true,
|
||||
notifyOnAutomationCompletion: true,
|
||||
notifyOnAutomationRelease: true,
|
||||
notifyOnAutomationMain: true,
|
||||
notifyOnAutomationFailure: true,
|
||||
notifyOnAutomationRestart: true,
|
||||
notifyOnAutomationIssueResolved: true,
|
||||
},
|
||||
worklogAutomation: {
|
||||
autoCreateDailyWorklog: false,
|
||||
dailyCreateTime: '18:00',
|
||||
repeatRequestEnabled: false,
|
||||
repeatIntervalMinutes: 60,
|
||||
includeScreenshots: true,
|
||||
includeChangedFiles: true,
|
||||
includeCommandLogs: true,
|
||||
template: 'detailed',
|
||||
},
|
||||
planDefaults: {
|
||||
jangsingProcessingRequired: true,
|
||||
autoDeployToMain: true,
|
||||
openEditorAfterCreate: true,
|
||||
},
|
||||
planCost: {
|
||||
baseCostPerMillionTokens: 10000,
|
||||
retryCostMultiplierPercent: 15,
|
||||
hourlyCostMultiplierPercent: 0,
|
||||
timeCostUnit: 'hour',
|
||||
attentionCostThresholdMultiplier: 1,
|
||||
warningCostThresholdMultiplier: 2,
|
||||
highCostThresholdMultiplier: 4,
|
||||
},
|
||||
gestureShortcuts: {
|
||||
openSearch: 'Mod+K',
|
||||
openWindowSearch: 'Mod+Shift+K',
|
||||
},
|
||||
};
|
||||
var WEEKLY_DAY_LABELS = {
|
||||
mon: '월요일',
|
||||
tue: '화요일',
|
||||
wed: '수요일',
|
||||
thu: '목요일',
|
||||
fri: '금요일',
|
||||
sat: '토요일',
|
||||
sun: '일요일',
|
||||
};
|
||||
var AUTOMATION_NOTIFICATION_KEYS = [
|
||||
'notifyOnAutomationStart',
|
||||
'notifyOnAutomationProgress',
|
||||
'notifyOnAutomationCompletion',
|
||||
'notifyOnAutomationRelease',
|
||||
'notifyOnAutomationMain',
|
||||
'notifyOnAutomationFailure',
|
||||
'notifyOnAutomationRestart',
|
||||
'notifyOnAutomationIssueResolved',
|
||||
];
|
||||
function clampIntervalSeconds(value, fallback) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(3600, Math.max(1, Math.round(value)));
|
||||
}
|
||||
function normalizeTimeValue(value, fallback) {
|
||||
if (/^\d{2}:\d{2}$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
function normalizeScheduleType(value) {
|
||||
if (value === 'daily' || value === 'weekly') {
|
||||
return value;
|
||||
}
|
||||
return 'interval';
|
||||
}
|
||||
function normalizeWeeklyDay(value) {
|
||||
if (value && value in WEEKLY_DAY_LABELS) {
|
||||
return value;
|
||||
}
|
||||
return exports.DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyDay;
|
||||
}
|
||||
function normalizeWorklogTemplate(value) {
|
||||
if (value === 'simple') {
|
||||
return 'simple';
|
||||
}
|
||||
return 'detailed';
|
||||
}
|
||||
function normalizeShortcutValue(value, fallback) {
|
||||
var trimmed = value === null || value === void 0 ? void 0 : value.trim();
|
||||
if (!trimmed) {
|
||||
return fallback;
|
||||
}
|
||||
return trimmed
|
||||
.split('+')
|
||||
.map(function (token) { return token.trim(); })
|
||||
.filter(Boolean)
|
||||
.join('+');
|
||||
}
|
||||
function normalizePlanCostValue(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(1000000, Math.max(100, Math.round(value)));
|
||||
}
|
||||
function normalizePlanCostMultiplierValue(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(100, Math.max(0.1, Math.round(value * 10) / 10));
|
||||
}
|
||||
function normalizePlanCostPercentValue(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(500, Math.max(0, Math.round(value)));
|
||||
}
|
||||
function normalizePlanCostTimeUnit(value) {
|
||||
if (value === 'minute' || value === 'second') {
|
||||
return value;
|
||||
}
|
||||
return 'hour';
|
||||
}
|
||||
function normalizeChatContextMessageLimit(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(50, Math.max(1, Math.round(value)));
|
||||
}
|
||||
function normalizeChatContextCharLimit(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(20000, Math.max(500, Math.round(value)));
|
||||
}
|
||||
function normalizeCodexLiveMaxExecutionSeconds(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(7200, Math.max(60, Math.round(value)));
|
||||
}
|
||||
function normalizeCodexLiveIdleTimeoutSeconds(value, fallback) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(3600, Math.max(30, Math.round(value)));
|
||||
}
|
||||
function normalizeBooleanValue(value, fallback) {
|
||||
if (typeof value !== 'boolean') {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function normalizeConfig(raw) {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
|
||||
var chat = raw === null || raw === void 0 ? void 0 : raw.chat;
|
||||
var automation = raw === null || raw === void 0 ? void 0 : raw.automation;
|
||||
var worklogAutomation = raw === null || raw === void 0 ? void 0 : raw.worklogAutomation;
|
||||
var planDefaults = raw === null || raw === void 0 ? void 0 : raw.planDefaults;
|
||||
var planCost = raw === null || raw === void 0 ? void 0 : raw.planCost;
|
||||
var gestureShortcuts = raw === null || raw === void 0 ? void 0 : raw.gestureShortcuts;
|
||||
return {
|
||||
chat: {
|
||||
maxContextMessages: normalizeChatContextMessageLimit(chat === null || chat === void 0 ? void 0 : chat.maxContextMessages, exports.DEFAULT_APP_CONFIG.chat.maxContextMessages),
|
||||
maxContextChars: normalizeChatContextCharLimit(chat === null || chat === void 0 ? void 0 : chat.maxContextChars, exports.DEFAULT_APP_CONFIG.chat.maxContextChars),
|
||||
codexLiveMaxExecutionSeconds: normalizeCodexLiveMaxExecutionSeconds(chat === null || chat === void 0 ? void 0 : chat.codexLiveMaxExecutionSeconds, exports.DEFAULT_APP_CONFIG.chat.codexLiveMaxExecutionSeconds),
|
||||
codexLiveIdleTimeoutSeconds: normalizeCodexLiveIdleTimeoutSeconds(chat === null || chat === void 0 ? void 0 : chat.codexLiveIdleTimeoutSeconds, exports.DEFAULT_APP_CONFIG.chat.codexLiveIdleTimeoutSeconds),
|
||||
receiveRoomNotifications: normalizeBooleanValue(chat === null || chat === void 0 ? void 0 : chat.receiveRoomNotifications, exports.DEFAULT_APP_CONFIG.chat.receiveRoomNotifications),
|
||||
},
|
||||
automation: {
|
||||
autoRefreshEnabled: (_a = automation === null || automation === void 0 ? void 0 : automation.autoRefreshEnabled) !== null && _a !== void 0 ? _a : exports.DEFAULT_APP_CONFIG.automation.autoRefreshEnabled,
|
||||
autoRefreshIntervalSeconds: clampIntervalSeconds((_b = automation === null || automation === void 0 ? void 0 : automation.autoRefreshIntervalSeconds) !== null && _b !== void 0 ? _b : exports.DEFAULT_APP_CONFIG.automation.autoRefreshIntervalSeconds, exports.DEFAULT_APP_CONFIG.automation.autoRefreshIntervalSeconds),
|
||||
autoReceiveScheduleType: normalizeScheduleType(automation === null || automation === void 0 ? void 0 : automation.autoReceiveScheduleType),
|
||||
autoReceiveIntervalSeconds: clampIntervalSeconds((_c = automation === null || automation === void 0 ? void 0 : automation.autoReceiveIntervalSeconds) !== null && _c !== void 0 ? _c : exports.DEFAULT_APP_CONFIG.automation.autoReceiveIntervalSeconds, exports.DEFAULT_APP_CONFIG.automation.autoReceiveIntervalSeconds),
|
||||
autoReceiveDailyTime: normalizeTimeValue((_d = automation === null || automation === void 0 ? void 0 : automation.autoReceiveDailyTime) !== null && _d !== void 0 ? _d : exports.DEFAULT_APP_CONFIG.automation.autoReceiveDailyTime, exports.DEFAULT_APP_CONFIG.automation.autoReceiveDailyTime),
|
||||
autoReceiveWeeklyDay: normalizeWeeklyDay(automation === null || automation === void 0 ? void 0 : automation.autoReceiveWeeklyDay),
|
||||
autoReceiveWeeklyTime: normalizeTimeValue((_e = automation === null || automation === void 0 ? void 0 : automation.autoReceiveWeeklyTime) !== null && _e !== void 0 ? _e : exports.DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyTime, exports.DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyTime),
|
||||
notifyOnAutomationStart: (_f = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationStart) !== null && _f !== void 0 ? _f : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationStart,
|
||||
notifyOnAutomationProgress: (_g = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationProgress) !== null && _g !== void 0 ? _g : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationProgress,
|
||||
notifyOnAutomationCompletion: (_h = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationCompletion) !== null && _h !== void 0 ? _h : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationCompletion,
|
||||
notifyOnAutomationRelease: (_j = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationRelease) !== null && _j !== void 0 ? _j : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationRelease,
|
||||
notifyOnAutomationMain: (_k = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationMain) !== null && _k !== void 0 ? _k : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationMain,
|
||||
notifyOnAutomationFailure: (_l = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationFailure) !== null && _l !== void 0 ? _l : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationFailure,
|
||||
notifyOnAutomationRestart: (_m = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationRestart) !== null && _m !== void 0 ? _m : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationRestart,
|
||||
notifyOnAutomationIssueResolved: (_o = automation === null || automation === void 0 ? void 0 : automation.notifyOnAutomationIssueResolved) !== null && _o !== void 0 ? _o : exports.DEFAULT_APP_CONFIG.automation.notifyOnAutomationIssueResolved,
|
||||
},
|
||||
worklogAutomation: {
|
||||
autoCreateDailyWorklog: (_p = worklogAutomation === null || worklogAutomation === void 0 ? void 0 : worklogAutomation.autoCreateDailyWorklog) !== null && _p !== void 0 ? _p : exports.DEFAULT_APP_CONFIG.worklogAutomation.autoCreateDailyWorklog,
|
||||
dailyCreateTime: normalizeTimeValue((_q = worklogAutomation === null || worklogAutomation === void 0 ? void 0 : worklogAutomation.dailyCreateTime) !== null && _q !== void 0 ? _q : exports.DEFAULT_APP_CONFIG.worklogAutomation.dailyCreateTime, exports.DEFAULT_APP_CONFIG.worklogAutomation.dailyCreateTime),
|
||||
repeatRequestEnabled: false,
|
||||
repeatIntervalMinutes: exports.DEFAULT_APP_CONFIG.worklogAutomation.repeatIntervalMinutes,
|
||||
includeScreenshots: (_r = worklogAutomation === null || worklogAutomation === void 0 ? void 0 : worklogAutomation.includeScreenshots) !== null && _r !== void 0 ? _r : exports.DEFAULT_APP_CONFIG.worklogAutomation.includeScreenshots,
|
||||
includeChangedFiles: (_s = worklogAutomation === null || worklogAutomation === void 0 ? void 0 : worklogAutomation.includeChangedFiles) !== null && _s !== void 0 ? _s : exports.DEFAULT_APP_CONFIG.worklogAutomation.includeChangedFiles,
|
||||
includeCommandLogs: (_t = worklogAutomation === null || worklogAutomation === void 0 ? void 0 : worklogAutomation.includeCommandLogs) !== null && _t !== void 0 ? _t : exports.DEFAULT_APP_CONFIG.worklogAutomation.includeCommandLogs,
|
||||
template: normalizeWorklogTemplate(worklogAutomation === null || worklogAutomation === void 0 ? void 0 : worklogAutomation.template),
|
||||
},
|
||||
planDefaults: {
|
||||
jangsingProcessingRequired: (_u = planDefaults === null || planDefaults === void 0 ? void 0 : planDefaults.jangsingProcessingRequired) !== null && _u !== void 0 ? _u : exports.DEFAULT_APP_CONFIG.planDefaults.jangsingProcessingRequired,
|
||||
autoDeployToMain: (_v = planDefaults === null || planDefaults === void 0 ? void 0 : planDefaults.autoDeployToMain) !== null && _v !== void 0 ? _v : exports.DEFAULT_APP_CONFIG.planDefaults.autoDeployToMain,
|
||||
openEditorAfterCreate: (_w = planDefaults === null || planDefaults === void 0 ? void 0 : planDefaults.openEditorAfterCreate) !== null && _w !== void 0 ? _w : exports.DEFAULT_APP_CONFIG.planDefaults.openEditorAfterCreate,
|
||||
},
|
||||
planCost: {
|
||||
baseCostPerMillionTokens: normalizePlanCostValue(planCost === null || planCost === void 0 ? void 0 : planCost.baseCostPerMillionTokens, exports.DEFAULT_APP_CONFIG.planCost.baseCostPerMillionTokens),
|
||||
retryCostMultiplierPercent: normalizePlanCostPercentValue(planCost === null || planCost === void 0 ? void 0 : planCost.retryCostMultiplierPercent, exports.DEFAULT_APP_CONFIG.planCost.retryCostMultiplierPercent),
|
||||
hourlyCostMultiplierPercent: normalizePlanCostPercentValue(planCost === null || planCost === void 0 ? void 0 : planCost.hourlyCostMultiplierPercent, exports.DEFAULT_APP_CONFIG.planCost.hourlyCostMultiplierPercent),
|
||||
timeCostUnit: normalizePlanCostTimeUnit(planCost === null || planCost === void 0 ? void 0 : planCost.timeCostUnit),
|
||||
attentionCostThresholdMultiplier: normalizePlanCostMultiplierValue(planCost === null || planCost === void 0 ? void 0 : planCost.attentionCostThresholdMultiplier, exports.DEFAULT_APP_CONFIG.planCost.attentionCostThresholdMultiplier),
|
||||
warningCostThresholdMultiplier: normalizePlanCostMultiplierValue(planCost === null || planCost === void 0 ? void 0 : planCost.warningCostThresholdMultiplier, exports.DEFAULT_APP_CONFIG.planCost.warningCostThresholdMultiplier),
|
||||
highCostThresholdMultiplier: normalizePlanCostMultiplierValue(planCost === null || planCost === void 0 ? void 0 : planCost.highCostThresholdMultiplier, exports.DEFAULT_APP_CONFIG.planCost.highCostThresholdMultiplier),
|
||||
},
|
||||
gestureShortcuts: {
|
||||
openSearch: normalizeShortcutValue(gestureShortcuts === null || gestureShortcuts === void 0 ? void 0 : gestureShortcuts.openSearch, exports.DEFAULT_APP_CONFIG.gestureShortcuts.openSearch),
|
||||
openWindowSearch: normalizeShortcutValue(gestureShortcuts === null || gestureShortcuts === void 0 ? void 0 : gestureShortcuts.openWindowSearch, exports.DEFAULT_APP_CONFIG.gestureShortcuts.openWindowSearch),
|
||||
},
|
||||
};
|
||||
}
|
||||
function pickAutomationNotificationSettings(automation) {
|
||||
return AUTOMATION_NOTIFICATION_KEYS.reduce(function (picked, key) {
|
||||
picked[key] = automation[key];
|
||||
return picked;
|
||||
}, {});
|
||||
}
|
||||
function mergeAutomationNotificationSettings(config, automation) {
|
||||
if (!automation) {
|
||||
return config;
|
||||
}
|
||||
return normalizeConfig(__assign(__assign({}, config), { automation: __assign(__assign({}, config.automation), automation) }));
|
||||
}
|
||||
function getCurrentAppOrigin() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return window.location.origin;
|
||||
}
|
||||
function getCurrentAppDomain() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return window.location.hostname;
|
||||
}
|
||||
function emitConfigChange() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new Event(APP_CONFIG_EVENT));
|
||||
}
|
||||
function resolveAppConfigApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
return '/api';
|
||||
}
|
||||
function resolveAppConfigFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
var hostname = window.location.hostname;
|
||||
var isLocalWorkServerHost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';
|
||||
if (!isLocalWorkServerHost) {
|
||||
return null;
|
||||
}
|
||||
var fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
var APP_CONFIG_API_BASE_URL = resolveAppConfigApiBaseUrl();
|
||||
var APP_CONFIG_FALLBACK_BASE_URL = resolveAppConfigFallbackBaseUrl();
|
||||
var AppConfigApiError = /** @class */ (function (_super) {
|
||||
__extends(AppConfigApiError, _super);
|
||||
function AppConfigApiError(message, status) {
|
||||
var _this = _super.call(this, message) || this;
|
||||
_this.name = 'AppConfigApiError';
|
||||
_this.status = status;
|
||||
return _this;
|
||||
}
|
||||
return AppConfigApiError;
|
||||
}(Error));
|
||||
function requestAppConfigOnce(baseUrl, path, init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var headers, hasBody, method, controller, timeoutId, appOrigin, appDomain, response, error_1, text, payload, contentType, text;
|
||||
var _a, _b, _c, _d;
|
||||
return __generator(this, function (_e) {
|
||||
switch (_e.label) {
|
||||
case 0:
|
||||
headers = (0, clientIdentity_1.appendClientIdHeader)(init === null || init === void 0 ? void 0 : init.headers);
|
||||
hasBody = (init === null || init === void 0 ? void 0 : init.body) !== undefined && init.body !== null;
|
||||
method = (_b = (_a = init === null || init === void 0 ? void 0 : init.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : 'GET';
|
||||
controller = new AbortController();
|
||||
timeoutId = setTimeout(function () { return controller.abort(); }, APP_CONFIG_REQUEST_TIMEOUT_MS);
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
appOrigin = getCurrentAppOrigin();
|
||||
appDomain = getCurrentAppDomain();
|
||||
if (appOrigin && !headers.has('X-App-Origin')) {
|
||||
headers.set('X-App-Origin', appOrigin);
|
||||
}
|
||||
if (appDomain && !headers.has('X-App-Domain')) {
|
||||
headers.set('X-App-Domain', appDomain);
|
||||
}
|
||||
_e.label = 1;
|
||||
case 1:
|
||||
_e.trys.push([1, 3, , 4]);
|
||||
return [4 /*yield*/, fetch("".concat(baseUrl).concat(path), __assign(__assign({}, init), { headers: headers, signal: controller.signal, cache: (_c = init === null || init === void 0 ? void 0 : init.cache) !== null && _c !== void 0 ? _c : (method === 'GET' ? 'no-store' : undefined) }))];
|
||||
case 2:
|
||||
response = _e.sent();
|
||||
return [3 /*break*/, 4];
|
||||
case 3:
|
||||
error_1 = _e.sent();
|
||||
clearTimeout(timeoutId);
|
||||
if (error_1 instanceof DOMException && error_1.name === 'AbortError') {
|
||||
throw new AppConfigApiError('서버 응답이 지연됩니다.', 408);
|
||||
}
|
||||
throw error_1;
|
||||
case 4:
|
||||
clearTimeout(timeoutId);
|
||||
if (!!response.ok) return [3 /*break*/, 6];
|
||||
return [4 /*yield*/, response.text()];
|
||||
case 5:
|
||||
text = _e.sent();
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
throw new AppConfigApiError(payload.message || '요청 처리에 실패했습니다.', response.status);
|
||||
}
|
||||
catch (_f) {
|
||||
throw new AppConfigApiError(text || '요청 처리에 실패했습니다.', response.status);
|
||||
}
|
||||
_e.label = 6;
|
||||
case 6:
|
||||
contentType = (_d = response.headers.get('content-type')) !== null && _d !== void 0 ? _d : '';
|
||||
if (!!contentType.toLowerCase().includes('application/json')) return [3 /*break*/, 8];
|
||||
return [4 /*yield*/, response.text()];
|
||||
case 7:
|
||||
text = _e.sent();
|
||||
throw new AppConfigApiError(text ? '서버 응답이 JSON이 아닙니다.' : '서버 응답을 확인할 수 없습니다.', 502);
|
||||
case 8: return [2 /*return*/, response.json()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function requestAppConfig(path, init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var error_2, shouldRetryWithFallback;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
_a.trys.push([0, 2, , 3]);
|
||||
return [4 /*yield*/, requestAppConfigOnce(APP_CONFIG_API_BASE_URL, path, init)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
case 2:
|
||||
error_2 = _a.sent();
|
||||
shouldRetryWithFallback = APP_CONFIG_FALLBACK_BASE_URL &&
|
||||
APP_CONFIG_FALLBACK_BASE_URL !== APP_CONFIG_API_BASE_URL &&
|
||||
(error_2 instanceof AppConfigApiError
|
||||
? error_2.status === 404 || error_2.status === 408 || error_2.status === 502
|
||||
: error_2 instanceof Error && /404|not found|Failed to fetch|Load failed|NetworkError/i.test(error_2.message));
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error_2;
|
||||
}
|
||||
return [2 /*return*/, requestAppConfigOnce(APP_CONFIG_FALLBACK_BASE_URL, path, init)];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function fetchAppConfigFromServer() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var response, config, preference, _a;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
_b.trys.push([0, 3, , 4]);
|
||||
return [4 /*yield*/, requestAppConfig(APP_CONFIG_API_PATH)];
|
||||
case 1:
|
||||
response = _b.sent();
|
||||
config = normalizeConfig(response.config);
|
||||
return [4 /*yield*/, fetchAutomationNotificationPreferenceFromServer()];
|
||||
case 2:
|
||||
preference = _b.sent();
|
||||
return [2 /*return*/, mergeAutomationNotificationSettings(config, preference)];
|
||||
case 3:
|
||||
_a = _b.sent();
|
||||
return [2 /*return*/, null];
|
||||
case 4: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function fetchAutomationNotificationPreferenceFromServer() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var target, query, response, _a;
|
||||
var _b;
|
||||
return __generator(this, function (_c) {
|
||||
switch (_c.label) {
|
||||
case 0:
|
||||
_c.trys.push([0, 2, , 3]);
|
||||
target = (0, notificationIdentity_1.getAutomationNotificationPreferenceTarget)();
|
||||
query = target ? "?".concat(new URLSearchParams(target).toString()) : '';
|
||||
return [4 /*yield*/, requestAppConfig("".concat(AUTOMATION_NOTIFICATION_PREFERENCE_API_PATH).concat(query))];
|
||||
case 1:
|
||||
response = _c.sent();
|
||||
return [2 /*return*/, (_b = response.automation) !== null && _b !== void 0 ? _b : null];
|
||||
case 2:
|
||||
_a = _c.sent();
|
||||
return [2 /*return*/, null];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function saveAppConfigToServer(config) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var response;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, requestAppConfig(APP_CONFIG_API_PATH, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config: config }),
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
return [2 /*return*/, normalizeConfig(response.config)];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function saveAutomationNotificationPreferenceToServer(config) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var target, response;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
target = (0, notificationIdentity_1.getAutomationNotificationPreferenceTarget)();
|
||||
return [4 /*yield*/, requestAppConfig(AUTOMATION_NOTIFICATION_PREFERENCE_API_PATH, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(__assign(__assign({}, (target !== null && target !== void 0 ? target : {})), { automation: pickAutomationNotificationSettings(config.automation) })),
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
return [2 /*return*/, mergeAutomationNotificationSettings(config, response.automation)];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function syncAppConfigFromServer() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var config;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, fetchAppConfigFromServer()];
|
||||
case 1:
|
||||
config = _a.sent();
|
||||
if (!config) {
|
||||
return [2 /*return*/, false];
|
||||
}
|
||||
setStoredAppConfig(config);
|
||||
return [2 /*return*/, true];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function getStoredAppConfig() {
|
||||
if (typeof window === 'undefined') {
|
||||
return exports.DEFAULT_APP_CONFIG;
|
||||
}
|
||||
try {
|
||||
var raw = window.localStorage.getItem(exports.APP_CONFIG_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
cachedConfig = exports.DEFAULT_APP_CONFIG;
|
||||
cachedRawConfig = null;
|
||||
return exports.DEFAULT_APP_CONFIG;
|
||||
}
|
||||
if (raw === cachedRawConfig && cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
var normalized = normalizeConfig(JSON.parse(raw));
|
||||
cachedConfig = normalized;
|
||||
cachedRawConfig = raw;
|
||||
return normalized;
|
||||
}
|
||||
catch (_a) {
|
||||
cachedConfig = exports.DEFAULT_APP_CONFIG;
|
||||
cachedRawConfig = null;
|
||||
return exports.DEFAULT_APP_CONFIG;
|
||||
}
|
||||
}
|
||||
function setStoredAppConfig(config) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
var normalized = normalizeConfig(config);
|
||||
var raw = JSON.stringify(normalized);
|
||||
cachedConfig = normalized;
|
||||
cachedRawConfig = raw;
|
||||
window.localStorage.setItem(exports.APP_CONFIG_STORAGE_KEY, raw);
|
||||
emitConfigChange();
|
||||
}
|
||||
function updateStoredAppConfig(updater) {
|
||||
var current = getStoredAppConfig();
|
||||
var next = updater(current);
|
||||
setStoredAppConfig(next);
|
||||
}
|
||||
function subscribeToAppConfig(callback) {
|
||||
if (typeof window === 'undefined') {
|
||||
return function () { return undefined; };
|
||||
}
|
||||
var handleChange = function () {
|
||||
callback();
|
||||
};
|
||||
window.addEventListener(APP_CONFIG_EVENT, handleChange);
|
||||
window.addEventListener('storage', handleChange);
|
||||
return function () {
|
||||
window.removeEventListener(APP_CONFIG_EVENT, handleChange);
|
||||
window.removeEventListener('storage', handleChange);
|
||||
};
|
||||
}
|
||||
function useAppConfig() {
|
||||
return (0, react_1.useSyncExternalStore)(subscribeToAppConfig, getStoredAppConfig, function () { return exports.DEFAULT_APP_CONFIG; });
|
||||
}
|
||||
function describeAutoReceiveSchedule(config) {
|
||||
var automation = config.automation;
|
||||
if (automation.autoReceiveScheduleType === 'daily') {
|
||||
return "\uB9E4\uC77C ".concat(automation.autoReceiveDailyTime);
|
||||
}
|
||||
if (automation.autoReceiveScheduleType === 'weekly') {
|
||||
return "\uB9E4\uC8FC ".concat(WEEKLY_DAY_LABELS[automation.autoReceiveWeeklyDay], " ").concat(automation.autoReceiveWeeklyTime);
|
||||
}
|
||||
return "".concat(automation.autoReceiveIntervalSeconds, "\uCD08\uB9C8\uB2E4");
|
||||
}
|
||||
function getWeeklyScheduleOptions() {
|
||||
return Object.entries(WEEKLY_DAY_LABELS).map(function (_a) {
|
||||
var value = _a[0], label = _a[1];
|
||||
return ({
|
||||
value: value,
|
||||
label: label,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export type AppConfig = {
|
||||
codexLiveMaxExecutionSeconds: number;
|
||||
codexLiveIdleTimeoutSeconds: number;
|
||||
receiveRoomNotifications: boolean;
|
||||
restartReservationCompletionDelaySeconds: number;
|
||||
};
|
||||
automation: {
|
||||
autoRefreshEnabled: boolean;
|
||||
@@ -76,6 +77,7 @@ export const DEFAULT_APP_CONFIG: AppConfig = {
|
||||
codexLiveMaxExecutionSeconds: 600,
|
||||
codexLiveIdleTimeoutSeconds: 180,
|
||||
receiveRoomNotifications: true,
|
||||
restartReservationCompletionDelaySeconds: 10,
|
||||
},
|
||||
automation: {
|
||||
autoRefreshEnabled: true,
|
||||
@@ -265,6 +267,14 @@ function normalizeCodexLiveIdleTimeoutSeconds(value: number | undefined, fallbac
|
||||
return Math.min(3600, Math.max(30, Math.round(value)));
|
||||
}
|
||||
|
||||
function normalizeRestartReservationCompletionDelaySeconds(value: number | undefined, fallback: number) {
|
||||
if (value === undefined || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return Math.min(300, Math.max(1, Math.round(value)));
|
||||
}
|
||||
|
||||
function normalizeBooleanValue(value: boolean | undefined, fallback: boolean) {
|
||||
if (typeof value !== 'boolean') {
|
||||
return fallback;
|
||||
@@ -300,6 +310,10 @@ function normalizeConfig(raw?: Partial<AppConfig>): AppConfig {
|
||||
chat?.receiveRoomNotifications,
|
||||
DEFAULT_APP_CONFIG.chat.receiveRoomNotifications,
|
||||
),
|
||||
restartReservationCompletionDelaySeconds: normalizeRestartReservationCompletionDelaySeconds(
|
||||
chat?.restartReservationCompletionDelaySeconds,
|
||||
DEFAULT_APP_CONFIG.chat.restartReservationCompletionDelaySeconds,
|
||||
),
|
||||
},
|
||||
automation: {
|
||||
autoRefreshEnabled: automation?.autoRefreshEnabled ?? DEFAULT_APP_CONFIG.automation.autoRefreshEnabled,
|
||||
@@ -423,6 +437,22 @@ function mergeAutomationNotificationSettings(
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentAppOrigin() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
function getCurrentAppDomain() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return window.location.hostname;
|
||||
}
|
||||
|
||||
function emitConfigChange() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -485,6 +515,17 @@ async function requestAppConfigOnce<T>(baseUrl: string, path: string, init?: Req
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
const appOrigin = getCurrentAppOrigin();
|
||||
const appDomain = getCurrentAppDomain();
|
||||
|
||||
if (appOrigin && !headers.has('X-App-Origin')) {
|
||||
headers.set('X-App-Origin', appOrigin);
|
||||
}
|
||||
|
||||
if (appDomain && !headers.has('X-App-Domain')) {
|
||||
headers.set('X-App-Domain', appDomain);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
|
||||
398
src/app/main/automationContextAccess.js
Normal file
398
src/app/main/automationContextAccess.js
Normal file
@@ -0,0 +1,398 @@
|
||||
"use strict";
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DEFAULT_AUTOMATION_CONTEXTS = void 0;
|
||||
exports.sanitizeAutomationContexts = sanitizeAutomationContexts;
|
||||
exports.upsertAutomationContext = upsertAutomationContext;
|
||||
exports.deleteAutomationContext = deleteAutomationContext;
|
||||
exports.buildAutomationContextOptions = buildAutomationContextOptions;
|
||||
exports.resolveDefaultAutomationContextIds = resolveDefaultAutomationContextIds;
|
||||
exports.useAutomationContextRegistry = useAutomationContextRegistry;
|
||||
var react_1 = require("react");
|
||||
var clientIdentity_1 = require("./clientIdentity");
|
||||
var AUTOMATION_CONTEXTS_API_PATH = '/automation-contexts';
|
||||
var AUTOMATION_CONTEXT_SYNC_EVENT = 'work-app:automation-contexts-changed';
|
||||
var AUTOMATION_CONTEXT_REQUEST_TIMEOUT_MS = 8000;
|
||||
exports.DEFAULT_AUTOMATION_CONTEXTS = [
|
||||
{
|
||||
id: 'general-inquiry-default',
|
||||
title: '기본 확인',
|
||||
content: '소스 수정 없이 조회, 상태 확인, 응답 정리를 우선합니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'none-default',
|
||||
title: '기본 처리',
|
||||
content: '## 기본 처리\n- 현재 저장소 규칙과 요청 본문을 우선 따릅니다.\n- 소스 수정이 필요하면 관련 파일을 수정하고, 필요 없으면 확인 결과만 정리합니다.\n- 작업 결과에는 변경 파일, diff, preview 같은 증적을 남길 수 있습니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'plan-default',
|
||||
title: '문서형 처리',
|
||||
content: 'Plan 등록/문서형 요청으로 처리하고, 구현보다 등록/정리 결과를 우선 남깁니다.',
|
||||
enabled: true,
|
||||
defaultSelected: false,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'command-execution-default',
|
||||
title: '명령 실행',
|
||||
content: '저장소 수정 없이 명령 실행, API 확인, 상태 조회 중심으로 처리합니다.',
|
||||
enabled: true,
|
||||
defaultSelected: false,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'non-source-work-default',
|
||||
title: '비소스 작업',
|
||||
content: '문서, 운영, 데이터 확인 등 비소스 작업으로 처리하고 코드 수정은 요청 시에만 제한적으로 수행합니다.',
|
||||
enabled: true,
|
||||
defaultSelected: false,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'auto-worker-default',
|
||||
title: '자동화 기본 규칙',
|
||||
content: '## context 사용 규칙\n- 자동화 실행기는 선택된 Context만 우선 참조합니다.\n- Codex Live 문맥이나 일반 채팅 문맥은 자동화 기본 context로 섞지 않습니다.\n\n## 처리 원칙\n- 세부 절차는 현재 운영 설정을 따릅니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
function normalizeText(value) {
|
||||
var _a;
|
||||
return (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
|
||||
}
|
||||
function compareContextUpdatedAt(left, right) {
|
||||
var leftTime = Date.parse(left.updatedAt);
|
||||
var rightTime = Date.parse(right.updatedAt);
|
||||
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
||||
return leftTime - rightTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function normalizeAutomationContext(record) {
|
||||
var title = normalizeText(record.title);
|
||||
var content = normalizeText(record.content);
|
||||
if (!title && !content) {
|
||||
return null;
|
||||
}
|
||||
var id = normalizeText(record.id) ||
|
||||
"automation-context-".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 8));
|
||||
return {
|
||||
id: id,
|
||||
title: title || 'Context',
|
||||
content: content,
|
||||
enabled: record.enabled !== false,
|
||||
defaultSelected: record.defaultSelected !== false,
|
||||
updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
function sanitizeAutomationContexts(items) {
|
||||
var byId = new Map();
|
||||
var bySemanticKey = new Map();
|
||||
(items !== null && items !== void 0 ? items : [])
|
||||
.map(function (item) { return normalizeAutomationContext(item); })
|
||||
.filter(function (item) { return Boolean(item); })
|
||||
.forEach(function (item) {
|
||||
var currentById = byId.get(item.id);
|
||||
if (!currentById || compareContextUpdatedAt(currentById, item) <= 0) {
|
||||
byId.set(item.id, item);
|
||||
}
|
||||
});
|
||||
for (var _i = 0, _a = byId.values(); _i < _a.length; _i++) {
|
||||
var item = _a[_i];
|
||||
var semanticKey = normalizeText(item.title).replace(/\s+/g, ' ').toLocaleLowerCase('ko-KR');
|
||||
var current = bySemanticKey.get(semanticKey);
|
||||
if (!current || compareContextUpdatedAt(current, item) <= 0) {
|
||||
bySemanticKey.set(semanticKey, item);
|
||||
}
|
||||
}
|
||||
var values = Array.from(bySemanticKey.values()).sort(function (left, right) { return left.title.localeCompare(right.title, 'ko-KR'); });
|
||||
return values.length > 0 ? values : exports.DEFAULT_AUTOMATION_CONTEXTS;
|
||||
}
|
||||
function emitAutomationContextsChange() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new Event(AUTOMATION_CONTEXT_SYNC_EVENT));
|
||||
}
|
||||
function resolveApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
return '/api';
|
||||
}
|
||||
function resolveFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
var hostname = window.location.hostname;
|
||||
if (!['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname)) {
|
||||
return null;
|
||||
}
|
||||
var fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
var API_BASE_URL = resolveApiBaseUrl();
|
||||
var FALLBACK_BASE_URL = resolveFallbackBaseUrl();
|
||||
function requestOnce(baseUrl, init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var headers, hasBody, controller, timeoutId, response, _a;
|
||||
var _b, _c;
|
||||
return __generator(this, function (_d) {
|
||||
switch (_d.label) {
|
||||
case 0:
|
||||
headers = (0, clientIdentity_1.appendClientIdHeader)(init === null || init === void 0 ? void 0 : init.headers);
|
||||
hasBody = (init === null || init === void 0 ? void 0 : init.body) !== undefined && (init === null || init === void 0 ? void 0 : init.body) !== null;
|
||||
controller = new AbortController();
|
||||
timeoutId = window.setTimeout(function () { return controller.abort(); }, AUTOMATION_CONTEXT_REQUEST_TIMEOUT_MS);
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
_d.label = 1;
|
||||
case 1:
|
||||
_d.trys.push([1, , 5, 6]);
|
||||
return [4 /*yield*/, fetch("".concat(baseUrl).concat(AUTOMATION_CONTEXTS_API_PATH), __assign(__assign({}, init), { headers: headers, signal: controller.signal, cache: (_b = init === null || init === void 0 ? void 0 : init.cache) !== null && _b !== void 0 ? _b : (((_c = init === null || init === void 0 ? void 0 : init.method) === null || _c === void 0 ? void 0 : _c.toUpperCase()) === 'GET' ? 'no-store' : undefined) }))];
|
||||
case 2:
|
||||
response = _d.sent();
|
||||
if (!!response.ok) return [3 /*break*/, 4];
|
||||
_a = Error.bind;
|
||||
return [4 /*yield*/, response.text()];
|
||||
case 3: throw new (_a.apply(Error, [void 0, (_d.sent()) || '자동화 Context 요청에 실패했습니다.']))();
|
||||
case 4: return [2 /*return*/, response.json()];
|
||||
case 5:
|
||||
window.clearTimeout(timeoutId);
|
||||
return [7 /*endfinally*/];
|
||||
case 6: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function requestAutomationContexts(init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var error_1, shouldRetryWithFallback;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
_a.trys.push([0, 2, , 3]);
|
||||
return [4 /*yield*/, requestOnce(API_BASE_URL, init)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
case 2:
|
||||
error_1 = _a.sent();
|
||||
shouldRetryWithFallback = FALLBACK_BASE_URL &&
|
||||
FALLBACK_BASE_URL !== API_BASE_URL &&
|
||||
error_1 instanceof Error &&
|
||||
/404|408|502|Failed to fetch|Load failed|NetworkError|응답이 지연/i.test(error_1.message);
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error_1;
|
||||
}
|
||||
return [2 /*return*/, requestOnce(FALLBACK_BASE_URL, init)];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function loadAutomationContextsFromServer() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var response;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, requestAutomationContexts({
|
||||
method: 'GET',
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
if (response.automationContexts == null) {
|
||||
return [2 /*return*/, exports.DEFAULT_AUTOMATION_CONTEXTS];
|
||||
}
|
||||
return [2 /*return*/, sanitizeAutomationContexts(response.automationContexts)];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function saveAutomationContextsToServer(items) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var resolved, response;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
resolved = sanitizeAutomationContexts(items);
|
||||
return [4 /*yield*/, requestAutomationContexts({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ automationContexts: resolved }),
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
return [2 /*return*/, sanitizeAutomationContexts(response.automationContexts)];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function upsertAutomationContext(items, input) {
|
||||
var nextItem = normalizeAutomationContext(input);
|
||||
if (!nextItem) {
|
||||
return sanitizeAutomationContexts(items);
|
||||
}
|
||||
var nextItems = items.filter(function (item) { return item.id !== nextItem.id; });
|
||||
nextItems.push(nextItem);
|
||||
return sanitizeAutomationContexts(nextItems);
|
||||
}
|
||||
function deleteAutomationContext(items, automationContextId) {
|
||||
var normalizedId = normalizeText(automationContextId);
|
||||
if (!normalizedId) {
|
||||
return sanitizeAutomationContexts(items);
|
||||
}
|
||||
return sanitizeAutomationContexts(items.filter(function (item) { return item.id !== normalizedId; }));
|
||||
}
|
||||
function buildAutomationContextOptions(items, selectedContextIds) {
|
||||
if (selectedContextIds === void 0) { selectedContextIds = []; }
|
||||
var contexts = sanitizeAutomationContexts(items);
|
||||
var selectedSet = new Set(selectedContextIds);
|
||||
var enabledIds = new Set(contexts.filter(function (item) { return item.enabled; }).map(function (item) { return item.id; }));
|
||||
return contexts
|
||||
.filter(function (item) { return enabledIds.has(item.id) || selectedSet.has(item.id); })
|
||||
.map(function (item) { return ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
}); });
|
||||
}
|
||||
function resolveDefaultAutomationContextIds(items) {
|
||||
return sanitizeAutomationContexts(items)
|
||||
.filter(function (item) { return item.enabled && item.defaultSelected; })
|
||||
.map(function (item) { return item.id; });
|
||||
}
|
||||
function useAutomationContextRegistry() {
|
||||
var _this = this;
|
||||
var _a = (0, react_1.useState)(exports.DEFAULT_AUTOMATION_CONTEXTS), automationContexts = _a[0], setAutomationContextsState = _a[1];
|
||||
var _b = (0, react_1.useState)(true), isLoading = _b[0], setIsLoading = _b[1];
|
||||
var _c = (0, react_1.useState)(''), errorMessage = _c[0], setErrorMessage = _c[1];
|
||||
var mountedRef = (0, react_1.useRef)(true);
|
||||
(0, react_1.useEffect)(function () {
|
||||
mountedRef.current = true;
|
||||
return function () {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
(0, react_1.useEffect)(function () {
|
||||
var cancelled = false;
|
||||
var load = function () { return __awaiter(_this, void 0, void 0, function () {
|
||||
var nextAutomationContexts, error_2;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
setIsLoading(true);
|
||||
setErrorMessage('');
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_a.trys.push([1, 3, 4, 5]);
|
||||
return [4 /*yield*/, loadAutomationContextsFromServer()];
|
||||
case 2:
|
||||
nextAutomationContexts = _a.sent();
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setAutomationContextsState(nextAutomationContexts);
|
||||
}
|
||||
return [3 /*break*/, 5];
|
||||
case 3:
|
||||
error_2 = _a.sent();
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setAutomationContextsState(exports.DEFAULT_AUTOMATION_CONTEXTS);
|
||||
setErrorMessage(error_2 instanceof Error ? error_2.message : '자동화 Context를 불러오지 못했습니다.');
|
||||
}
|
||||
return [3 /*break*/, 5];
|
||||
case 4:
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return [7 /*endfinally*/];
|
||||
case 5: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
void load();
|
||||
var handleSync = function () {
|
||||
void load();
|
||||
};
|
||||
window.addEventListener(AUTOMATION_CONTEXT_SYNC_EVENT, handleSync);
|
||||
return function () {
|
||||
cancelled = true;
|
||||
window.removeEventListener(AUTOMATION_CONTEXT_SYNC_EVENT, handleSync);
|
||||
};
|
||||
}, []);
|
||||
var setAutomationContexts = function (nextItems) { return __awaiter(_this, void 0, void 0, function () {
|
||||
var saved;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, saveAutomationContextsToServer(nextItems)];
|
||||
case 1:
|
||||
saved = _a.sent();
|
||||
if (mountedRef.current) {
|
||||
setAutomationContextsState(saved);
|
||||
setErrorMessage('');
|
||||
}
|
||||
emitAutomationContextsChange();
|
||||
return [2 /*return*/, saved];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
return {
|
||||
automationContexts: automationContexts,
|
||||
setAutomationContexts: setAutomationContexts,
|
||||
isLoading: isLoading,
|
||||
errorMessage: errorMessage,
|
||||
};
|
||||
}
|
||||
577
src/app/main/automationTypeAccess.js
Normal file
577
src/app/main/automationTypeAccess.js
Normal file
@@ -0,0 +1,577 @@
|
||||
"use strict";
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
||||
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
||||
if (ar || !(i in from)) {
|
||||
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
||||
ar[i] = from[i];
|
||||
}
|
||||
}
|
||||
return to.concat(ar || Array.prototype.slice.call(from));
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AUTOMATION_BEHAVIOR_LABELS = exports.AUTOMATION_BEHAVIOR_TYPES = void 0;
|
||||
exports.normalizeAutomationTypeId = normalizeAutomationTypeId;
|
||||
exports.sanitizeAutomationContexts = sanitizeAutomationContexts;
|
||||
exports.upsertAutomationType = upsertAutomationType;
|
||||
exports.deleteAutomationType = deleteAutomationType;
|
||||
exports.resolveAutomationTypeLabel = resolveAutomationTypeLabel;
|
||||
exports.buildAutomationTypeOptions = buildAutomationTypeOptions;
|
||||
exports.buildAutomationContextOptions = buildAutomationContextOptions;
|
||||
exports.resolveDefaultAutomationContextIds = resolveDefaultAutomationContextIds;
|
||||
exports.useAutomationTypeRegistry = useAutomationTypeRegistry;
|
||||
var react_1 = require("react");
|
||||
var clientIdentity_1 = require("./clientIdentity");
|
||||
exports.AUTOMATION_BEHAVIOR_TYPES = [
|
||||
'none',
|
||||
'plan',
|
||||
'command_execution',
|
||||
'non_source_work',
|
||||
'auto_worker',
|
||||
];
|
||||
var AUTOMATION_TYPES_API_PATH = '/automation-types';
|
||||
var AUTOMATION_TYPE_SYNC_EVENT = 'work-app:automation-types-changed';
|
||||
var AUTOMATION_TYPE_REQUEST_TIMEOUT_MS = 8000;
|
||||
exports.AUTOMATION_BEHAVIOR_LABELS = {
|
||||
none: '기본유형',
|
||||
plan: '작업 요청 등록',
|
||||
command_execution: 'Command 실행',
|
||||
non_source_work: '비 소스작업',
|
||||
auto_worker: 'autoWorker',
|
||||
};
|
||||
var DEFAULT_AUTOMATION_TYPES = [
|
||||
{
|
||||
id: 'general-inquiry',
|
||||
name: '일반 문의',
|
||||
description: '일반 문의/확인 요청으로 처리합니다.',
|
||||
contexts: [
|
||||
{
|
||||
id: 'general-inquiry-default',
|
||||
title: '기본 확인',
|
||||
content: '소스 수정 없이 조회, 상태 확인, 응답 정리를 우선합니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
behaviorType: 'command_execution',
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'none',
|
||||
name: '기본유형',
|
||||
description: '기본 자동화 처리용 유형입니다.',
|
||||
contexts: [
|
||||
{
|
||||
id: 'none-default',
|
||||
title: '기본 처리',
|
||||
content: '## 기본 처리\n- 현재 저장소 규칙과 요청 본문을 우선 따릅니다.\n- 소스 수정이 필요하면 관련 파일을 수정하고, 필요 없으면 확인 결과만 정리합니다.\n- 작업 결과에는 변경 파일, diff, preview 같은 증적을 남길 수 있습니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
behaviorType: 'none',
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'plan',
|
||||
name: '작업 요청 등록',
|
||||
description: 'Plan 등록/문서형 자동화 요청으로 처리합니다.',
|
||||
contexts: [
|
||||
{
|
||||
id: 'plan-default',
|
||||
title: '문서형 처리',
|
||||
content: 'Plan 등록/문서형 요청으로 처리하고, 구현보다 등록/정리 결과를 우선 남깁니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
behaviorType: 'plan',
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'command_execution',
|
||||
name: 'Command 실행',
|
||||
description: '저장소 수정 없이 명령 실행/조회 중심으로 처리합니다.',
|
||||
contexts: [
|
||||
{
|
||||
id: 'command-execution-default',
|
||||
title: '명령 실행',
|
||||
content: '저장소 수정 없이 명령 실행, API 확인, 상태 조회 중심으로 처리합니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
behaviorType: 'command_execution',
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'non_source_work',
|
||||
name: '비 소스작업',
|
||||
description: '문서/운영/확인 등 비소스 작업으로 처리합니다.',
|
||||
contexts: [
|
||||
{
|
||||
id: 'non-source-work-default',
|
||||
title: '비소스 작업',
|
||||
content: '문서, 운영, 데이터 확인 등 비소스 작업으로 처리하고 코드 수정은 요청 시에만 제한적으로 수행합니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
behaviorType: 'non_source_work',
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'auto_worker',
|
||||
name: 'autoWorker',
|
||||
description: '자동화 작업메모로 처리합니다.',
|
||||
contexts: [
|
||||
{
|
||||
id: 'auto-worker-default',
|
||||
title: '자동화 기본 규칙',
|
||||
content: '## context 사용 규칙\n- 자동화 실행기는 선택된 자동화 유형의 context만 우선 참조합니다.\n- Codex Live 문맥이나 일반 채팅 문맥은 자동화 기본 context로 섞지 않습니다.\n\n## 처리 원칙\n- 세부 절차는 현재 운영 설정을 따릅니다.',
|
||||
enabled: true,
|
||||
defaultSelected: true,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
behaviorType: 'auto_worker',
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-23T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
function normalizeText(value) {
|
||||
var _a;
|
||||
return (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
|
||||
}
|
||||
function normalizeAutomationTypeId(value) {
|
||||
var normalized = normalizeText(typeof value === 'string' ? value : '');
|
||||
if (normalized === 'stock-alert') {
|
||||
return 'general-inquiry';
|
||||
}
|
||||
if (normalized === 'plan_registration') {
|
||||
return 'plan';
|
||||
}
|
||||
if (normalized === 'general_development') {
|
||||
return 'auto_worker';
|
||||
}
|
||||
return normalized || 'none';
|
||||
}
|
||||
function normalizeBehaviorType(value) {
|
||||
var normalized = normalizeAutomationTypeId(value);
|
||||
return exports.AUTOMATION_BEHAVIOR_TYPES.includes(normalized)
|
||||
? normalized
|
||||
: 'none';
|
||||
}
|
||||
function getSemanticKey(record) {
|
||||
return "".concat(record.behaviorType, ":").concat(normalizeText(record.name).replace(/\s+/g, ' ').toLocaleLowerCase('ko-KR'));
|
||||
}
|
||||
function compareUpdatedAt(left, right) {
|
||||
var leftTime = Date.parse(left.updatedAt);
|
||||
var rightTime = Date.parse(right.updatedAt);
|
||||
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
||||
return leftTime - rightTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function compareContextUpdatedAt(left, right) {
|
||||
var leftTime = Date.parse(left.updatedAt);
|
||||
var rightTime = Date.parse(right.updatedAt);
|
||||
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
||||
return leftTime - rightTime;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function normalizeAutomationContext(record) {
|
||||
var title = normalizeText(record.title);
|
||||
var content = normalizeText(record.content);
|
||||
if (!title && !content) {
|
||||
return null;
|
||||
}
|
||||
var id = normalizeText(record.id) ||
|
||||
"automation-context-".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 8));
|
||||
return {
|
||||
id: id,
|
||||
title: title || 'Context',
|
||||
content: content,
|
||||
enabled: record.enabled !== false,
|
||||
defaultSelected: record.defaultSelected !== false,
|
||||
updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
function sanitizeAutomationContexts(items) {
|
||||
var byId = new Map();
|
||||
var bySemanticKey = new Map();
|
||||
(items !== null && items !== void 0 ? items : [])
|
||||
.map(function (item) { return normalizeAutomationContext(item); })
|
||||
.filter(function (item) { return Boolean(item); })
|
||||
.forEach(function (item) {
|
||||
var currentById = byId.get(item.id);
|
||||
if (!currentById || compareContextUpdatedAt(currentById, item) <= 0) {
|
||||
byId.set(item.id, item);
|
||||
}
|
||||
});
|
||||
for (var _i = 0, _a = byId.values(); _i < _a.length; _i++) {
|
||||
var item = _a[_i];
|
||||
var semanticKey = normalizeText(item.title).replace(/\s+/g, ' ').toLocaleLowerCase('ko-KR');
|
||||
var current = bySemanticKey.get(semanticKey);
|
||||
if (!current || compareContextUpdatedAt(current, item) <= 0) {
|
||||
bySemanticKey.set(semanticKey, item);
|
||||
}
|
||||
}
|
||||
return Array.from(bySemanticKey.values()).sort(function (left, right) { return left.title.localeCompare(right.title, 'ko-KR'); });
|
||||
}
|
||||
function normalizeAutomationType(record) {
|
||||
var name = normalizeText(record.name);
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
var id = normalizeText(record.id) ||
|
||||
"automation-type-".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 8));
|
||||
return {
|
||||
id: id,
|
||||
name: name,
|
||||
description: normalizeText(record.description),
|
||||
contexts: sanitizeAutomationContexts(record.contexts),
|
||||
behaviorType: normalizeBehaviorType(record.behaviorType),
|
||||
enabled: record.enabled !== false,
|
||||
updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
function sanitizeAutomationTypes(items) {
|
||||
var byId = new Map();
|
||||
var bySemanticKey = new Map();
|
||||
items
|
||||
.map(function (item) { return normalizeAutomationType(item); })
|
||||
.filter(function (item) { return Boolean(item); })
|
||||
.forEach(function (item) {
|
||||
var currentById = byId.get(item.id);
|
||||
if (!currentById || compareUpdatedAt(currentById, item) <= 0) {
|
||||
byId.set(item.id, item);
|
||||
}
|
||||
});
|
||||
for (var _i = 0, _a = byId.values(); _i < _a.length; _i++) {
|
||||
var item = _a[_i];
|
||||
var semanticKey = getSemanticKey(item);
|
||||
var current = bySemanticKey.get(semanticKey);
|
||||
if (!current || compareUpdatedAt(current, item) <= 0) {
|
||||
bySemanticKey.set(semanticKey, item);
|
||||
}
|
||||
}
|
||||
var values = Array.from(bySemanticKey.values()).sort(function (left, right) { return left.name.localeCompare(right.name, 'ko-KR'); });
|
||||
return values.length > 0 ? values : DEFAULT_AUTOMATION_TYPES;
|
||||
}
|
||||
function emitAutomationTypesChange() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new Event(AUTOMATION_TYPE_SYNC_EVENT));
|
||||
}
|
||||
function resolveApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
return '/api';
|
||||
}
|
||||
function resolveFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
var hostname = window.location.hostname;
|
||||
if (!['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname)) {
|
||||
return null;
|
||||
}
|
||||
var fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
var API_BASE_URL = resolveApiBaseUrl();
|
||||
var FALLBACK_BASE_URL = resolveFallbackBaseUrl();
|
||||
function requestOnce(baseUrl, init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var headers, hasBody, controller, timeoutId, response, _a;
|
||||
var _b, _c;
|
||||
return __generator(this, function (_d) {
|
||||
switch (_d.label) {
|
||||
case 0:
|
||||
headers = (0, clientIdentity_1.appendClientIdHeader)(init === null || init === void 0 ? void 0 : init.headers);
|
||||
hasBody = (init === null || init === void 0 ? void 0 : init.body) !== undefined && (init === null || init === void 0 ? void 0 : init.body) !== null;
|
||||
controller = new AbortController();
|
||||
timeoutId = window.setTimeout(function () { return controller.abort(); }, AUTOMATION_TYPE_REQUEST_TIMEOUT_MS);
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
_d.label = 1;
|
||||
case 1:
|
||||
_d.trys.push([1, , 5, 6]);
|
||||
return [4 /*yield*/, fetch("".concat(baseUrl).concat(AUTOMATION_TYPES_API_PATH), __assign(__assign({}, init), { headers: headers, signal: controller.signal, cache: (_b = init === null || init === void 0 ? void 0 : init.cache) !== null && _b !== void 0 ? _b : (((_c = init === null || init === void 0 ? void 0 : init.method) === null || _c === void 0 ? void 0 : _c.toUpperCase()) === 'GET' ? 'no-store' : undefined) }))];
|
||||
case 2:
|
||||
response = _d.sent();
|
||||
if (!!response.ok) return [3 /*break*/, 4];
|
||||
_a = Error.bind;
|
||||
return [4 /*yield*/, response.text()];
|
||||
case 3: throw new (_a.apply(Error, [void 0, (_d.sent()) || '자동화 처리 유형 요청에 실패했습니다.']))();
|
||||
case 4: return [2 /*return*/, response.json()];
|
||||
case 5:
|
||||
window.clearTimeout(timeoutId);
|
||||
return [7 /*endfinally*/];
|
||||
case 6: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function requestAutomationTypes(init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var error_1, shouldRetryWithFallback;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
_a.trys.push([0, 2, , 3]);
|
||||
return [4 /*yield*/, requestOnce(API_BASE_URL, init)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
case 2:
|
||||
error_1 = _a.sent();
|
||||
shouldRetryWithFallback = FALLBACK_BASE_URL &&
|
||||
FALLBACK_BASE_URL !== API_BASE_URL &&
|
||||
error_1 instanceof Error &&
|
||||
/404|408|502|Failed to fetch|Load failed|NetworkError|응답이 지연/i.test(error_1.message);
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error_1;
|
||||
}
|
||||
return [2 /*return*/, requestOnce(FALLBACK_BASE_URL, init)];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function loadAutomationTypesFromServer() {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var response;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, requestAutomationTypes({
|
||||
method: 'GET',
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
if (response.automationTypes == null) {
|
||||
return [2 /*return*/, DEFAULT_AUTOMATION_TYPES];
|
||||
}
|
||||
return [2 /*return*/, sanitizeAutomationTypes(response.automationTypes)];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function saveAutomationTypesToServer(items) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var resolved, response;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
resolved = sanitizeAutomationTypes(items);
|
||||
return [4 /*yield*/, requestAutomationTypes({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ automationTypes: resolved }),
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
return [2 /*return*/, sanitizeAutomationTypes(response.automationTypes)];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function upsertAutomationType(items, input) {
|
||||
var nextItem = normalizeAutomationType(input);
|
||||
if (!nextItem) {
|
||||
return sanitizeAutomationTypes(items);
|
||||
}
|
||||
var nextItems = items.filter(function (item) { return item.id !== nextItem.id; });
|
||||
nextItems.push(nextItem);
|
||||
return sanitizeAutomationTypes(nextItems);
|
||||
}
|
||||
function deleteAutomationType(items, automationTypeId) {
|
||||
var normalizedId = normalizeText(automationTypeId);
|
||||
if (!normalizedId) {
|
||||
return sanitizeAutomationTypes(items);
|
||||
}
|
||||
return sanitizeAutomationTypes(items.filter(function (item) { return item.id !== normalizedId; }));
|
||||
}
|
||||
function resolveAutomationTypeLabel(items, automationTypeId) {
|
||||
var _a, _b;
|
||||
var normalizedId = normalizeAutomationTypeId(automationTypeId);
|
||||
return (_b = (_a = items.find(function (item) { return item.id === normalizedId; })) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : normalizedId;
|
||||
}
|
||||
function buildAutomationTypeOptions(items, value) {
|
||||
var normalizedValue = normalizeAutomationTypeId(value);
|
||||
var enabledItems = items.filter(function (item) { return item.enabled; });
|
||||
var currentItem = items.find(function (item) { return item.id === normalizedValue; });
|
||||
var source = currentItem && !enabledItems.some(function (item) { return item.id === currentItem.id; }) ? __spreadArray(__spreadArray([], enabledItems, true), [currentItem], false) : enabledItems;
|
||||
if (source.length === 0) {
|
||||
return [{ label: '선택 안함', value: 'none' }];
|
||||
}
|
||||
return source.map(function (item) { return ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}); });
|
||||
}
|
||||
function buildAutomationContextOptions(items, automationTypeId, selectedContextIds) {
|
||||
var _a;
|
||||
if (selectedContextIds === void 0) { selectedContextIds = []; }
|
||||
var normalizedId = normalizeAutomationTypeId(automationTypeId);
|
||||
var automationType = (_a = items.find(function (item) { return item.id === normalizedId; })) !== null && _a !== void 0 ? _a : null;
|
||||
var contexts = sanitizeAutomationContexts(automationType === null || automationType === void 0 ? void 0 : automationType.contexts);
|
||||
var selectedSet = new Set(selectedContextIds);
|
||||
var enabledIds = new Set(contexts.filter(function (item) { return item.enabled; }).map(function (item) { return item.id; }));
|
||||
return contexts
|
||||
.filter(function (item) { return enabledIds.has(item.id) || selectedSet.has(item.id); })
|
||||
.map(function (item) { return ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
}); });
|
||||
}
|
||||
function resolveDefaultAutomationContextIds(items, automationTypeId) {
|
||||
var _a;
|
||||
var normalizedId = normalizeAutomationTypeId(automationTypeId);
|
||||
var automationType = (_a = items.find(function (item) { return item.id === normalizedId; })) !== null && _a !== void 0 ? _a : null;
|
||||
return sanitizeAutomationContexts(automationType === null || automationType === void 0 ? void 0 : automationType.contexts)
|
||||
.filter(function (item) { return item.enabled && item.defaultSelected; })
|
||||
.map(function (item) { return item.id; });
|
||||
}
|
||||
function useAutomationTypeRegistry() {
|
||||
var _this = this;
|
||||
var _a = (0, react_1.useState)(DEFAULT_AUTOMATION_TYPES), automationTypes = _a[0], setAutomationTypesState = _a[1];
|
||||
var _b = (0, react_1.useState)(true), isLoading = _b[0], setIsLoading = _b[1];
|
||||
var _c = (0, react_1.useState)(''), errorMessage = _c[0], setErrorMessage = _c[1];
|
||||
var mountedRef = (0, react_1.useRef)(true);
|
||||
(0, react_1.useEffect)(function () {
|
||||
mountedRef.current = true;
|
||||
return function () {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
(0, react_1.useEffect)(function () {
|
||||
var cancelled = false;
|
||||
var load = function () { return __awaiter(_this, void 0, void 0, function () {
|
||||
var nextAutomationTypes, error_2;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
setIsLoading(true);
|
||||
setErrorMessage('');
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_a.trys.push([1, 3, 4, 5]);
|
||||
return [4 /*yield*/, loadAutomationTypesFromServer()];
|
||||
case 2:
|
||||
nextAutomationTypes = _a.sent();
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setAutomationTypesState(nextAutomationTypes);
|
||||
}
|
||||
return [3 /*break*/, 5];
|
||||
case 3:
|
||||
error_2 = _a.sent();
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setAutomationTypesState(DEFAULT_AUTOMATION_TYPES);
|
||||
setErrorMessage(error_2 instanceof Error ? error_2.message : '자동화 처리 유형을 불러오지 못했습니다.');
|
||||
}
|
||||
return [3 /*break*/, 5];
|
||||
case 4:
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return [7 /*endfinally*/];
|
||||
case 5: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
void load();
|
||||
var handleSync = function () {
|
||||
void load();
|
||||
};
|
||||
window.addEventListener(AUTOMATION_TYPE_SYNC_EVENT, handleSync);
|
||||
return function () {
|
||||
cancelled = true;
|
||||
window.removeEventListener(AUTOMATION_TYPE_SYNC_EVENT, handleSync);
|
||||
};
|
||||
}, []);
|
||||
var setAutomationTypes = function (nextItems) { return __awaiter(_this, void 0, void 0, function () {
|
||||
var saved;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, saveAutomationTypesToServer(nextItems)];
|
||||
case 1:
|
||||
saved = _a.sent();
|
||||
if (mountedRef.current) {
|
||||
setAutomationTypesState(saved);
|
||||
setErrorMessage('');
|
||||
}
|
||||
emitAutomationTypesChange();
|
||||
return [2 /*return*/, saved];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
return {
|
||||
automationTypes: automationTypes,
|
||||
setAutomationTypes: setAutomationTypes,
|
||||
isLoading: isLoading,
|
||||
errorMessage: errorMessage,
|
||||
};
|
||||
}
|
||||
521
src/app/main/chatContextSettingsAccess.ts
Normal file
521
src/app/main/chatContextSettingsAccess.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { appendClientIdHeader } from './clientIdentity';
|
||||
|
||||
export type ChatDefaultContextRecord = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ChatTypeDefaultContextSelection = {
|
||||
chatTypeId: string;
|
||||
defaultContextIds: string[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ChatRoomContextSettings = {
|
||||
sessionId: string;
|
||||
defaultContextIds: string[];
|
||||
customContextTitle: string;
|
||||
customContextContent: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type ChatContextSettingsStore = {
|
||||
defaultContexts: ChatDefaultContextRecord[];
|
||||
chatTypeDefaults: ChatTypeDefaultContextSelection[];
|
||||
roomContexts: ChatRoomContextSettings[];
|
||||
};
|
||||
|
||||
const CHAT_CONTEXT_SETTINGS_STORAGE_KEY = 'work-app:chat-context-settings';
|
||||
const CHAT_CONTEXT_SETTINGS_SYNC_EVENT = 'work-app:chat-context-settings-changed';
|
||||
const CHAT_CONTEXT_SETTINGS_API_PATH = '/chat-context-settings';
|
||||
const CHAT_CONTEXT_SETTINGS_REQUEST_TIMEOUT_MS = 8000;
|
||||
|
||||
const DEFAULT_CHAT_DEFAULT_CONTEXTS: ChatDefaultContextRecord[] = [
|
||||
{
|
||||
id: 'chat-default-mobile-verification',
|
||||
title: '모바일 검증',
|
||||
content:
|
||||
'## 검증\n- UI 변경은 모바일 브라우저 환경에서 먼저 확인합니다.\n- 토큰 등록이 필요한 화면은 등록 토큰이 주입된 상태에서 검증합니다.',
|
||||
enabled: true,
|
||||
updatedAt: '2026-05-03T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'chat-default-resource-output',
|
||||
title: '리소스 출력',
|
||||
content:
|
||||
'## 산출물\n- 문서, 이미지, 코드 리소스는 `public/.codex_chat/<chat-session-id>/resource/` 경로 기준으로 제공합니다.\n- 최종 검증 이미지는 `[[preview:URL]]` 형식으로 남깁니다.',
|
||||
enabled: true,
|
||||
updatedAt: '2026-05-03T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeText(value: string | null | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
|
||||
function compareUpdatedAt(left: { updatedAt: string }, right: { updatedAt: string }) {
|
||||
const leftTime = Date.parse(left.updatedAt);
|
||||
const rightTime = Date.parse(right.updatedAt);
|
||||
|
||||
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
||||
return leftTime - rightTime;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function normalizeDefaultContext(record: Partial<ChatDefaultContextRecord>): ChatDefaultContextRecord | null {
|
||||
const title = normalizeText(record.title);
|
||||
const content = normalizeText(record.content);
|
||||
|
||||
if (!title && !content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id:
|
||||
normalizeText(record.id) ||
|
||||
`chat-default-context-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
title: title || '기본 유형',
|
||||
content,
|
||||
enabled: record.enabled !== false,
|
||||
updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDefaultContextIds(defaultContextIds: string[] | null | undefined) {
|
||||
return Array.from(new Set((defaultContextIds ?? []).map((item) => normalizeText(item)).filter(Boolean)));
|
||||
}
|
||||
|
||||
function sanitizeDefaultContexts(items: Partial<ChatDefaultContextRecord>[] | null | undefined) {
|
||||
const byId = new Map<string, ChatDefaultContextRecord>();
|
||||
|
||||
[...(items ?? []), ...DEFAULT_CHAT_DEFAULT_CONTEXTS]
|
||||
.map((item) => normalizeDefaultContext(item))
|
||||
.filter((item): item is ChatDefaultContextRecord => Boolean(item))
|
||||
.forEach((item) => {
|
||||
const current = byId.get(item.id);
|
||||
|
||||
if (!current || compareUpdatedAt(current, item) <= 0) {
|
||||
byId.set(item.id, item);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(byId.values()).sort((left, right) => left.title.localeCompare(right.title, 'ko-KR'));
|
||||
}
|
||||
|
||||
function sanitizeChatTypeDefaultSelections(items: Partial<ChatTypeDefaultContextSelection>[] | null | undefined) {
|
||||
const byChatTypeId = new Map<string, ChatTypeDefaultContextSelection>();
|
||||
|
||||
(items ?? []).forEach((item) => {
|
||||
const chatTypeId = normalizeText(item.chatTypeId);
|
||||
|
||||
if (!chatTypeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRecord: ChatTypeDefaultContextSelection = {
|
||||
chatTypeId,
|
||||
defaultContextIds: normalizeDefaultContextIds(item.defaultContextIds),
|
||||
updatedAt: normalizeText(item.updatedAt) || new Date().toISOString(),
|
||||
};
|
||||
const current = byChatTypeId.get(chatTypeId);
|
||||
|
||||
if (!current || compareUpdatedAt(current, nextRecord) <= 0) {
|
||||
byChatTypeId.set(chatTypeId, nextRecord);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(byChatTypeId.values()).sort((left, right) => left.chatTypeId.localeCompare(right.chatTypeId, 'ko-KR'));
|
||||
}
|
||||
|
||||
function sanitizeRoomContexts(items: Partial<ChatRoomContextSettings>[] | null | undefined) {
|
||||
const bySessionId = new Map<string, ChatRoomContextSettings>();
|
||||
|
||||
(items ?? []).forEach((item) => {
|
||||
const sessionId = normalizeText(item.sessionId);
|
||||
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRecord: ChatRoomContextSettings = {
|
||||
sessionId,
|
||||
defaultContextIds: normalizeDefaultContextIds(item.defaultContextIds),
|
||||
customContextTitle: normalizeText(item.customContextTitle),
|
||||
customContextContent: normalizeText(item.customContextContent),
|
||||
updatedAt: normalizeText(item.updatedAt) || new Date().toISOString(),
|
||||
};
|
||||
|
||||
const hasCustomContext = Boolean(nextRecord.customContextTitle || nextRecord.customContextContent);
|
||||
const hasDefaultOverrides = nextRecord.defaultContextIds.length > 0;
|
||||
|
||||
if (!hasCustomContext && !hasDefaultOverrides) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = bySessionId.get(sessionId);
|
||||
|
||||
if (!current || compareUpdatedAt(current, nextRecord) <= 0) {
|
||||
bySessionId.set(sessionId, nextRecord);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(bySessionId.values()).sort((left, right) => left.sessionId.localeCompare(right.sessionId, 'ko-KR'));
|
||||
}
|
||||
|
||||
function sanitizeStore(input: Partial<ChatContextSettingsStore> | null | undefined): ChatContextSettingsStore {
|
||||
return {
|
||||
defaultContexts: sanitizeDefaultContexts(input?.defaultContexts),
|
||||
chatTypeDefaults: sanitizeChatTypeDefaultSelections(input?.chatTypeDefaults),
|
||||
roomContexts: sanitizeRoomContexts(input?.roomContexts),
|
||||
};
|
||||
}
|
||||
|
||||
function loadStore() {
|
||||
if (typeof window === 'undefined') {
|
||||
return sanitizeStore(null);
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(CHAT_CONTEXT_SETTINGS_STORAGE_KEY);
|
||||
|
||||
if (!raw) {
|
||||
return sanitizeStore(null);
|
||||
}
|
||||
|
||||
return sanitizeStore(JSON.parse(raw) as Partial<ChatContextSettingsStore>);
|
||||
} catch {
|
||||
return sanitizeStore(null);
|
||||
}
|
||||
}
|
||||
|
||||
function emitStoreChange() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new Event(CHAT_CONTEXT_SETTINGS_SYNC_EVENT));
|
||||
}
|
||||
|
||||
function saveStore(store: ChatContextSettingsStore) {
|
||||
if (typeof window === 'undefined') {
|
||||
return store;
|
||||
}
|
||||
|
||||
const sanitized = sanitizeStore(store);
|
||||
window.localStorage.setItem(CHAT_CONTEXT_SETTINGS_STORAGE_KEY, JSON.stringify(sanitized));
|
||||
emitStoreChange();
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function resolveChatContextSettingsApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
|
||||
return '/api';
|
||||
}
|
||||
|
||||
function resolveChatContextSettingsFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
if (!['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
const CHAT_CONTEXT_SETTINGS_API_BASE_URL = resolveChatContextSettingsApiBaseUrl();
|
||||
const CHAT_CONTEXT_SETTINGS_FALLBACK_BASE_URL = resolveChatContextSettingsFallbackBaseUrl();
|
||||
|
||||
async function requestChatContextSettingsOnce<T>(baseUrl: string, init?: RequestInit) {
|
||||
const headers = appendClientIdHeader(init?.headers);
|
||||
const hasBody = init?.body !== undefined && init.body !== null;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), CHAT_CONTEXT_SETTINGS_REQUEST_TIMEOUT_MS);
|
||||
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${CHAT_CONTEXT_SETTINGS_API_PATH}`, {
|
||||
...init,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
cache: init?.cache ?? (init?.method?.toUpperCase() === 'GET' ? 'no-store' : undefined),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error((await response.text()) || '채팅 Context 설정 요청에 실패했습니다.');
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
} finally {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestChatContextSettings<T>(init?: RequestInit) {
|
||||
try {
|
||||
return await requestChatContextSettingsOnce<T>(CHAT_CONTEXT_SETTINGS_API_BASE_URL, init);
|
||||
} catch (error) {
|
||||
const shouldRetryWithFallback =
|
||||
CHAT_CONTEXT_SETTINGS_FALLBACK_BASE_URL &&
|
||||
CHAT_CONTEXT_SETTINGS_FALLBACK_BASE_URL !== CHAT_CONTEXT_SETTINGS_API_BASE_URL &&
|
||||
error instanceof Error &&
|
||||
/404|408|502|Failed to fetch|Load failed|NetworkError|응답이 지연/i.test(error.message);
|
||||
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return requestChatContextSettingsOnce<T>(CHAT_CONTEXT_SETTINGS_FALLBACK_BASE_URL, init);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStoreFromServer() {
|
||||
const response = await requestChatContextSettings<{
|
||||
ok: boolean;
|
||||
settings?: Partial<ChatContextSettingsStore> | null;
|
||||
}>({
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return sanitizeStore(response.settings);
|
||||
}
|
||||
|
||||
async function saveStoreToServer(store: ChatContextSettingsStore) {
|
||||
const sanitized = sanitizeStore(store);
|
||||
const response = await requestChatContextSettings<{
|
||||
ok: boolean;
|
||||
settings?: Partial<ChatContextSettingsStore> | null;
|
||||
}>({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ settings: sanitized }),
|
||||
});
|
||||
|
||||
emitStoreChange();
|
||||
return sanitizeStore(response.settings ?? sanitized);
|
||||
}
|
||||
|
||||
export function resolveChatTypeDefaultContextIds(
|
||||
selections: ChatTypeDefaultContextSelection[],
|
||||
chatTypeId: string | null | undefined,
|
||||
) {
|
||||
const normalizedChatTypeId = normalizeText(chatTypeId);
|
||||
|
||||
if (!normalizedChatTypeId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
selections.find((item) => item.chatTypeId === normalizedChatTypeId)?.defaultContextIds.map((item) => item.trim()).filter(Boolean) ??
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveChatRoomContextSettings(
|
||||
roomContexts: ChatRoomContextSettings[],
|
||||
sessionId: string | null | undefined,
|
||||
) {
|
||||
const normalizedSessionId = normalizeText(sessionId);
|
||||
|
||||
if (!normalizedSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return roomContexts.find((item) => item.sessionId === normalizedSessionId) ?? null;
|
||||
}
|
||||
|
||||
export function upsertChatDefaultContext(
|
||||
defaultContexts: ChatDefaultContextRecord[],
|
||||
input: Partial<ChatDefaultContextRecord>,
|
||||
) {
|
||||
const nextRecord = normalizeDefaultContext({
|
||||
...input,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (!nextRecord) {
|
||||
return sanitizeDefaultContexts(defaultContexts);
|
||||
}
|
||||
|
||||
return sanitizeDefaultContexts([
|
||||
...defaultContexts.filter((item) => item.id !== nextRecord.id),
|
||||
nextRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
export function deleteChatDefaultContext(defaultContexts: ChatDefaultContextRecord[], contextId: string) {
|
||||
const normalizedContextId = normalizeText(contextId);
|
||||
|
||||
if (!normalizedContextId) {
|
||||
return sanitizeDefaultContexts(defaultContexts);
|
||||
}
|
||||
|
||||
return sanitizeDefaultContexts(defaultContexts.filter((item) => item.id !== normalizedContextId));
|
||||
}
|
||||
|
||||
export function upsertChatTypeDefaultContextSelection(
|
||||
selections: ChatTypeDefaultContextSelection[],
|
||||
chatTypeId: string,
|
||||
defaultContextIds: string[],
|
||||
) {
|
||||
const normalizedChatTypeId = normalizeText(chatTypeId);
|
||||
|
||||
if (!normalizedChatTypeId) {
|
||||
return sanitizeChatTypeDefaultSelections(selections);
|
||||
}
|
||||
|
||||
const nextRecord: ChatTypeDefaultContextSelection = {
|
||||
chatTypeId: normalizedChatTypeId,
|
||||
defaultContextIds: normalizeDefaultContextIds(defaultContextIds),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return sanitizeChatTypeDefaultSelections([
|
||||
...selections.filter((item) => item.chatTypeId !== normalizedChatTypeId),
|
||||
nextRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
export function pruneChatTypeDefaultSelections(
|
||||
selections: ChatTypeDefaultContextSelection[],
|
||||
contextId: string,
|
||||
) {
|
||||
const normalizedContextId = normalizeText(contextId);
|
||||
|
||||
if (!normalizedContextId) {
|
||||
return sanitizeChatTypeDefaultSelections(selections);
|
||||
}
|
||||
|
||||
return sanitizeChatTypeDefaultSelections(
|
||||
selections.map((item) => ({
|
||||
...item,
|
||||
defaultContextIds: item.defaultContextIds.filter((candidate) => candidate !== normalizedContextId),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function upsertChatRoomContextSettings(
|
||||
roomContexts: ChatRoomContextSettings[],
|
||||
input: Partial<ChatRoomContextSettings> & { sessionId: string },
|
||||
) {
|
||||
const nextRecord: ChatRoomContextSettings = {
|
||||
sessionId: normalizeText(input.sessionId),
|
||||
defaultContextIds: normalizeDefaultContextIds(input.defaultContextIds),
|
||||
customContextTitle: normalizeText(input.customContextTitle),
|
||||
customContextContent: normalizeText(input.customContextContent),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (!nextRecord.sessionId) {
|
||||
return sanitizeRoomContexts(roomContexts);
|
||||
}
|
||||
|
||||
return sanitizeRoomContexts([
|
||||
...roomContexts.filter((item) => item.sessionId !== nextRecord.sessionId),
|
||||
nextRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
export function pruneChatRoomContextSettings(roomContexts: ChatRoomContextSettings[], contextId: string) {
|
||||
const normalizedContextId = normalizeText(contextId);
|
||||
|
||||
if (!normalizedContextId) {
|
||||
return sanitizeRoomContexts(roomContexts);
|
||||
}
|
||||
|
||||
return sanitizeRoomContexts(
|
||||
roomContexts.map((item) => ({
|
||||
...item,
|
||||
defaultContextIds: item.defaultContextIds.filter((candidate) => candidate !== normalizedContextId),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function useChatContextSettingsRegistry() {
|
||||
const [store, setStoreState] = useState<ChatContextSettingsStore>(() => loadStore());
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
const syncStore = async () => {
|
||||
try {
|
||||
const serverStore = await fetchStoreFromServer();
|
||||
const saved = saveStore(serverStore);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setStoreState(saved);
|
||||
setErrorMessage('');
|
||||
}
|
||||
} catch (error) {
|
||||
const localStore = loadStore();
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setStoreState(localStore);
|
||||
setErrorMessage(error instanceof Error ? error.message : '채팅 Context 설정을 불러오지 못했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void syncStore();
|
||||
const handleSync = () => {
|
||||
void syncStore();
|
||||
};
|
||||
|
||||
window.addEventListener(CHAT_CONTEXT_SETTINGS_SYNC_EVENT, handleSync);
|
||||
window.addEventListener('storage', syncStore);
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
window.removeEventListener(CHAT_CONTEXT_SETTINGS_SYNC_EVENT, handleSync);
|
||||
window.removeEventListener('storage', syncStore);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...store,
|
||||
errorMessage,
|
||||
setStore: (
|
||||
updater:
|
||||
| ChatContextSettingsStore
|
||||
| ((current: ChatContextSettingsStore) => ChatContextSettingsStore),
|
||||
) => {
|
||||
const nextStore = typeof updater === 'function' ? updater(loadStore()) : updater;
|
||||
const saved = saveStore(nextStore);
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setStoreState(saved);
|
||||
setErrorMessage('');
|
||||
}
|
||||
|
||||
return saveStoreToServer(saved).then((serverSaved) => {
|
||||
if (isMountedRef.current) {
|
||||
setStoreState(serverSaved);
|
||||
}
|
||||
|
||||
return serverSaved;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { appendClientIdHeader } from './clientIdentity';
|
||||
import {
|
||||
LAYOUT_EDITOR_CHAT_TYPE_DESCRIPTION,
|
||||
LAYOUT_EDITOR_CHAT_TYPE_ID,
|
||||
LAYOUT_EDITOR_CHAT_TYPE_NAME,
|
||||
} from './chatTypeDefaults';
|
||||
import { DEFAULT_CHAT_TYPES } from './chatTypeDefaults';
|
||||
|
||||
export type ChatPermissionRole = 'guest' | 'token-user';
|
||||
|
||||
@@ -33,43 +29,6 @@ export const CHAT_PERMISSION_ROLE_LABELS: Record<ChatPermissionRole, string> = {
|
||||
'token-user': '토큰 사용자',
|
||||
};
|
||||
|
||||
const DEFAULT_CHAT_TYPES: ChatTypeRecord[] = [
|
||||
{
|
||||
id: 'general-request',
|
||||
name: '일반 요청',
|
||||
description:
|
||||
'## 기본 처리\n- 실제 수정 범위와 방식은 채팅 유형 context와 AGENTS.md 규칙을 최우선으로 따릅니다.\n- 최근 대화 내용과 현재 요청은 채팅 유형 context를 해치지 않는 범위에서만 보조 문맥으로 참조합니다.\n- 실패 시에는 현재 세션에서 수정한 소스만 범위를 좁혀 되돌립니다.\n\n## 문맥과 리소스\n- 채팅 유형 정보는 대화방 내용보다 먼저 읽고 적용합니다.\n- 산출물과 첨부 리소스는 `public/.codex_chat/<chat-session-id>/resource/` 기준으로 제공합니다.\n\n## 검증\n- 사소한 UI 변경이 있더라도 기본값은 모바일 브라우저 환경에서 캡처하고 검증합니다.\n- 모바일 검증은 `ai-code-app-preview` 컨테이너 기준으로 진행합니다.\n- 외부 접속 확인은 `https://test.sm-home.cloud/` 기준으로 진행합니다.\n- 모바일 캡처는 루트 `.env`의 등록 토큰을 `localStorage`에 주입한 상태에서 진행하고 비로그인 기본 화면으로 확인하지 않습니다.\n- preview 컨테이너만 필요시 재기동 후 검증합니다.\n- preview 외 다른 서버나 컨테이너는 종료하거나 재기동하지 않습니다.\n- 추가 재기동이나 후속 테스트가 필요하면 직접 처리하지 말고 앱알림으로 필요한 대상과 사유를 안내합니다.\n- 캡처 화면으로 요청사항 반영 여부를 다시 확인하고, 부족하면 추가 작업을 이어서 진행합니다.\n\n## 구현 기준\n- 소스는 덕지덕지 붙지 않게 정리하고, 분리할 부분은 분리합니다.\n- 이전 처리에서 불필요해진 부분은 제거해 clean code 상태를 유지합니다.\n- 답변 스타일과 기본 문맥은 반드시 채팅 유형 정보만 기준으로 적용하세요.\n- Plan 자동화용 자동화 유형 context가 있더라도 Codex Live에서는 별도 요청이 없는 한 참조하지 마세요.',
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-21T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: LAYOUT_EDITOR_CHAT_TYPE_ID,
|
||||
name: LAYOUT_EDITOR_CHAT_TYPE_NAME,
|
||||
description: LAYOUT_EDITOR_CHAT_TYPE_DESCRIPTION,
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-27T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'api-request-template',
|
||||
name: 'API요청',
|
||||
description: '## 처리 범위\n- 호출 가능한 API 요청만 진행합니다.\n- 자동화, 작업요청, 스케줄 등 API 중심 요청에 사용합니다.',
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-16T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'general-inquiry',
|
||||
name: '일반 문의',
|
||||
description:
|
||||
'## 기본 원칙\n- 문의 응답과 리소스 제공만 처리합니다.\n- 프로젝트 소스 또는 다른 파일시스템에 대한 파일 생성과 수정은 하지 않습니다.\n\n## 파일과 리소스\n- 파일 생성은 `public/.codex_chat/<chat-session-id>/resource/` 경로에서만 가능합니다.\n- 그 외 경로의 파일과 디렉터리는 읽기만 가능합니다.\n\n## 명령 사용\n- 상태 확인 목적의 조회성 command만 사용할 수 있습니다.\n- 빌드, 실행, 설치, 변경을 발생시키는 다른 command는 사용하지 않습니다.',
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-24T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeText(value: string | null | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
@@ -151,6 +110,10 @@ function sanitizeChatTypes(chatTypes: Partial<ChatTypeRecord>[]) {
|
||||
);
|
||||
}
|
||||
|
||||
function mergeWithDefaultChatTypes(chatTypes: Partial<ChatTypeRecord>[] | null | undefined) {
|
||||
return sanitizeChatTypes([...(chatTypes ?? []), ...DEFAULT_CHAT_TYPES]);
|
||||
}
|
||||
|
||||
function emitChatTypesChange() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -242,21 +205,21 @@ async function fetchChatTypesFromServer() {
|
||||
});
|
||||
|
||||
if (response.chatTypes == null) {
|
||||
return null;
|
||||
return mergeWithDefaultChatTypes(DEFAULT_CHAT_TYPES);
|
||||
}
|
||||
|
||||
return sanitizeChatTypes(response.chatTypes);
|
||||
return mergeWithDefaultChatTypes(response.chatTypes);
|
||||
}
|
||||
|
||||
async function saveChatTypesToServer(chatTypes: ChatTypeRecord[]) {
|
||||
const resolved = sanitizeChatTypes(chatTypes);
|
||||
const resolved = mergeWithDefaultChatTypes(chatTypes);
|
||||
const response = await requestChatTypes<{ ok: boolean; chatTypes: Partial<ChatTypeRecord>[] }>({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ chatTypes: resolved }),
|
||||
});
|
||||
|
||||
emitChatTypesChange();
|
||||
return sanitizeChatTypes(response.chatTypes);
|
||||
return mergeWithDefaultChatTypes(response.chatTypes);
|
||||
}
|
||||
|
||||
export function upsertChatType(chatTypes: ChatTypeRecord[], input: ChatTypeInput) {
|
||||
|
||||
@@ -1,6 +1,76 @@
|
||||
export type DefaultChatTypeRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Array<'guest' | 'token-user'>;
|
||||
enabled: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const GENERAL_REQUEST_CHAT_TYPE_ID = 'general-request';
|
||||
export const GENERAL_REQUEST_CHAT_TYPE_NAME = '일반 요청';
|
||||
export const GENERAL_REQUEST_CHAT_TYPE_DESCRIPTION =
|
||||
'## 기본 처리\n- 실제 수정 범위와 방식은 채팅 유형 context와 AGENTS.md 규칙을 최우선으로 따릅니다.\n- 최근 대화 내용과 현재 요청은 채팅 유형 context를 해치지 않는 범위에서만 보조 문맥으로 참조합니다.\n- 실패 시에는 현재 세션에서 수정한 소스만 범위를 좁혀 되돌립니다.\n\n## 문맥과 리소스\n- 채팅 유형 정보는 대화방 내용보다 먼저 읽고 적용합니다.\n- 산출물과 첨부 리소스는 `public/.codex_chat/<chat-session-id>/resource/` 기준으로 제공합니다.\n- `[[link-card:...]]`는 외부 공개 링크에만 사용하고, 내부 리소스(`/api/chat/resources/...`, `/.codex_chat/...`)에는 사용하지 않습니다.\n- 내부 문서, 로그, 코드, 테이블 파일은 일반 리소스 URL이나 preview 컴포넌트로 바로 열 수 있는 형식으로 제공합니다.\n\n## 검증\n- 사소한 UI 변경이 있더라도 기본값은 모바일 브라우저 환경에서 캡처하고 검증합니다.\n- 모바일 검증은 `ai-code-app-preview` 컨테이너 기준으로 진행합니다.\n- 외부 접속 확인은 `https://test.sm-home.cloud/` 기준으로 진행합니다.\n- 모바일 캡처는 루트 `.env`의 등록 토큰을 `localStorage`에 주입한 상태에서 진행하고 비로그인 기본 화면으로 확인하지 않습니다.\n- 채팅 결과물 중 모바일 캡처와 최종 화면 검증 결과는 링크 카드로 바꾸지 말고 반드시 `[[preview:URL]]` 형식으로 제공합니다.\n- preview 컨테이너만 필요시 재기동 후 검증합니다.\n- preview 외 다른 서버나 컨테이너는 종료하거나 재기동하지 않습니다.\n- 추가 재기동이나 후속 테스트가 필요하면 직접 처리하지 말고 앱알림으로 필요한 대상과 사유를 안내합니다.\n- 캡처 화면으로 요청사항 반영 여부를 다시 확인하고, 부족하면 추가 작업을 이어서 진행합니다.\n\n## 구현 기준\n- 소스는 덕지덕지 붙지 않게 정리하고, 분리할 부분은 분리합니다.\n- 이전 처리에서 불필요해진 부분은 제거해 clean code 상태를 유지합니다.\n- 답변 스타일과 기본 문맥은 반드시 채팅 유형 정보만 기준으로 적용하세요.\n- Plan 자동화용 자동화 유형 context가 있더라도 Codex Live에서는 별도 요청이 없는 한 참조하지 마세요.';
|
||||
|
||||
export const LAYOUT_EDITOR_CHAT_TYPE_ID = 'layout-editor-execution';
|
||||
|
||||
export const LAYOUT_EDITOR_CHAT_TYPE_NAME = 'Layout editor 실행';
|
||||
|
||||
export const LAYOUT_EDITOR_CHAT_TYPE_DESCRIPTION =
|
||||
'## 처리 범위\n- Layout editor 실행 유형은 호출 가능한 API 요청만 처리합니다.\n- 자동화, 작업요청, 스케줄 등 API 중심 요청에 사용합니다.\n- Layout editor에서 추가기능이나 컴포넌트 간 연계동작 수정을 요청하면 기능개선 관련 API 처리와 해당 Layout의 기능설명 API 데이터 갱신을 함께 진행합니다.\n- 연결 요청은 source 컴포넌트, target 컴포넌트, 적용 레이아웃 또는 ID가 모두 식별될 때만 처리합니다.\n- 기능 구현이 진행되면 기능설명 API 데이터 갱신을 필수 후속 단계로 함께 수행합니다. 기능설명 API 데이터 갱신에는 신규 등록과 기존 설명 수정이 모두 포함됩니다.\n- 메모나 요청 첫 줄에 적힌 연결 지시는 Base Input 등 실제 대상 컴포넌트 연결 규칙으로 해석할 수 있지만, 식별 정보가 부족하면 구현하지 않고 API 또는 데이터 기준으로 다시 확인합니다.\n- 메모 첫번째 줄을 Base Input에 연결하는 기능구현, 화면 바인딩 변경, 이벤트 연결 수정, 저장/조회 API 데이터 갱신 요청도 위 식별 조건을 충족하면 이 유형에서 처리할 수 있습니다.\n\n## 금지 사항\n- 서버나 컨테이너 재기동은 이 유형에서 직접 처리하지 않습니다.\n- 전역 상태 변경, 전체 레이아웃 공통 반영, 전체 컴포넌트 타입 반영은 사용자가 명시적으로 요청한 경우에만 처리합니다.\n- 레이아웃 구조, 배치, 스타일 자체만 설명해달라는 요청은 이 유형에서 처리하지 않습니다.\n- 화면 미리보기 감상이나 단순 UI 소개처럼 API 처리와 무관한 레이아웃 설명 요청은 이 유형에서 처리하지 않습니다.\n\n## 검증 기준\n- 변경이 있으면 preview 서버 기준으로 검증 스크린샷을 기본 제공하고, 별도 지시가 없어도 모바일 버전 캡처를 우선 제공합니다.\n- 모바일 캡처는 등록 토큰이 주입된 상태에서 진행하고 비로그인 기본 화면으로 확인하지 않습니다.\n- 검증 결과에 포함하는 스크린샷, 문서, diff 같은 리소스는 preview 컴포넌트에서 바로 열리도록 이미지 URL 또는 `[[preview:URL]]` 형식으로 함께 남깁니다.\n- 링크 카드가 따로 필요하더라도 preview 리소스 표시는 별도로 유지합니다.\n\n## 응답 기준\n- 필요한 경우 API 요청 범위인지 먼저 좁혀서 답합니다.\n- 연결 대상 식별이 안 되면 구현하지 말고 source, target, 적용 레이아웃 또는 ID를 API나 데이터 기준으로 재질문합니다.\n- 컴포넌트 간 연결, 이벤트 흐름, 기능설명 데이터 갱신, 메모 첫 줄과 Base Input 연결처럼 실행 가능한 요청은 범위 안에서 처리합니다.\n- 서버 재기동이나 추가 운영 작업이 필요하면 직접 수행하지 말고 필요한 대상과 사유만 안내합니다.\n- 요청 완료 후 검증에 사용한 스크린샷 리소스는 항상 함께 제공합니다.\n- 단순 설명 요청이 아니라 실제 기능구현이나 연계 수정 요청이면 처리 불가로 제한하지 않습니다.';
|
||||
'## 처리 범위\n- Layout editor 실행 유형은 호출 가능한 API 요청만 처리합니다.\n- 자동화, 작업요청, 스케줄 등 API 중심 요청에 사용합니다.\n- Layout editor에서 추가기능이나 컴포넌트 간 연계동작 수정을 요청하면 기능개선 관련 API 처리와 해당 Layout의 기능설명 API 데이터 갱신을 함께 진행합니다.\n- 연결 요청은 source 컴포넌트, target 컴포넌트, 적용 레이아웃 또는 ID가 모두 식별될 때만 처리합니다.\n- 기능 구현이 진행되면 기능설명 API 데이터 갱신을 필수 후속 단계로 함께 수행합니다. 기능설명 API 데이터 갱신에는 신규 등록과 기존 설명 수정이 모두 포함됩니다.\n- 메모나 요청 첫 줄에 적힌 연결 지시는 Base Input 등 실제 대상 컴포넌트 연결 규칙으로 해석할 수 있지만, 식별 정보가 부족하면 구현하지 않고 API 또는 데이터 기준으로 다시 확인합니다.\n- 메모 첫번째 줄을 Base Input에 연결하는 기능구현, 화면 바인딩 변경, 이벤트 연결 수정, 저장/조회 API 데이터 갱신 요청도 위 식별 조건을 충족하면 이 유형에서 처리할 수 있습니다.\n\n## 금지 사항\n- 서버나 컨테이너 재기동은 이 유형에서 직접 처리하지 않습니다.\n- 전역 상태 변경, 전체 레이아웃 공통 반영, 전체 컴포넌트 타입 반영은 사용자가 명시적으로 요청한 경우에만 처리합니다.\n- 레이아웃 구조, 배치, 스타일 자체만 설명해달라는 요청은 이 유형에서 처리하지 않습니다.\n- 화면 미리보기 감상이나 단순 UI 소개처럼 API 처리와 무관한 레이아웃 설명 요청은 이 유형에서 처리하지 않습니다.\n\n## 검증 기준\n- 변경이 있으면 preview 서버 기준으로 검증 스크린샷을 기본 제공하고, 별도 지시가 없어도 모바일 버전 캡처를 우선 제공합니다.\n- 모바일 캡처는 등록 토큰이 주입된 상태에서 진행하고 비로그인 기본 화면으로 확인하지 않습니다.\n- 검증 결과에 포함하는 스크린샷, 문서, diff 같은 리소스는 preview 컴포넌트에서 바로 열리도록 이미지 URL 또는 `[[preview:URL]]` 형식으로 함께 남깁니다.\n- 모바일 캡처와 최종 화면 검증 결과는 링크 카드로 대체하지 않고 `[[preview:URL]]` 표기를 유지합니다.\n- 링크 카드가 따로 필요하더라도 preview 리소스 표시는 별도로 유지합니다.\n\n## 응답 기준\n- 필요한 경우 API 요청 범위인지 먼저 좁혀서 답합니다.\n- 연결 대상 식별이 안 되면 구현하지 말고 source, target, 적용 레이아웃 또는 ID를 API나 데이터 기준으로 재질문합니다.\n- 컴포넌트 간 연결, 이벤트 흐름, 기능설명 데이터 갱신, 메모 첫 줄과 Base Input 연결처럼 실행 가능한 요청은 범위 안에서 처리합니다.\n- 서버 재기동이나 추가 운영 작업이 필요하면 직접 수행하지 말고 필요한 대상과 사유만 안내합니다.\n- 요청 완료 후 검증에 사용한 스크린샷 리소스는 항상 함께 제공합니다.\n- 단순 설명 요청이 아니라 실제 기능구현이나 연계 수정 요청이면 처리 불가로 제한하지 않습니다.';
|
||||
|
||||
export const LAYOUT_EDITOR_GUIDED_CHAT_TYPE_ID = 'layout-editor-guided-execution';
|
||||
export const LAYOUT_EDITOR_GUIDED_CHAT_TYPE_NAME = 'Layout editor 단계별 실행';
|
||||
export const LAYOUT_EDITOR_GUIDED_CHAT_TYPE_DESCRIPTION =
|
||||
'## 기본 처리\n- Layout editor 요청을 단계별로 나눠 진행합니다.\n- 현재 단계 결과를 확인하면서 다음 단계로 이어가는 실행 흐름에 사용합니다.\n- 실제 수정 범위와 검증 방식은 현재 채팅 유형 context와 AGENTS.md 규칙을 최우선으로 따릅니다.\n- 이 채팅유형은 특정 메뉴 하나의 전용 context를 기본값으로 두지 않고, Layout editor 화면 구성 전반에서 공통으로 사용합니다.\n\n## 레이아웃 패키지 기준\n- 특정 레이아웃에서만 쓰는 전용 기능은 공용 app 계층으로 퍼뜨리지 말고 해당 layout feature package 안에서만 구조화합니다.\n- 패키지 개선이 필요하면 전용 화면을 해당 package 내부의 page, utils, types, chat helper 등으로 먼저 분리합니다.\n- 공통 컴포넌트 승격은 다른 화면 재사용 근거가 확인될 때만 진행합니다.\n\n## 화면 정리 기준\n- 현재 단계에서 필요한 입력 영역과 참고 영역은 한 화면에 섞어 두지 말고 분리합니다.\n- 상시 노출 액션은 화면 맥락에 맞게 간결하게 유지하고, 필요하면 tooltip과 aria-label로 의미를 보완합니다.\n- 실제 수정본이 있으면 문서 설명보다 화면 결과와 preview 검증을 우선합니다.\n\n## 산출물 기준\n- 단계별 산출물은 현재 요청 범위에 맞춰 유지합니다.\n- 특정 메뉴 전용 문서 저장 위치나 세부 규칙은 해당 메뉴 요청이 있을 때만 그 패키지 기준으로 추가 적용합니다.';
|
||||
|
||||
export const API_REQUEST_CHAT_TYPE_ID = 'api-request-template';
|
||||
export const API_REQUEST_CHAT_TYPE_NAME = 'API요청';
|
||||
export const API_REQUEST_CHAT_TYPE_DESCRIPTION =
|
||||
'## 처리 범위\n- 호출 가능한 API 요청만 진행합니다.\n- 자동화, 작업요청, 스케줄 등 API 중심 요청에 사용합니다.';
|
||||
|
||||
export const GENERAL_INQUIRY_CHAT_TYPE_ID = 'general-inquiry';
|
||||
export const GENERAL_INQUIRY_CHAT_TYPE_NAME = '일반 문의';
|
||||
export const GENERAL_INQUIRY_CHAT_TYPE_DESCRIPTION =
|
||||
'## 기본 원칙\n- 문의 응답과 리소스 제공만 처리합니다.\n- 프로젝트 소스 또는 다른 파일시스템에 대한 파일 생성과 수정은 하지 않습니다.\n\n## 파일과 리소스\n- 파일 생성은 `public/.codex_chat/<chat-session-id>/resource/` 경로에서만 가능합니다.\n- 그 외 경로의 파일과 디렉터리는 읽기만 가능합니다.\n\n## 명령 사용\n- 상태 확인 목적의 조회성 command만 사용할 수 있습니다.\n- 빌드, 실행, 설치, 변경을 발생시키는 다른 command는 사용하지 않습니다.';
|
||||
|
||||
export const DEFAULT_CHAT_TYPES: DefaultChatTypeRecord[] = [
|
||||
{
|
||||
id: GENERAL_REQUEST_CHAT_TYPE_ID,
|
||||
name: GENERAL_REQUEST_CHAT_TYPE_NAME,
|
||||
description: GENERAL_REQUEST_CHAT_TYPE_DESCRIPTION,
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-21T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: LAYOUT_EDITOR_CHAT_TYPE_ID,
|
||||
name: LAYOUT_EDITOR_CHAT_TYPE_NAME,
|
||||
description: LAYOUT_EDITOR_CHAT_TYPE_DESCRIPTION,
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-27T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: LAYOUT_EDITOR_GUIDED_CHAT_TYPE_ID,
|
||||
name: LAYOUT_EDITOR_GUIDED_CHAT_TYPE_NAME,
|
||||
description: LAYOUT_EDITOR_GUIDED_CHAT_TYPE_DESCRIPTION,
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-05-02T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: API_REQUEST_CHAT_TYPE_ID,
|
||||
name: API_REQUEST_CHAT_TYPE_NAME,
|
||||
description: API_REQUEST_CHAT_TYPE_DESCRIPTION,
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-16T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: GENERAL_INQUIRY_CHAT_TYPE_ID,
|
||||
name: GENERAL_INQUIRY_CHAT_TYPE_NAME,
|
||||
description: GENERAL_INQUIRY_CHAT_TYPE_DESCRIPTION,
|
||||
permissions: ['token-user'],
|
||||
enabled: true,
|
||||
updatedAt: '2026-04-24T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,7 +47,13 @@ export type ChatGateway = {
|
||||
payload: Partial<
|
||||
Pick<
|
||||
ChatConversationSummary,
|
||||
'title' | 'chatTypeId' | 'lastChatTypeId' | 'contextLabel' | 'contextDescription' | 'notifyOffline'
|
||||
| 'title'
|
||||
| 'chatTypeId'
|
||||
| 'lastChatTypeId'
|
||||
| 'generalSectionName'
|
||||
| 'contextLabel'
|
||||
| 'contextDescription'
|
||||
| 'notifyOffline'
|
||||
>
|
||||
>,
|
||||
) => Promise<ChatConversationSummary>;
|
||||
|
||||
@@ -20,6 +20,7 @@ type PendingChatRequest = {
|
||||
requestId: string;
|
||||
text: string;
|
||||
mode: 'queue' | 'direct';
|
||||
omitPromptHistory?: boolean;
|
||||
chatTypeId: string;
|
||||
chatTypeLabel: string;
|
||||
chatTypeDescription: string;
|
||||
@@ -35,6 +36,7 @@ type PendingContextConfirm = {
|
||||
chatTypeDescription: string;
|
||||
includedContextCount: number;
|
||||
omittedContextCount: number;
|
||||
omitPromptHistory?: boolean;
|
||||
};
|
||||
|
||||
type SelectedChatType = {
|
||||
@@ -87,6 +89,11 @@ type UseConversationComposerControllerOptions = {
|
||||
scrollViewportToBottom: () => void;
|
||||
};
|
||||
|
||||
type SendMessageOptions = {
|
||||
mode: 'queue' | 'direct';
|
||||
draftText?: string;
|
||||
};
|
||||
|
||||
export function useConversationComposerController({
|
||||
activeSessionId,
|
||||
appConfigChat,
|
||||
@@ -219,13 +226,14 @@ export function useConversationComposerController({
|
||||
|
||||
const executeSendMessage = useCallback(
|
||||
(request: PendingContextConfirm) => {
|
||||
const { mode, text, chatTypeId, chatTypeLabel, chatTypeDescription } = request;
|
||||
const { mode, text, chatTypeId, chatTypeLabel, chatTypeDescription, omitPromptHistory } = request;
|
||||
const requestId = `client-${Date.now().toString(36)}`;
|
||||
const outgoingRequest: PendingChatRequest = {
|
||||
sessionId: activeSessionId,
|
||||
requestId,
|
||||
text,
|
||||
mode,
|
||||
omitPromptHistory: omitPromptHistory === true,
|
||||
chatTypeId,
|
||||
chatTypeLabel,
|
||||
chatTypeDescription,
|
||||
@@ -361,12 +369,12 @@ export function useConversationComposerController({
|
||||
);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
(mode: 'queue' | 'direct') => {
|
||||
({ mode, draftText }: SendMessageOptions) => {
|
||||
if (isComposerAttachmentUploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmed = buildOutgoingMessageText(draft, composerAttachments).trim();
|
||||
const trimmed = buildOutgoingMessageText(draftText ?? draft, composerAttachments).trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return;
|
||||
@@ -427,11 +435,11 @@ export function useConversationComposerController({
|
||||
);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
sendMessage('queue');
|
||||
sendMessage({ mode: 'queue' });
|
||||
}, [sendMessage]);
|
||||
|
||||
const handleSendImmediate = useCallback(() => {
|
||||
sendMessage('direct');
|
||||
sendMessage({ mode: 'direct' });
|
||||
}, [sendMessage]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { chatGateway } from '../data/chatGateway';
|
||||
|
||||
type UseConversationListDataOptions = {
|
||||
requestedSessionId: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
type UseConversationListDataResult = {
|
||||
@@ -17,37 +18,71 @@ type UseConversationListDataResult = {
|
||||
setConversationSearch: Dispatch<SetStateAction<string>>;
|
||||
};
|
||||
|
||||
const CONVERSATION_LIST_POLL_INTERVAL_MS = 5000;
|
||||
|
||||
function mergeConversationItemsPreservingRequestedSession(
|
||||
nextItems: ChatConversationSummary[],
|
||||
previousItems: ChatConversationSummary[],
|
||||
requestedSessionId: string,
|
||||
) {
|
||||
const previousBySessionId = new Map(previousItems.map((item) => [item.sessionId, item] as const));
|
||||
const normalizedNextItems = nextItems.map((item) => {
|
||||
const previousItem = previousBySessionId.get(item.sessionId);
|
||||
|
||||
if (!previousItem) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const chatTypeId = item.chatTypeId?.trim() || previousItem.chatTypeId?.trim() || null;
|
||||
const lastChatTypeId =
|
||||
item.lastChatTypeId?.trim() ||
|
||||
chatTypeId ||
|
||||
previousItem.lastChatTypeId?.trim() ||
|
||||
previousItem.chatTypeId?.trim() ||
|
||||
null;
|
||||
|
||||
return {
|
||||
...item,
|
||||
chatTypeId,
|
||||
lastChatTypeId,
|
||||
generalSectionName: item.generalSectionName?.trim() || previousItem.generalSectionName?.trim() || null,
|
||||
contextLabel: item.contextLabel?.trim() || previousItem.contextLabel?.trim() || null,
|
||||
contextDescription: item.contextDescription?.trim() || previousItem.contextDescription?.trim() || null,
|
||||
lastMessagePreview: item.lastMessagePreview.trim() || previousItem.lastMessagePreview.trim(),
|
||||
lastResponsePreview: item.lastResponsePreview.trim() || previousItem.lastResponsePreview.trim(),
|
||||
};
|
||||
});
|
||||
const normalizedRequestedSessionId = requestedSessionId.trim();
|
||||
const nextSessionIds = new Set(normalizedNextItems.map((item) => item.sessionId));
|
||||
const preservedTransientItems = previousItems.filter((item) => {
|
||||
if (!item.sessionId || nextSessionIds.has(item.sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.isDraftOnly || item.currentJobStatus === 'queued' || item.currentJobStatus === 'started';
|
||||
});
|
||||
|
||||
if (!normalizedRequestedSessionId) {
|
||||
return sortChatConversationSummaries(nextItems);
|
||||
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
const hasRequestedSession = nextItems.some((item) => item.sessionId === normalizedRequestedSessionId);
|
||||
const hasRequestedSession = normalizedNextItems.some((item) => item.sessionId === normalizedRequestedSessionId);
|
||||
|
||||
if (hasRequestedSession) {
|
||||
return sortChatConversationSummaries(nextItems);
|
||||
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
const preservedRequestedSession =
|
||||
previousItems.find((item) => item.sessionId === normalizedRequestedSessionId) ?? null;
|
||||
|
||||
if (!preservedRequestedSession) {
|
||||
return sortChatConversationSummaries(nextItems);
|
||||
return sortChatConversationSummaries([...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
return sortChatConversationSummaries([preservedRequestedSession, ...nextItems]);
|
||||
return sortChatConversationSummaries([preservedRequestedSession, ...preservedTransientItems, ...normalizedNextItems]);
|
||||
}
|
||||
|
||||
export function useConversationListData({
|
||||
requestedSessionId,
|
||||
enabled = true,
|
||||
}: UseConversationListDataOptions): UseConversationListDataResult {
|
||||
const [conversationItems, setConversationItems] = useState<ChatConversationSummary[]>([]);
|
||||
const [isConversationListLoading, setIsConversationListLoading] = useState(false);
|
||||
@@ -104,65 +139,17 @@ export function useConversationListData({
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
setIsConversationListLoading(true);
|
||||
void loadConversationItems();
|
||||
if (enabled) {
|
||||
setIsConversationListLoading(true);
|
||||
void loadConversationItems();
|
||||
} else {
|
||||
setIsConversationListLoading(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, [loadConversationItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let intervalId: number | null = null;
|
||||
|
||||
const startPolling = () => {
|
||||
if (intervalId != null || document.visibilityState !== 'visible') {
|
||||
return;
|
||||
}
|
||||
|
||||
intervalId = window.setInterval(() => {
|
||||
void loadConversationItems({ silent: true });
|
||||
}, CONVERSATION_LIST_POLL_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (intervalId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void loadConversationItems({ silent: true });
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
stopPolling();
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
void loadConversationItems({ silent: true });
|
||||
startPolling();
|
||||
};
|
||||
|
||||
startPolling();
|
||||
window.addEventListener('focus', handleFocus);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [loadConversationItems]);
|
||||
}, [enabled, loadConversationItems]);
|
||||
|
||||
return {
|
||||
conversationItems,
|
||||
|
||||
@@ -14,6 +14,7 @@ type PendingChatRequest = {
|
||||
requestId: string;
|
||||
text: string;
|
||||
mode: 'queue' | 'direct';
|
||||
omitPromptHistory?: boolean;
|
||||
chatTypeId: string;
|
||||
chatTypeLabel: string;
|
||||
chatTypeDescription: string;
|
||||
|
||||
@@ -18,14 +18,40 @@ function mergeConversationRequests(
|
||||
sessionId: string,
|
||||
) {
|
||||
const previousSessionItems = previous.filter((item) => item.sessionId === sessionId);
|
||||
const previousByRequestId = new Map(previousSessionItems.map((item) => [item.requestId, item] as const));
|
||||
const incomingRequestIds = new Set(incoming.map((item) => item.requestId));
|
||||
const mergedIncoming = incoming.map((item) => {
|
||||
const previousItem = previousByRequestId.get(item.requestId);
|
||||
|
||||
if (!previousItem) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const nextUserText = item.userText.trim() || previousItem.userText.trim();
|
||||
const nextResponseText = item.responseText.trim() || previousItem.responseText.trim();
|
||||
const nextStatusMessage = item.statusMessage?.trim() || previousItem.statusMessage?.trim() || null;
|
||||
|
||||
return {
|
||||
...item,
|
||||
statusMessage: nextStatusMessage,
|
||||
userMessageId: item.userMessageId ?? previousItem.userMessageId,
|
||||
userText: nextUserText,
|
||||
responseMessageId: item.responseMessageId ?? previousItem.responseMessageId,
|
||||
responseText: nextResponseText,
|
||||
hasResponse: item.hasResponse || previousItem.hasResponse || nextResponseText.length > 0,
|
||||
answeredAt: item.answeredAt ?? previousItem.answeredAt,
|
||||
terminalAt: item.terminalAt ?? previousItem.terminalAt,
|
||||
};
|
||||
});
|
||||
const preservedLocalItems = previousSessionItems.filter((item) => !incomingRequestIds.has(item.requestId));
|
||||
|
||||
return [...incoming, ...preservedLocalItems].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
||||
return [...mergedIncoming, ...preservedLocalItems].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
||||
}
|
||||
|
||||
type UseConversationRoomDataOptions = {
|
||||
activeSessionId: string;
|
||||
activeConversationIsDraftOnly?: boolean;
|
||||
activeConversationHasLocalActivity?: boolean;
|
||||
oldestLoadedMessageId: number | null;
|
||||
reloadKey: number;
|
||||
shouldForceStickToBottomOnNextLoadRef: MutableRefObject<boolean>;
|
||||
@@ -50,6 +76,8 @@ type UseConversationRoomDataOptions = {
|
||||
|
||||
export function useConversationRoomData({
|
||||
activeSessionId,
|
||||
activeConversationIsDraftOnly = false,
|
||||
activeConversationHasLocalActivity = false,
|
||||
oldestLoadedMessageId,
|
||||
reloadKey,
|
||||
shouldForceStickToBottomOnNextLoadRef,
|
||||
@@ -85,6 +113,18 @@ export function useConversationRoomData({
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeConversationIsDraftOnly && !activeConversationHasLocalActivity) {
|
||||
previousSessionIdRef.current = activeSessionId;
|
||||
setMessages([]);
|
||||
setRequestItems((previous) => previous.filter((item) => item.sessionId !== activeSessionId));
|
||||
setConversationLoadingLabel('첫 요청을 보내면 대화가 저장됩니다.');
|
||||
setIsConversationContentLoading(false);
|
||||
setIsLoadingOlderMessages(false);
|
||||
setHasOlderMessages(false);
|
||||
setOldestLoadedMessageId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
const requestedSessionId = activeSessionId;
|
||||
|
||||
@@ -195,6 +235,8 @@ export function useConversationRoomData({
|
||||
};
|
||||
}, [
|
||||
activeSessionId,
|
||||
activeConversationHasLocalActivity,
|
||||
activeConversationIsDraftOnly,
|
||||
captureViewportRestoreSnapshot,
|
||||
messagesRef,
|
||||
pendingViewportRestoreRef,
|
||||
|
||||
92
src/app/main/clientIdentity.js
Normal file
92
src/app/main/clientIdentity.js
Normal file
@@ -0,0 +1,92 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CLIENT_ID_STORAGE_KEY = void 0;
|
||||
exports.getClientId = getClientId;
|
||||
exports.clearClientId = clearClientId;
|
||||
exports.getOrCreateClientId = getOrCreateClientId;
|
||||
exports.appendClientIdHeader = appendClientIdHeader;
|
||||
exports.buildTrackedPageUrl = buildTrackedPageUrl;
|
||||
exports.CLIENT_ID_STORAGE_KEY = 'work-app.visitor.client-id';
|
||||
function generateFallbackClientId() {
|
||||
return "client-".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 10));
|
||||
}
|
||||
function generateClientId() {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return generateFallbackClientId();
|
||||
}
|
||||
function getClientId() {
|
||||
var _a, _b;
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return (_b = (_a = window.localStorage.getItem(exports.CLIENT_ID_STORAGE_KEY)) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
|
||||
}
|
||||
function clearClientId() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(exports.CLIENT_ID_STORAGE_KEY);
|
||||
}
|
||||
function getOrCreateClientId() {
|
||||
var existingClientId = getClientId();
|
||||
if (existingClientId) {
|
||||
return existingClientId;
|
||||
}
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
var nextClientId = generateClientId();
|
||||
window.localStorage.setItem(exports.CLIENT_ID_STORAGE_KEY, nextClientId);
|
||||
return nextClientId;
|
||||
}
|
||||
function appendClientIdHeader(headersInit) {
|
||||
var headers = new Headers(headersInit);
|
||||
var clientId = getOrCreateClientId();
|
||||
if (clientId && !headers.has('X-Client-Id')) {
|
||||
headers.set('X-Client-Id', clientId);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
function buildTrackedPageUrl(page) {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
var url = new URL(window.location.href);
|
||||
if (page.topMenu === 'plans') {
|
||||
url.searchParams.set('topMenu', 'plans');
|
||||
url.searchParams.set('planFilter', page.section);
|
||||
url.searchParams.delete('planSection');
|
||||
}
|
||||
else {
|
||||
url.searchParams.set('topMenu', page.topMenu);
|
||||
url.searchParams.delete('planSection');
|
||||
url.searchParams.delete('planFilter');
|
||||
}
|
||||
if (page.topMenu === 'docs') {
|
||||
url.searchParams.set('docsSection', page.section);
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete('docsSection');
|
||||
}
|
||||
if (page.topMenu === 'apis') {
|
||||
url.searchParams.set('apiSection', page.section);
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete('apiSection');
|
||||
}
|
||||
if (page.topMenu === 'chat') {
|
||||
url.searchParams.set('chatSection', page.section);
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete('chatSection');
|
||||
}
|
||||
if (page.topMenu === 'play') {
|
||||
url.searchParams.set('playSection', page.section);
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete('playSection');
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
217
src/app/main/errorLogApi.js
Normal file
217
src/app/main/errorLogApi.js
Normal file
@@ -0,0 +1,217 @@
|
||||
"use strict";
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
if (typeof b !== "function" && b !== null)
|
||||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fetchErrorLogs = fetchErrorLogs;
|
||||
exports.reportClientError = reportClientError;
|
||||
var clientIdentity_1 = require("./clientIdentity");
|
||||
var tokenAccess_1 = require("./tokenAccess");
|
||||
var ErrorLogApiError = /** @class */ (function (_super) {
|
||||
__extends(ErrorLogApiError, _super);
|
||||
function ErrorLogApiError(message, status) {
|
||||
var _this = _super.call(this, message) || this;
|
||||
_this.name = 'ErrorLogApiError';
|
||||
_this.status = status;
|
||||
return _this;
|
||||
}
|
||||
return ErrorLogApiError;
|
||||
}(Error));
|
||||
function resolveErrorLogApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
return '/api';
|
||||
}
|
||||
function resolveWorkServerFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
var hostname = window.location.hostname;
|
||||
var isLocalWorkServerHost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';
|
||||
if (!isLocalWorkServerHost) {
|
||||
return null;
|
||||
}
|
||||
var fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
var ERROR_LOG_API_BASE_URL = resolveErrorLogApiBaseUrl();
|
||||
var ERROR_LOG_API_FALLBACK_BASE_URL = !import.meta.env.VITE_WORK_SERVER_URL && ERROR_LOG_API_BASE_URL === '/api'
|
||||
? resolveWorkServerFallbackBaseUrl()
|
||||
: null;
|
||||
function requestOnce(baseUrl, path, init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var headers, hasBody, method, response, text, payload;
|
||||
var _a, _b, _c;
|
||||
return __generator(this, function (_d) {
|
||||
switch (_d.label) {
|
||||
case 0:
|
||||
headers = (0, clientIdentity_1.appendClientIdHeader)(init === null || init === void 0 ? void 0 : init.headers);
|
||||
hasBody = (init === null || init === void 0 ? void 0 : init.body) !== undefined && init.body !== null;
|
||||
method = (_b = (_a = init === null || init === void 0 ? void 0 : init.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : 'GET';
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
return [4 /*yield*/, fetch("".concat(baseUrl).concat(path), __assign(__assign({}, init), { headers: headers, cache: (_c = init === null || init === void 0 ? void 0 : init.cache) !== null && _c !== void 0 ? _c : (method === 'GET' ? 'no-store' : undefined) }))];
|
||||
case 1:
|
||||
response = _d.sent();
|
||||
if (!!response.ok) return [3 /*break*/, 3];
|
||||
return [4 /*yield*/, response.text()];
|
||||
case 2:
|
||||
text = _d.sent();
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
throw new ErrorLogApiError(payload.message || '에러 로그 요청에 실패했습니다.', response.status);
|
||||
}
|
||||
catch (_e) {
|
||||
throw new ErrorLogApiError(text || '에러 로그 요청에 실패했습니다.', response.status);
|
||||
}
|
||||
_d.label = 3;
|
||||
case 3: return [2 /*return*/, response.json()];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function request(path, init) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var error_1, shouldRetryWithFallback;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
_a.trys.push([0, 2, , 3]);
|
||||
return [4 /*yield*/, requestOnce(ERROR_LOG_API_BASE_URL, path, init)];
|
||||
case 1: return [2 /*return*/, _a.sent()];
|
||||
case 2:
|
||||
error_1 = _a.sent();
|
||||
shouldRetryWithFallback = ERROR_LOG_API_FALLBACK_BASE_URL &&
|
||||
ERROR_LOG_API_FALLBACK_BASE_URL !== ERROR_LOG_API_BASE_URL &&
|
||||
(error_1 instanceof ErrorLogApiError
|
||||
? error_1.status === 404 || error_1.status === 408 || error_1.status === 502
|
||||
: error_1 instanceof Error && /404|Failed to fetch|NetworkError/i.test(error_1.message));
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error_1;
|
||||
}
|
||||
return [2 /*return*/, requestOnce(ERROR_LOG_API_FALLBACK_BASE_URL, path, init)];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function fetchErrorLogs() {
|
||||
return __awaiter(this, arguments, void 0, function (limit) {
|
||||
var token, response;
|
||||
if (limit === void 0) { limit = 50; }
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
token = (0, tokenAccess_1.getRegisteredAccessToken)();
|
||||
return [4 /*yield*/, request("/error-logs?limit=".concat(limit), {
|
||||
headers: {
|
||||
'X-Access-Token': token,
|
||||
},
|
||||
})];
|
||||
case 1:
|
||||
response = _a.sent();
|
||||
return [2 /*return*/, response.items];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function reportClientError(payload) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var _a;
|
||||
var _b, _c, _d, _e, _f, _g, _h;
|
||||
return __generator(this, function (_j) {
|
||||
switch (_j.label) {
|
||||
case 0:
|
||||
_j.trys.push([0, 2, , 3]);
|
||||
return [4 /*yield*/, request('/error-logs/report', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
source: 'client',
|
||||
sourceLabel: '프론트엔드',
|
||||
errorType: payload.errorType,
|
||||
errorName: (_b = payload.errorName) !== null && _b !== void 0 ? _b : null,
|
||||
errorMessage: payload.errorMessage,
|
||||
detail: (_c = payload.detail) !== null && _c !== void 0 ? _c : null,
|
||||
stackTrace: (_d = payload.stackTrace) !== null && _d !== void 0 ? _d : null,
|
||||
statusCode: (_e = payload.statusCode) !== null && _e !== void 0 ? _e : null,
|
||||
requestMethod: (_f = payload.requestMethod) !== null && _f !== void 0 ? _f : null,
|
||||
requestPath: (_g = payload.requestPath) !== null && _g !== void 0 ? _g : null,
|
||||
context: (_h = payload.context) !== null && _h !== void 0 ? _h : null,
|
||||
}),
|
||||
})];
|
||||
case 1:
|
||||
_j.sent();
|
||||
return [3 /*break*/, 3];
|
||||
case 2:
|
||||
_a = _j.sent();
|
||||
return [3 /*break*/, 3];
|
||||
case 3: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -101,7 +101,15 @@ function parseRoute(pathname: string): {
|
||||
};
|
||||
}
|
||||
|
||||
if (top === 'chat' && (first === 'live' || first === 'changes' || first === 'errors' || first === 'manage')) {
|
||||
if (
|
||||
top === 'chat' &&
|
||||
(first === 'live' ||
|
||||
first === 'changes' ||
|
||||
first === 'resources' ||
|
||||
first === 'errors' ||
|
||||
first === 'manage' ||
|
||||
first === 'manage-defaults')
|
||||
) {
|
||||
return {
|
||||
topMenu: 'chat',
|
||||
docsMenu: DOCS_DEFAULT_FOLDER,
|
||||
@@ -112,7 +120,7 @@ function parseRoute(pathname: string): {
|
||||
};
|
||||
}
|
||||
|
||||
if (top === 'play' && first === 'layout') {
|
||||
if (top === 'play' && (first === 'layout' || first === 'test' || first === 'cbt')) {
|
||||
return {
|
||||
topMenu: 'play',
|
||||
docsMenu: DOCS_DEFAULT_FOLDER,
|
||||
@@ -135,7 +143,7 @@ function parseRoute(pathname: string): {
|
||||
}
|
||||
|
||||
return {
|
||||
topMenu: 'plans',
|
||||
topMenu: 'chat',
|
||||
docsMenu: DOCS_DEFAULT_FOLDER,
|
||||
apiMenu: 'components',
|
||||
planMenu: 'all',
|
||||
@@ -169,7 +177,7 @@ function resolveSidebarCollapsedForViewport(isSidebarOverlayViewport: boolean, t
|
||||
return false;
|
||||
}
|
||||
|
||||
return topMenu !== 'docs';
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveSidebarOpenKeys(
|
||||
@@ -202,7 +210,7 @@ function resolveSidebarOpenKeys(
|
||||
return ['app-log-group'];
|
||||
}
|
||||
|
||||
return chatMenu === 'manage' ? ['chat-manage-group'] : ['codex-live-group'];
|
||||
return chatMenu === 'manage' || chatMenu === 'manage-defaults' ? ['chat-manage-group'] : ['codex-live-group'];
|
||||
}
|
||||
|
||||
export function MainLayout() {
|
||||
@@ -328,6 +336,10 @@ export function MainLayout() {
|
||||
activeStates: ['anyway'],
|
||||
mobileOnly: true,
|
||||
trigger: 'pull-left-middle-right',
|
||||
hotZoneSize: 36,
|
||||
minDistance: 180,
|
||||
minViewportDistanceRatio: 0.35,
|
||||
maxHorizontalDrift: 72,
|
||||
onTrigger: () => {
|
||||
openSearch('window');
|
||||
},
|
||||
@@ -467,7 +479,7 @@ export function MainLayout() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Layout>
|
||||
<Layout className="app-shell__body">
|
||||
{contentExpanded || (isSidebarOverlayViewport && sidebarCollapsed) ? null : (
|
||||
<MainSidebar
|
||||
activeTopMenu={routeState.topMenu}
|
||||
@@ -514,7 +526,7 @@ export function MainLayout() {
|
||||
}}
|
||||
onSelectPlayMenu={(key) => {
|
||||
const savedLayoutId = resolveSavedLayoutIdFromMenuKey(key);
|
||||
navigate(savedLayoutId ? buildSavedLayoutPath(savedLayoutId) : buildPlayPath(key as 'layout'));
|
||||
navigate(savedLayoutId ? buildSavedLayoutPath(savedLayoutId) : buildPlayPath(key as 'layout' | 'test'));
|
||||
if (isSidebarOverlayViewport) {
|
||||
setSidebarCollapsed(true);
|
||||
}
|
||||
@@ -525,11 +537,13 @@ export function MainLayout() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSidebarOverlayViewport && !sidebarCollapsed ? null : (
|
||||
<MainContent contentExpanded={contentExpanded} onToggleContentExpanded={() => setContentExpanded((previous) => !previous)}>
|
||||
<Outlet />
|
||||
</MainContent>
|
||||
)}
|
||||
<MainContent
|
||||
contentExpanded={contentExpanded}
|
||||
sidebarOverlayActive={isSidebarOverlayViewport && !sidebarCollapsed}
|
||||
onToggleContentExpanded={() => setContentExpanded((previous) => !previous)}
|
||||
>
|
||||
<Outlet />
|
||||
</MainContent>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</MainLayoutContextProvider>
|
||||
|
||||
@@ -207,6 +207,18 @@ export function buildSearchOptions({
|
||||
},
|
||||
onSelectWindow,
|
||||
},
|
||||
{
|
||||
id: 'page:chat:resources',
|
||||
label: 'Codex Live / 리소스 관리',
|
||||
group: 'Page',
|
||||
keywords: ['codex live', 'resource', 'resources', 'file', 'files', '리소스', '파일', '파일 시스템'],
|
||||
onSelect: () => {
|
||||
requestPlanQuickFilter(null);
|
||||
navigateTo(buildChatPath('resources'));
|
||||
setFocusedComponentId(null);
|
||||
},
|
||||
onSelectWindow,
|
||||
},
|
||||
{
|
||||
id: 'page:chat:errors',
|
||||
label: '앱로그 / 에러 로그',
|
||||
@@ -232,6 +244,18 @@ export function buildSearchOptions({
|
||||
setFocusedComponentId(null);
|
||||
},
|
||||
onSelectWindow,
|
||||
},
|
||||
{
|
||||
id: 'page:chat:manage-defaults',
|
||||
label: '채팅 관리 / 기본 유형 관리',
|
||||
group: 'Page',
|
||||
keywords: ['chat manage', 'default type', 'default context', '기본 유형', '기본 context', '채팅 관리'],
|
||||
onSelect: () => {
|
||||
requestPlanQuickFilter(null);
|
||||
navigateTo(buildChatPath('manage-defaults'));
|
||||
setFocusedComponentId(null);
|
||||
},
|
||||
onSelectWindow,
|
||||
} satisfies SearchKeywordOption,
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { Alert, Button, Checkbox, Input, Select, Spin, message } from 'antd';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import {
|
||||
startTransition,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -33,12 +34,17 @@ import {
|
||||
import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { CodexDiffBlock } from '../../../components/previewer';
|
||||
import type { ComposerFilePickResult } from '../chatV2/hooks/useConversationComposerController';
|
||||
import { ChatPreviewBody, resolveChatPreviewKindLabel, type ChatPreviewKind } from './ChatPreviewBody';
|
||||
import {
|
||||
ChatPreviewBody,
|
||||
resolveChatPreviewGlyph,
|
||||
resolveChatPreviewKindLabel,
|
||||
type ChatPreviewKind,
|
||||
} from './ChatPreviewBody';
|
||||
import { triggerResourceDownload } from './downloadUtils';
|
||||
import { extractAutoDetectedPreviewUrls } from './inlinePreviewUrls';
|
||||
import { extractHiddenPreviewUrls, stripHiddenPreviewTags } from './previewMarkers';
|
||||
import { normalizeChatResourceUrl } from './chatResourceUrl';
|
||||
import { copyPreviewContent, copyText } from './chatUtils';
|
||||
import { copyPreviewContent, copyText, isExecutionFailureMessage, isMissingRequestMessage, isPreparingChatReplyText } from './chatUtils';
|
||||
import { ChatLinkCardPreview } from './ChatLinkCardPreview';
|
||||
import { openChatExternalLink } from './linkNavigation';
|
||||
import { ChatRankedLinkPreview, type RankedLinkPreviewTarget } from './ChatRankedLinkPreview';
|
||||
@@ -110,6 +116,8 @@ type PreviewFetchError = Error & {
|
||||
const MARKDOWN_IMAGE_LINE_PATTERN = /^\s*!\[([^\]]*)\]\(([^)]+)\)\s*$/;
|
||||
const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
const CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
|
||||
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
|
||||
const COLLAPSIBLE_MESSAGE_LINE_COUNT = 6;
|
||||
const COLLAPSIBLE_MESSAGE_CHAR_COUNT = 280;
|
||||
const DIFF_CODE_BLOCK_PATTERN = /```diff[^\n]*\n([\s\S]*?)```/g;
|
||||
@@ -199,6 +207,25 @@ function buildPreviewFileName(item: Pick<PreviewOption, 'url' | 'label'>) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePreviewFileExtension(item: Pick<PreviewOption, 'url' | 'label'>) {
|
||||
const fileName = buildPreviewFileName(item).toLowerCase();
|
||||
const match = fileName.match(/\.([a-z0-9]{1,16})$/i);
|
||||
return match?.[1] ?? '';
|
||||
}
|
||||
|
||||
function buildResourceChipMeta(item: Pick<PreviewOption, 'url' | 'label' | 'kind'>) {
|
||||
const extension = resolvePreviewFileExtension(item);
|
||||
|
||||
if (extension) {
|
||||
return extension.toUpperCase();
|
||||
}
|
||||
|
||||
return resolveChatPreviewKindLabel(item.kind as ChatPreviewKind)
|
||||
.replace(/\s+preview$/i, '')
|
||||
.replace(/\s+download$/i, '')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function normalizeRankedLinkTitle(value: string) {
|
||||
return value
|
||||
.replace(/^\[(.+)\]\([^)]+\)$/u, '$1')
|
||||
@@ -561,6 +588,22 @@ function isActivityLogMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function getMissingRequestMessageText(message: ChatMessage) {
|
||||
if (!isMissingRequestMessage(message)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return message.text.slice(`${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n`.length).trim();
|
||||
}
|
||||
|
||||
function getExecutionFailureMessageText(message: ChatMessage) {
|
||||
if (!isExecutionFailureMessage(message)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return message.text.slice(`${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n`.length).trim();
|
||||
}
|
||||
|
||||
function extractActivityLines(message: ChatMessage) {
|
||||
return message.text
|
||||
.slice(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`.length)
|
||||
@@ -570,8 +613,20 @@ function extractActivityLines(message: ChatMessage) {
|
||||
}
|
||||
|
||||
function summarizeActivityLines(lines: string[]) {
|
||||
const latestLine = lines.at(-1) ?? '';
|
||||
return latestLine;
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
const summary = lines[index]
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('# 이유:') || line.startsWith('# 진행:') || line.startsWith('# 상태:'));
|
||||
|
||||
if (!summary) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return summary.replace(/^#\s*(이유|진행|상태):\s*/u, '').trim();
|
||||
}
|
||||
|
||||
return lines.at(-1) ?? '';
|
||||
}
|
||||
|
||||
function isLikelyCollapsibleMessage(text: string) {
|
||||
@@ -948,8 +1003,8 @@ type ChatConversationViewProps = {
|
||||
onPickComposerFiles: (files: File[]) => ComposerFilePickResult | Promise<ComposerFilePickResult>;
|
||||
onRemoveComposerAttachment: (attachmentId: string) => void;
|
||||
onSelectChatType: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onSendImmediate: () => void;
|
||||
onSend: (draftText?: string) => void;
|
||||
onSendImmediate: (draftText?: string) => void;
|
||||
onToggleSendWithoutContext: () => void;
|
||||
onClearDraft: () => void;
|
||||
onScrollToBottom: () => void;
|
||||
@@ -1018,12 +1073,58 @@ export function ChatConversationView({
|
||||
const [showBusyOverlay, setShowBusyOverlay] = useState(false);
|
||||
const [showLatestResourceOnly, setShowLatestResourceOnly] = useState(true);
|
||||
const [pendingComposerUploads, setPendingComposerUploads] = useState<PendingComposerUpload[]>([]);
|
||||
const [composerDraft, setComposerDraft] = useState(draft);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const activitySectionRefs = useRef(new Map<string, HTMLElement>());
|
||||
const messageBodyRefs = useRef(new Map<number, HTMLDivElement>());
|
||||
const autoCollapsedActivityRequestIdsRef = useRef(new Set<string>());
|
||||
const lastReportedDraftRef = useRef(draft);
|
||||
|
||||
useEffect(() => {
|
||||
if (draft === lastReportedDraftRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setComposerDraft(draft);
|
||||
}, [draft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (composerDraft === lastReportedDraftRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
startTransition(() => {
|
||||
onDraftChange(composerDraft);
|
||||
});
|
||||
}, 120);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [composerDraft, onDraftChange]);
|
||||
|
||||
const orderedMessages = useMemo(() => {
|
||||
const shouldDisplayActivityMessage = (activityMessage: ChatMessage) => {
|
||||
const requestId = activityMessage.clientRequestId?.trim();
|
||||
|
||||
if (!requestId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestState = requestStateMap.get(requestId);
|
||||
const hasCodexResponse = visibleMessages.some(
|
||||
(candidate) =>
|
||||
candidate.clientRequestId?.trim() === requestId &&
|
||||
candidate.author === 'codex' &&
|
||||
candidate.text.trim().length > 0 &&
|
||||
!isPreparingChatReplyText(candidate.text),
|
||||
);
|
||||
|
||||
return !isTerminalRequestStatus(requestState?.status) || !hasCodexResponse;
|
||||
};
|
||||
|
||||
const latestActivityByRequestId = new Map<string, ChatMessage>();
|
||||
const orphanActivityMessages: ChatMessage[] = [];
|
||||
const baseMessages = visibleMessages.filter((message) => {
|
||||
@@ -1038,7 +1139,9 @@ export function ChatConversationView({
|
||||
return false;
|
||||
}
|
||||
|
||||
latestActivityByRequestId.set(activityKey, message);
|
||||
if (shouldDisplayActivityMessage(message)) {
|
||||
latestActivityByRequestId.set(activityKey, message);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const insertedActivityRequestIds = new Set<string>();
|
||||
@@ -1074,19 +1177,9 @@ export function ChatConversationView({
|
||||
});
|
||||
|
||||
return [...ordered, ...orphanActivityMessages];
|
||||
}, [visibleMessages]);
|
||||
}, [requestStateMap, visibleMessages]);
|
||||
const previewItemsByUrl = useMemo(() => new Map(previewItems.map((item) => [item.url, item])), [previewItems]);
|
||||
const isChatTypeReadonly = useMemo(() => {
|
||||
if (isChatTypeSelectionLocked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(new URLSearchParams(window.location.search).get('sessionId')?.trim());
|
||||
}, [isChatTypeSelectionLocked]);
|
||||
const isChatTypeReadonly = isChatTypeSelectionLocked;
|
||||
const visiblePreviewItems = useMemo(() => {
|
||||
if (!showLatestResourceOnly) {
|
||||
return previewItems;
|
||||
@@ -1590,8 +1683,20 @@ export function ChatConversationView({
|
||||
onOpenPreview(item.id);
|
||||
}}
|
||||
>
|
||||
<span title={item.label}>{item.label}</span>
|
||||
<span>{item.kind}</span>
|
||||
<span className="app-chat-panel__resource-chip-main">
|
||||
<span className="app-chat-panel__resource-chip-icon" aria-hidden="true">
|
||||
{resolveChatPreviewGlyph(item.kind as ChatPreviewKind)}
|
||||
</span>
|
||||
<span className="app-chat-panel__resource-chip-label" title={item.label}>
|
||||
{item.label}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="app-chat-panel__resource-chip-meta"
|
||||
aria-label={`${resolveChatPreviewKindLabel(item.kind as ChatPreviewKind)} 형식`}
|
||||
>
|
||||
{buildResourceChipMeta(item)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1638,8 +1743,17 @@ export function ChatConversationView({
|
||||
const canCollapseMessage = collapsibleMessageIds.includes(message.id);
|
||||
const isExpandedMessage = expandedMessageIds.includes(message.id);
|
||||
const shouldTruncateMessage = canCollapseMessage && !isExpandedMessage;
|
||||
const messageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}`;
|
||||
const isRecoveredMissingRequest = isMissingRequestMessage(message);
|
||||
const isRecoveredExecutionFailure = isExecutionFailureMessage(message);
|
||||
const baseMessageBodyClassName = `app-chat-message__body${shouldTruncateMessage ? ' app-chat-message__body--collapsed' : ''}${
|
||||
isRecoveredMissingRequest || isRecoveredExecutionFailure ? ' app-chat-message__body--system-status' : ''
|
||||
}`;
|
||||
const { previewSourceText, visibleText, diffBlocks, rankedLinkTargets, linkCardTargets } = extractMessageRenderPayload(message);
|
||||
const renderedText = isRecoveredMissingRequest
|
||||
? getMissingRequestMessageText(message)
|
||||
: isRecoveredExecutionFailure
|
||||
? getExecutionFailureMessageText(message)
|
||||
: visibleText;
|
||||
|
||||
if (isActivityLogMessage(message)) {
|
||||
return renderActivityCard(message);
|
||||
@@ -1651,9 +1765,9 @@ export function ChatConversationView({
|
||||
const shouldRenderStandalonePreview =
|
||||
hasPreviewCards && !visibleText && (message.author === 'codex' || message.author === 'system');
|
||||
const stackClassName = [
|
||||
`app-chat-message-stack app-chat-message-stack--${message.author}`,
|
||||
shouldRenderStandalonePreview ? 'app-chat-message-stack--artifact-only' : '',
|
||||
]
|
||||
`app-chat-message-stack app-chat-message-stack--${message.author}`,
|
||||
shouldRenderStandalonePreview ? 'app-chat-message-stack--artifact-only' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const requestState = message.clientRequestId ? requestStateMap.get(message.clientRequestId) : undefined;
|
||||
@@ -1663,10 +1777,26 @@ export function ChatConversationView({
|
||||
return (
|
||||
<div key={message.id} className={stackClassName}>
|
||||
{shouldRenderStandalonePreview ? null : (
|
||||
<article className={`app-chat-message app-chat-message--${message.author}`}>
|
||||
<article
|
||||
className={`app-chat-message ${
|
||||
isRecoveredMissingRequest || isRecoveredExecutionFailure
|
||||
? 'app-chat-message--system-inline'
|
||||
: `app-chat-message--${message.author}`
|
||||
}`}
|
||||
>
|
||||
<div className="app-chat-message__header">
|
||||
<div className="app-chat-message__header-meta">
|
||||
<strong>{message.author === 'codex' ? 'Codex' : message.author === 'user' ? 'You' : 'System'}</strong>
|
||||
<strong>
|
||||
{isRecoveredMissingRequest
|
||||
? '원문 누락'
|
||||
: isRecoveredExecutionFailure
|
||||
? '실행 실패'
|
||||
: message.author === 'codex'
|
||||
? 'Codex'
|
||||
: message.author === 'user'
|
||||
? 'You'
|
||||
: 'System'}
|
||||
</strong>
|
||||
<span>{formatChatTimestamp(message.timestamp)}</span>
|
||||
{message.author === 'user' && requestStatusLabel ? (
|
||||
<span className="app-chat-message__status" aria-label={`요청 상태 ${requestStatusLabel}`}>
|
||||
@@ -1743,13 +1873,13 @@ export function ChatConversationView({
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
ref={(element) => {
|
||||
setMessageBodyRef(message.id, element);
|
||||
}}
|
||||
className={messageBodyClassName}
|
||||
>
|
||||
{visibleText ? renderMessageBody(visibleText) : null}
|
||||
<div
|
||||
ref={(element) => {
|
||||
setMessageBodyRef(message.id, element);
|
||||
}}
|
||||
className={baseMessageBodyClassName}
|
||||
>
|
||||
{renderedText ? renderMessageBody(renderedText) : null}
|
||||
</div>
|
||||
{message.author === 'user' && requestDetailText ? (
|
||||
<div className="app-chat-message__request-detail" role="status" aria-live="polite">
|
||||
@@ -1917,22 +2047,38 @@ export function ChatConversationView({
|
||||
isSendWithoutContextEnabled ? ' app-chat-panel__composer-contextless-toggle--active' : ''
|
||||
}`}
|
||||
icon={<DisconnectOutlined />}
|
||||
aria-label={isSendWithoutContextEnabled ? '이전 문맥 없이 보내기 켜짐' : '이전 문맥 없이 보내기 꺼짐'}
|
||||
title={isSendWithoutContextEnabled ? '이전 문맥 없이 보내기 켜짐' : '이전 문맥 없이 보내기 꺼짐'}
|
||||
aria-label={
|
||||
isSendWithoutContextEnabled
|
||||
? '다음 1회만 문맥 없이 보냄'
|
||||
: '다음 전송을 문맥 없이 보내기'
|
||||
}
|
||||
title={
|
||||
isSendWithoutContextEnabled
|
||||
? '다음 1회만 문맥 없이 보냄'
|
||||
: '다음 전송을 문맥 없이 보내기'
|
||||
}
|
||||
onClick={onToggleSendWithoutContext}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
<Button
|
||||
icon={<ThunderboltOutlined />}
|
||||
aria-label="즉시 요청"
|
||||
onClick={onSendImmediate}
|
||||
onClick={() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
onDraftChange(composerDraft);
|
||||
onSendImmediate(composerDraft);
|
||||
}}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
aria-label="큐로 보내기"
|
||||
onClick={onSend}
|
||||
onClick={() => {
|
||||
lastReportedDraftRef.current = composerDraft;
|
||||
onDraftChange(composerDraft);
|
||||
onSend(composerDraft);
|
||||
}}
|
||||
disabled={isComposerDisabled || isComposerAttachmentUploading}
|
||||
/>
|
||||
</div>
|
||||
@@ -1987,12 +2133,12 @@ export function ChatConversationView({
|
||||
|
||||
<Input.TextArea
|
||||
ref={composerRef}
|
||||
value={draft}
|
||||
value={composerDraft}
|
||||
autoSize={false}
|
||||
placeholder={composerPlaceholder}
|
||||
disabled={isComposerDisabled}
|
||||
onChange={(event) => {
|
||||
onDraftChange(event.target.value);
|
||||
setComposerDraft(event.target.value);
|
||||
}}
|
||||
onPaste={handleComposerPaste}
|
||||
onKeyDown={(event) => {
|
||||
@@ -2012,16 +2158,18 @@ export function ChatConversationView({
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onSend();
|
||||
lastReportedDraftRef.current = event.currentTarget.value;
|
||||
onDraftChange(event.currentTarget.value);
|
||||
onSend(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className={`app-chat-panel__composer-clear${draft.trim() ? ' app-chat-panel__composer-clear--visible' : ''}`}
|
||||
className={`app-chat-panel__composer-clear${composerDraft.trim() ? ' app-chat-panel__composer-clear--visible' : ''}`}
|
||||
aria-label="입력창 비우기"
|
||||
onClick={onClearDraft}
|
||||
disabled={!draft.trim()}
|
||||
disabled={!composerDraft.trim()}
|
||||
>
|
||||
clear
|
||||
</Button>
|
||||
|
||||
191
src/app/main/mainChatPanel/ChatDataTablePreview.tsx
Normal file
191
src/app/main/mainChatPanel/ChatDataTablePreview.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Typography } from 'antd';
|
||||
import type { ChatPreviewTarget } from './ChatPreviewBody';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type TableCellValue = string;
|
||||
|
||||
type TabularPreviewModel = {
|
||||
columns: string[];
|
||||
rows: TableCellValue[][];
|
||||
rowCount: number;
|
||||
sourceLabel: string;
|
||||
};
|
||||
|
||||
type ChatDataTablePreviewProps = {
|
||||
model: TabularPreviewModel;
|
||||
};
|
||||
|
||||
function stringifyCellValue(value: unknown): TableCellValue {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeObjectRows(rows: Record<string, unknown>[], sourceLabel: string): TabularPreviewModel | null {
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = Array.from(
|
||||
rows.reduce((set, row) => {
|
||||
Object.keys(row).forEach((key) => set.add(key));
|
||||
return set;
|
||||
}, new Set<string>()),
|
||||
);
|
||||
|
||||
if (!columns.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows: rows.map((row) => columns.map((column) => stringifyCellValue(row[column]))),
|
||||
rowCount: rows.length,
|
||||
sourceLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveJsonRows(value: unknown): Record<string, unknown>[] | null {
|
||||
if (Array.isArray(value) && value.every((item) => item && typeof item === 'object' && !Array.isArray(item))) {
|
||||
return value as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const entries = Object.values(value as Record<string, unknown>);
|
||||
for (const entry of entries) {
|
||||
const resolved = resolveJsonRows(entry);
|
||||
if (resolved?.length) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCsvLine(line: string) {
|
||||
const cells: string[] = [];
|
||||
let current = '';
|
||||
let quoted = false;
|
||||
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const char = line[index] ?? '';
|
||||
const nextChar = line[index + 1] ?? '';
|
||||
|
||||
if (char === '"') {
|
||||
if (quoted && nextChar === '"') {
|
||||
current += '"';
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ',' && !quoted) {
|
||||
cells.push(current.trim());
|
||||
current = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
current += char;
|
||||
}
|
||||
|
||||
cells.push(current.trim());
|
||||
return cells;
|
||||
}
|
||||
|
||||
function parseCsvTable(previewText: string, sourceLabel: string): TabularPreviewModel | null {
|
||||
const lines = previewText
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (lines.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const header = parseCsvLine(lines[0] ?? '');
|
||||
if (!header.length || header.every((column) => !column)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = lines.slice(1).map((line) => {
|
||||
const parsed = parseCsvLine(line);
|
||||
return header.map((_, index) => parsed[index] ?? '');
|
||||
});
|
||||
|
||||
return {
|
||||
columns: header,
|
||||
rows,
|
||||
rowCount: rows.length,
|
||||
sourceLabel,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTabularPreviewModel(target: ChatPreviewTarget, previewText: string): TabularPreviewModel | null {
|
||||
const pathname = target.url.toLowerCase().split('?')[0] ?? '';
|
||||
|
||||
if (pathname.endsWith('.json')) {
|
||||
try {
|
||||
const parsed = JSON.parse(previewText) as unknown;
|
||||
const rows = resolveJsonRows(parsed);
|
||||
return rows ? normalizeObjectRows(rows, target.label) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname.endsWith('.csv')) {
|
||||
return parseCsvTable(previewText, target.label);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ChatDataTablePreview({ model }: ChatDataTablePreviewProps) {
|
||||
return (
|
||||
<div className="app-chat-panel__preview-table">
|
||||
<div className="app-chat-panel__preview-table-meta">
|
||||
<Text strong>{model.sourceLabel}</Text>
|
||||
<Text type="secondary">{`행 ${model.rowCount}개 · 열 ${model.columns.length}개`}</Text>
|
||||
</div>
|
||||
<div className="app-chat-panel__preview-table-scroll">
|
||||
<table className="app-chat-panel__preview-table-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
{model.columns.map((column) => (
|
||||
<th key={column}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{model.rows.map((row, rowIndex) => (
|
||||
<tr key={`${model.sourceLabel}-${rowIndex}`}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={`${model.columns[cellIndex] ?? cellIndex}-${rowIndex}`}>{cell || '-'}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { InlineImage } from '../../../components/common/InlineImage';
|
||||
import { MarkdownPreviewContent } from '../../../components/markdownPreview/MarkdownPreviewContent';
|
||||
import { CodexDiffBlock } from '../../../components/previewer';
|
||||
import { inferCodeLanguage, renderEditorBlock } from '../../../components/previewer/renderers';
|
||||
import { ChatDataTablePreview, resolveTabularPreviewModel } from './ChatDataTablePreview';
|
||||
import { triggerResourceDownload } from './downloadUtils';
|
||||
import '../../../components/previewer/PreviewerUI.css';
|
||||
|
||||
@@ -359,6 +360,12 @@ export function ChatPreviewBody({
|
||||
}
|
||||
|
||||
if (target.kind === 'code' || target.kind === 'diff' || target.kind === 'document') {
|
||||
const tabularModel = resolveTabularPreviewModel(target, previewText);
|
||||
|
||||
if (tabularModel) {
|
||||
return <ChatDataTablePreview model={tabularModel} />;
|
||||
}
|
||||
|
||||
const resolvedLanguage = resolveCodeLanguage(target, previewText);
|
||||
|
||||
if (renderHtmlAsFrame && resolvedLanguage === 'html' && canRenderFramePreview(target.url)) {
|
||||
|
||||
32
src/app/main/mainChatPanel/chatResourceUrl.js
Normal file
32
src/app/main/mainChatPanel/chatResourceUrl.js
Normal file
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.normalizeChatResourceUrl = normalizeChatResourceUrl;
|
||||
var CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
|
||||
var CHAT_PUBLIC_RESOURCE_MARKER = '/.codex_chat/';
|
||||
function extractEmbeddedResourcePath(value) {
|
||||
var normalized = String(value !== null && value !== void 0 ? value : '').trim();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
var apiMarkerIndex = normalized.lastIndexOf(CHAT_API_RESOURCE_MARKER);
|
||||
if (apiMarkerIndex >= 0) {
|
||||
return normalized.slice(apiMarkerIndex);
|
||||
}
|
||||
var publicMarkerIndex = normalized.lastIndexOf(CHAT_PUBLIC_RESOURCE_MARKER);
|
||||
if (publicMarkerIndex >= 0) {
|
||||
return normalized.slice(publicMarkerIndex);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
function normalizeChatResourceUrl(value) {
|
||||
var normalized = extractEmbeddedResourcePath(value);
|
||||
if (typeof window === 'undefined') {
|
||||
return normalized;
|
||||
}
|
||||
try {
|
||||
return new URL(normalized, window.location.href).toString();
|
||||
}
|
||||
catch (_a) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
1693
src/app/main/mainChatPanel/chatUtils.js
Normal file
1693
src/app/main/mainChatPanel/chatUtils.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,8 @@ const CHAT_NOTIFY_OFFLINE_STORAGE_PREFIX = 'main-chat-panel:notify-offline:';
|
||||
const CHAT_SESSION_LAST_TYPE_STORAGE_PREFIX = 'main-chat-panel:last-chat-type:';
|
||||
const CHAT_INTRO_MESSAGE = '요청은 기본적으로 순차 처리됩니다. 급한 요청만 즉시 실행을 사용하세요.';
|
||||
const CHAT_ACTIVITY_MESSAGE_PREFIX = '[[activity-log]]';
|
||||
const CHAT_MISSING_REQUEST_MESSAGE_PREFIX = '[[missing-request]]';
|
||||
const CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX = '[[execution-failure]]';
|
||||
const CHAT_COMPOSER_UPLOAD_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
||||
const KST_TIME_ZONE = 'Asia/Seoul';
|
||||
const chatSessionLastTypeMemory = new Map<string, string>();
|
||||
@@ -46,18 +48,23 @@ function toConversationSortTime(value: string | null | undefined) {
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
function getConversationLastMessageSortTime(item: ChatConversationSummary) {
|
||||
const lastMessageTime = toConversationSortTime(item.lastMessageAt);
|
||||
|
||||
if (lastMessageTime > 0) {
|
||||
return lastMessageTime;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
toConversationSortTime(item.createdAt),
|
||||
toConversationSortTime(item.updatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
export function sortChatConversationSummaries(items: ChatConversationSummary[]) {
|
||||
return [...items].sort((left, right) => {
|
||||
const leftTime = Math.max(
|
||||
toConversationSortTime(left.lastMessageAt),
|
||||
toConversationSortTime(left.updatedAt),
|
||||
toConversationSortTime(left.createdAt),
|
||||
);
|
||||
const rightTime = Math.max(
|
||||
toConversationSortTime(right.lastMessageAt),
|
||||
toConversationSortTime(right.updatedAt),
|
||||
toConversationSortTime(right.createdAt),
|
||||
);
|
||||
const leftTime = getConversationLastMessageSortTime(left);
|
||||
const rightTime = getConversationLastMessageSortTime(right);
|
||||
|
||||
if (rightTime !== leftTime) {
|
||||
return rightTime - leftTime;
|
||||
@@ -289,18 +296,29 @@ function createLocalMessageId() {
|
||||
return Date.now() * 1_000 + localMessageSequence;
|
||||
}
|
||||
|
||||
function createRecoveredMessageId(requestId: string, variant: 'user' | 'codex' | 'activity') {
|
||||
function createRecoveredMessageId(
|
||||
requestId: string,
|
||||
variant: 'user' | 'codex' | 'activity' | 'missing-request' | 'execution-failure',
|
||||
) {
|
||||
const baseId = hashRequestId(requestId) * 10;
|
||||
|
||||
if (variant === 'user') {
|
||||
return -(baseId + 3);
|
||||
}
|
||||
|
||||
if (variant === 'activity') {
|
||||
if (variant === 'missing-request') {
|
||||
return -(baseId + 2);
|
||||
}
|
||||
|
||||
return -(baseId + 1);
|
||||
if (variant === 'activity') {
|
||||
return -(baseId + 1);
|
||||
}
|
||||
|
||||
if (variant === 'execution-failure') {
|
||||
return -(baseId + 4);
|
||||
}
|
||||
|
||||
return -(baseId + 5);
|
||||
}
|
||||
|
||||
function hashRequestId(value: string) {
|
||||
@@ -355,6 +373,170 @@ function isActivityLogMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
export function isMissingRequestMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
export function isExecutionFailureMessage(message: ChatMessage) {
|
||||
return message.author === 'system' && message.text.startsWith(`${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n`);
|
||||
}
|
||||
|
||||
function isEmptyCodexExecutionResponse(text: string) {
|
||||
const normalized = text.replace(/\s+/g, ' ').trim();
|
||||
return normalized === 'Codex 실행 결과가 비어 있습니다.';
|
||||
}
|
||||
|
||||
function extractActivityLogFailureReason(lines?: string[] | null) {
|
||||
const normalizedLines = (lines ?? []).map((line) => line.trim()).filter(Boolean);
|
||||
|
||||
for (let index = normalizedLines.length - 1; index >= 0; index -= 1) {
|
||||
const line = normalizedLines[index];
|
||||
|
||||
if (!line.startsWith('# 오류:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const raw = line.slice('# 오류:'.length).trim();
|
||||
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { message?: unknown };
|
||||
const message = typeof parsed.message === 'string' ? parsed.message.trim() : '';
|
||||
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildExecutionFailureMessage(reason: string) {
|
||||
const normalizedReason = reason.trim();
|
||||
|
||||
if (!normalizedReason) {
|
||||
return `${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n실행 중 오류가 발생했습니다.`;
|
||||
}
|
||||
|
||||
const simplifiedReason = normalizedReason.includes("mkdir '") && normalizedReason.includes('/resource/uploads')
|
||||
? `세션 리소스 업로드 폴더를 만들 권한이 없어 응답 생성이 중단되었습니다.\n\n원인: ${normalizedReason}`
|
||||
: `실행 중 오류가 발생했습니다.\n\n원인: ${normalizedReason}`;
|
||||
|
||||
return `${CHAT_EXECUTION_FAILURE_MESSAGE_PREFIX}\n${simplifiedReason}`;
|
||||
}
|
||||
|
||||
function buildFailurePreviewText(reason: string) {
|
||||
const normalizedReason = reason.trim();
|
||||
|
||||
if (!normalizedReason) {
|
||||
return '실행 실패';
|
||||
}
|
||||
|
||||
if (normalizedReason.includes("mkdir '") && normalizedReason.includes('/resource/uploads')) {
|
||||
return '실행 실패: 세션 리소스 업로드 폴더 권한 오류';
|
||||
}
|
||||
|
||||
return `실행 실패: ${normalizedReason}`;
|
||||
}
|
||||
|
||||
function enrichFailedRequestsWithActivityLogs(
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
) {
|
||||
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
|
||||
|
||||
return requests.map((request) => {
|
||||
if (request.status !== 'failed') {
|
||||
return request;
|
||||
}
|
||||
|
||||
const activityLog = activityLogMap.get(request.requestId.trim());
|
||||
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
|
||||
const normalizedStatusMessage = String(request.statusMessage ?? '').trim();
|
||||
|
||||
if (!failureReason) {
|
||||
return request;
|
||||
}
|
||||
|
||||
if (normalizedStatusMessage && normalizedStatusMessage !== '요청 처리 실패') {
|
||||
return request;
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
statusMessage: failureReason,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function replaceGenericFailureMessages(
|
||||
messages: ChatMessage[],
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
): ChatMessage[] {
|
||||
const requestMap = new Map(requests.map((item) => [item.requestId.trim(), item]));
|
||||
const activityLogMap = new Map(activityLogs.map((item) => [item.requestId.trim(), item]));
|
||||
|
||||
return messages.map((message) => {
|
||||
const requestId = message.clientRequestId?.trim() ?? '';
|
||||
|
||||
if (!requestId || message.author !== 'codex' || !isEmptyCodexExecutionResponse(message.text)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const request = requestMap.get(requestId);
|
||||
|
||||
if (request?.status !== 'failed') {
|
||||
return message;
|
||||
}
|
||||
|
||||
const failureReason = extractActivityLogFailureReason(activityLogMap.get(requestId)?.lines);
|
||||
|
||||
if (!failureReason) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
author: 'system' as const,
|
||||
text: buildExecutionFailureMessage(failureReason),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function resolveConversationFailurePreview(
|
||||
currentPreview: string,
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
) {
|
||||
if (!isEmptyCodexExecutionResponse(currentPreview)) {
|
||||
return currentPreview;
|
||||
}
|
||||
|
||||
const latestFailedRequest = [...requests]
|
||||
.reverse()
|
||||
.find((request) => request.status === 'failed' && isEmptyCodexExecutionResponse(String(request.responseText ?? '').trim()));
|
||||
|
||||
if (!latestFailedRequest) {
|
||||
return currentPreview;
|
||||
}
|
||||
|
||||
const activityLog = activityLogs.find((item) => item.requestId.trim() === latestFailedRequest.requestId.trim());
|
||||
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
|
||||
|
||||
if (!failureReason) {
|
||||
return currentPreview;
|
||||
}
|
||||
|
||||
return buildFailurePreviewText(failureReason);
|
||||
}
|
||||
|
||||
function extractActivityLogLines(text: string) {
|
||||
return text
|
||||
.slice(`${CHAT_ACTIVITY_MESSAGE_PREFIX}\n`.length)
|
||||
@@ -934,7 +1116,9 @@ export async function fetchChatConversations() {
|
||||
}
|
||||
|
||||
const clientId = getOrCreateClientId();
|
||||
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>('/conversations')
|
||||
chatConversationListRequestPromise = requestChatApi<{ ok: boolean; items: ChatConversationSummary[] }>(
|
||||
'/conversations?limit=200',
|
||||
)
|
||||
.then((response) => {
|
||||
return sortChatConversationSummaries(
|
||||
response.items.map((item) => ({
|
||||
@@ -971,17 +1155,24 @@ export async function fetchChatConversationDetail(
|
||||
const response = await requestChatApi<ChatConversationDetailResponse>(
|
||||
`/conversations/${encodeURIComponent(sessionId)}${query.toString() ? `?${query.toString()}` : ''}`,
|
||||
);
|
||||
const normalizedRequests = response.requests.map((item) => normalizeChatConversationRequest(item));
|
||||
const normalizedRequests = enrichFailedRequestsWithActivityLogs(
|
||||
response.requests.map((item) => normalizeChatConversationRequest(item)),
|
||||
response.activityLogs,
|
||||
);
|
||||
const visibleRequestIds = new Set(
|
||||
response.messages
|
||||
.map((message) => message.clientRequestId?.trim() ?? '')
|
||||
.filter(Boolean),
|
||||
);
|
||||
const hydratedMessages = hydrateActivityLogMessages(
|
||||
response.messages,
|
||||
replaceGenericFailureMessages(response.messages, normalizedRequests, response.activityLogs),
|
||||
response.activityLogs.filter((item) => visibleRequestIds.has(item.requestId?.trim() ?? '')),
|
||||
).filter(
|
||||
(message) => message.author !== 'system' || isActivityLogMessage(message),
|
||||
(message) =>
|
||||
message.author !== 'system' ||
|
||||
isActivityLogMessage(message) ||
|
||||
isMissingRequestMessage(message) ||
|
||||
isExecutionFailureMessage(message),
|
||||
);
|
||||
const recoveredMessages = buildRecoveredMessagesFromConversationDetail(normalizedRequests, response.activityLogs);
|
||||
|
||||
@@ -990,6 +1181,11 @@ export async function fetchChatConversationDetail(
|
||||
messages: mergeRecoveredChatMessages(recoveredMessages, hydratedMessages),
|
||||
item: {
|
||||
...response.item,
|
||||
lastMessagePreview: resolveConversationFailurePreview(
|
||||
response.item.lastMessagePreview,
|
||||
normalizedRequests,
|
||||
response.activityLogs,
|
||||
),
|
||||
notifyOffline: resolveSyncedChatOfflineNotificationSetting(
|
||||
response.item.sessionId,
|
||||
response.item.notifyOffline,
|
||||
@@ -1123,6 +1319,7 @@ export async function createChatConversationRoom(args: {
|
||||
title?: string;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
contextLabel?: string;
|
||||
contextDescription?: string;
|
||||
notifyOffline?: boolean;
|
||||
@@ -1136,6 +1333,7 @@ export async function createChatConversationRoom(args: {
|
||||
title: args.title ?? '새 대화',
|
||||
chatTypeId: args.chatTypeId ?? null,
|
||||
lastChatTypeId: args.lastChatTypeId ?? args.chatTypeId ?? null,
|
||||
generalSectionName: args.generalSectionName ?? null,
|
||||
contextLabel: args.contextLabel ?? null,
|
||||
contextDescription: args.contextDescription ?? null,
|
||||
notifyOffline,
|
||||
@@ -1173,6 +1371,7 @@ export async function updateChatConversationRoom(
|
||||
title?: string;
|
||||
chatTypeId?: string | null;
|
||||
lastChatTypeId?: string | null;
|
||||
generalSectionName?: string | null;
|
||||
contextLabel?: string | null;
|
||||
contextDescription?: string | null;
|
||||
notifyOffline?: boolean;
|
||||
@@ -1307,6 +1506,14 @@ function isSameChatMessage(left: ChatMessage, right: ChatMessage) {
|
||||
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
|
||||
}
|
||||
|
||||
if (isMissingRequestMessage(left) && isMissingRequestMessage(right)) {
|
||||
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
|
||||
}
|
||||
|
||||
if (isExecutionFailureMessage(left) && isExecutionFailureMessage(right)) {
|
||||
return Boolean(left.clientRequestId && right.clientRequestId && left.clientRequestId === right.clientRequestId);
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
(left.author === 'user' || left.author === 'codex') &&
|
||||
left.author === right.author &&
|
||||
@@ -1321,6 +1528,14 @@ function buildComparableChatMessageKey(message: ChatMessage) {
|
||||
return `activity:${message.clientRequestId}`;
|
||||
}
|
||||
|
||||
if (isMissingRequestMessage(message) && message.clientRequestId) {
|
||||
return `missing-request:${message.clientRequestId}`;
|
||||
}
|
||||
|
||||
if (isExecutionFailureMessage(message) && message.clientRequestId) {
|
||||
return `execution-failure:${message.clientRequestId}`;
|
||||
}
|
||||
|
||||
if (message.author === 'user' && message.clientRequestId) {
|
||||
return `user-request:${message.clientRequestId}`;
|
||||
}
|
||||
@@ -1337,6 +1552,123 @@ function getComparableChatMessageTime(message: ChatMessage) {
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function getChatMessageRequestId(message: ChatMessage) {
|
||||
return message.clientRequestId?.trim() || '';
|
||||
}
|
||||
|
||||
function getChatMessageOrderRank(message: ChatMessage) {
|
||||
if (message.author === 'user') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isMissingRequestMessage(message)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isExecutionFailureMessage(message)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (isActivityLogMessage(message)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (message.author === 'codex') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
function sortConversationMessages(messages: ChatMessage[]) {
|
||||
if (messages.length <= 1) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const messageIndexMap = new Map(messages.map((message, index) => [message, index]));
|
||||
const requestOrder = new Map<
|
||||
string,
|
||||
{
|
||||
time: number;
|
||||
firstIndex: number;
|
||||
}
|
||||
>();
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
const requestId = getChatMessageRequestId(message);
|
||||
|
||||
if (!requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const time = getComparableChatMessageTime(message);
|
||||
const existing = requestOrder.get(requestId);
|
||||
|
||||
if (!existing) {
|
||||
requestOrder.set(requestId, {
|
||||
time,
|
||||
firstIndex: index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
requestOrder.set(requestId, {
|
||||
time:
|
||||
existing.time > 0 && time > 0
|
||||
? Math.min(existing.time, time)
|
||||
: existing.time > 0
|
||||
? existing.time
|
||||
: time,
|
||||
firstIndex: Math.min(existing.firstIndex, index),
|
||||
});
|
||||
});
|
||||
|
||||
return [...messages].sort((left, right) => {
|
||||
const leftRequestId = getChatMessageRequestId(left);
|
||||
const rightRequestId = getChatMessageRequestId(right);
|
||||
|
||||
if (leftRequestId && rightRequestId && leftRequestId === rightRequestId) {
|
||||
const rankDiff = getChatMessageOrderRank(left) - getChatMessageOrderRank(right);
|
||||
|
||||
if (rankDiff !== 0) {
|
||||
return rankDiff;
|
||||
}
|
||||
}
|
||||
|
||||
const leftOrder = leftRequestId ? requestOrder.get(leftRequestId) : null;
|
||||
const rightOrder = rightRequestId ? requestOrder.get(rightRequestId) : null;
|
||||
const leftTime = leftOrder?.time ?? getComparableChatMessageTime(left);
|
||||
const rightTime = rightOrder?.time ?? getComparableChatMessageTime(right);
|
||||
|
||||
if (leftTime !== rightTime) {
|
||||
return leftTime - rightTime;
|
||||
}
|
||||
|
||||
const leftIndex = leftOrder?.firstIndex ?? messageIndexMap.get(left) ?? 0;
|
||||
const rightIndex = rightOrder?.firstIndex ?? messageIndexMap.get(right) ?? 0;
|
||||
|
||||
if (leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
|
||||
if (leftRequestId && rightRequestId && leftRequestId !== rightRequestId) {
|
||||
const requestDiff = leftRequestId.localeCompare(rightRequestId, 'ko-KR');
|
||||
|
||||
if (requestDiff !== 0) {
|
||||
return requestDiff;
|
||||
}
|
||||
}
|
||||
|
||||
const messageTimeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
|
||||
|
||||
if (messageTimeDiff !== 0) {
|
||||
return messageTimeDiff;
|
||||
}
|
||||
|
||||
return left.id - right.id;
|
||||
});
|
||||
}
|
||||
|
||||
function buildRecoveredMessagesFromConversationDetail(
|
||||
requests: ChatConversationRequest[],
|
||||
activityLogs: ChatConversationActivityLog[],
|
||||
@@ -1354,6 +1686,9 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
const userText = String(request.userText ?? '').trim();
|
||||
const responseText = String(request.responseText ?? '').trim();
|
||||
const activityLog = activityLogMap.get(requestId);
|
||||
const failureReason = extractActivityLogFailureReason(activityLog?.lines);
|
||||
const shouldReplaceEmptyFailureResponse =
|
||||
request.status === 'failed' && isEmptyCodexExecutionResponse(responseText) && Boolean(failureReason);
|
||||
|
||||
if (userText) {
|
||||
nextMessages.push({
|
||||
@@ -1363,9 +1698,17 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
timestamp: request.createdAt || request.updatedAt || '',
|
||||
clientRequestId: requestId,
|
||||
});
|
||||
} else if (responseText || activityLog?.lines.length) {
|
||||
nextMessages.push({
|
||||
id: createRecoveredMessageId(requestId, 'missing-request'),
|
||||
author: 'system',
|
||||
text: `${CHAT_MISSING_REQUEST_MESSAGE_PREFIX}\n이 요청은 저장된 원문이 없어 실제 요청 문장을 표시할 수 없습니다.`,
|
||||
timestamp: request.createdAt || request.updatedAt || '',
|
||||
clientRequestId: requestId,
|
||||
});
|
||||
}
|
||||
|
||||
if (responseText) {
|
||||
if (responseText && !shouldReplaceEmptyFailureResponse) {
|
||||
nextMessages.push({
|
||||
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'codex'),
|
||||
author: 'codex',
|
||||
@@ -1375,6 +1718,16 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldReplaceEmptyFailureResponse) {
|
||||
nextMessages.push({
|
||||
id: request.responseMessageId ?? createRecoveredMessageId(requestId, 'execution-failure'),
|
||||
author: 'system',
|
||||
text: buildExecutionFailureMessage(failureReason),
|
||||
timestamp: request.answeredAt || request.updatedAt || request.createdAt || '',
|
||||
clientRequestId: requestId,
|
||||
});
|
||||
}
|
||||
|
||||
if (activityLog && activityLog.lines.length > 0) {
|
||||
nextMessages.push({
|
||||
id: createRecoveredMessageId(requestId, 'activity'),
|
||||
@@ -1386,15 +1739,7 @@ function buildRecoveredMessagesFromConversationDetail(
|
||||
}
|
||||
});
|
||||
|
||||
return nextMessages.sort((left, right) => {
|
||||
const timeDiff = getComparableChatMessageTime(left) - getComparableChatMessageTime(right);
|
||||
|
||||
if (timeDiff !== 0) {
|
||||
return timeDiff;
|
||||
}
|
||||
|
||||
return left.id - right.id;
|
||||
});
|
||||
return sortConversationMessages(nextMessages);
|
||||
}
|
||||
|
||||
export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: ChatMessage[]) {
|
||||
@@ -1459,7 +1804,7 @@ export function mergeRecoveredChatMessages(previous: ChatMessage[], incoming: Ch
|
||||
});
|
||||
|
||||
const unmatchedLocalMessages = Array.from(previousBuckets.values()).flat();
|
||||
const nextMessages = [...mergedServerMessages, ...unmatchedLocalMessages];
|
||||
const nextMessages = sortConversationMessages([...mergedServerMessages, ...unmatchedLocalMessages]);
|
||||
|
||||
return areChatMessagesEquivalent(previous, nextMessages) ? previous : nextMessages;
|
||||
}
|
||||
|
||||
49
src/app/main/mainChatPanel/index.js
Normal file
49
src/app/main/mainChatPanel/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useErrorLogs = exports.useChatConnection = exports.subscribeChatConnection = exports.setSharedChatRuntimeSnapshot = exports.getSharedChatRuntimeSnapshot = exports.updateChatConversationRoom = exports.upsertChatMessage = exports.uploadChatComposerFile = exports.sortChatConversationSummaries = exports.setChatClientSessionId = exports.setStoredChatSessionLastTypeId = exports.resetLastReceivedChatEventId = exports.removeChatRuntimeJob = exports.renameChatConversationRoom = exports.mergeRecoveredChatMessages = exports.markChatConversationResponsesRead = exports.getChatClientSessionId = exports.isPreparingChatReplyText = exports.isMissingRequestMessage = exports.getStoredChatSessionLastTypeId = exports.fetchChatRuntimeSnapshot = exports.fetchChatRuntimeJobDetail = exports.fetchChatConversations = exports.fetchChatConversationDetail = exports.deleteChatConversationRoom = exports.deleteChatConversationRequest = exports.cancelChatRuntimeJob = exports.createLocalMessage = exports.createIntroMessage = exports.createChatMessage = exports.createChatConversationRoom = exports.createActivityLogPlaceholder = exports.resolvePreviewBodyForCopy = exports.copyText = exports.copyPreviewContent = exports.clearStoredChatClientConversationState = exports.buildOfflineReply = exports.ErrorLogViewer = exports.ChatRuntimeDashboard = exports.ChatConversationView = void 0;
|
||||
var ChatConversationView_1 = require("./ChatConversationView");
|
||||
Object.defineProperty(exports, "ChatConversationView", { enumerable: true, get: function () { return ChatConversationView_1.ChatConversationView; } });
|
||||
var ChatRuntimeDashboard_1 = require("./ChatRuntimeDashboard");
|
||||
Object.defineProperty(exports, "ChatRuntimeDashboard", { enumerable: true, get: function () { return ChatRuntimeDashboard_1.ChatRuntimeDashboard; } });
|
||||
var ErrorLogViewer_1 = require("./ErrorLogViewer");
|
||||
Object.defineProperty(exports, "ErrorLogViewer", { enumerable: true, get: function () { return ErrorLogViewer_1.ErrorLogViewer; } });
|
||||
var chatUtils_1 = require("./chatUtils");
|
||||
Object.defineProperty(exports, "buildOfflineReply", { enumerable: true, get: function () { return chatUtils_1.buildOfflineReply; } });
|
||||
Object.defineProperty(exports, "clearStoredChatClientConversationState", { enumerable: true, get: function () { return chatUtils_1.clearStoredChatClientConversationState; } });
|
||||
Object.defineProperty(exports, "copyPreviewContent", { enumerable: true, get: function () { return chatUtils_1.copyPreviewContent; } });
|
||||
Object.defineProperty(exports, "copyText", { enumerable: true, get: function () { return chatUtils_1.copyText; } });
|
||||
Object.defineProperty(exports, "resolvePreviewBodyForCopy", { enumerable: true, get: function () { return chatUtils_1.resolvePreviewBodyForCopy; } });
|
||||
Object.defineProperty(exports, "createActivityLogPlaceholder", { enumerable: true, get: function () { return chatUtils_1.createActivityLogPlaceholder; } });
|
||||
Object.defineProperty(exports, "createChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.createChatConversationRoom; } });
|
||||
Object.defineProperty(exports, "createChatMessage", { enumerable: true, get: function () { return chatUtils_1.createChatMessage; } });
|
||||
Object.defineProperty(exports, "createIntroMessage", { enumerable: true, get: function () { return chatUtils_1.createIntroMessage; } });
|
||||
Object.defineProperty(exports, "createLocalMessage", { enumerable: true, get: function () { return chatUtils_1.createLocalMessage; } });
|
||||
Object.defineProperty(exports, "cancelChatRuntimeJob", { enumerable: true, get: function () { return chatUtils_1.cancelChatRuntimeJob; } });
|
||||
Object.defineProperty(exports, "deleteChatConversationRequest", { enumerable: true, get: function () { return chatUtils_1.deleteChatConversationRequest; } });
|
||||
Object.defineProperty(exports, "deleteChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.deleteChatConversationRoom; } });
|
||||
Object.defineProperty(exports, "fetchChatConversationDetail", { enumerable: true, get: function () { return chatUtils_1.fetchChatConversationDetail; } });
|
||||
Object.defineProperty(exports, "fetchChatConversations", { enumerable: true, get: function () { return chatUtils_1.fetchChatConversations; } });
|
||||
Object.defineProperty(exports, "fetchChatRuntimeJobDetail", { enumerable: true, get: function () { return chatUtils_1.fetchChatRuntimeJobDetail; } });
|
||||
Object.defineProperty(exports, "fetchChatRuntimeSnapshot", { enumerable: true, get: function () { return chatUtils_1.fetchChatRuntimeSnapshot; } });
|
||||
Object.defineProperty(exports, "getStoredChatSessionLastTypeId", { enumerable: true, get: function () { return chatUtils_1.getStoredChatSessionLastTypeId; } });
|
||||
Object.defineProperty(exports, "isMissingRequestMessage", { enumerable: true, get: function () { return chatUtils_1.isMissingRequestMessage; } });
|
||||
Object.defineProperty(exports, "isPreparingChatReplyText", { enumerable: true, get: function () { return chatUtils_1.isPreparingChatReplyText; } });
|
||||
Object.defineProperty(exports, "getChatClientSessionId", { enumerable: true, get: function () { return chatUtils_1.getChatClientSessionId; } });
|
||||
Object.defineProperty(exports, "markChatConversationResponsesRead", { enumerable: true, get: function () { return chatUtils_1.markChatConversationResponsesRead; } });
|
||||
Object.defineProperty(exports, "mergeRecoveredChatMessages", { enumerable: true, get: function () { return chatUtils_1.mergeRecoveredChatMessages; } });
|
||||
Object.defineProperty(exports, "renameChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.renameChatConversationRoom; } });
|
||||
Object.defineProperty(exports, "removeChatRuntimeJob", { enumerable: true, get: function () { return chatUtils_1.removeChatRuntimeJob; } });
|
||||
Object.defineProperty(exports, "resetLastReceivedChatEventId", { enumerable: true, get: function () { return chatUtils_1.resetLastReceivedChatEventId; } });
|
||||
Object.defineProperty(exports, "setStoredChatSessionLastTypeId", { enumerable: true, get: function () { return chatUtils_1.setStoredChatSessionLastTypeId; } });
|
||||
Object.defineProperty(exports, "setChatClientSessionId", { enumerable: true, get: function () { return chatUtils_1.setChatClientSessionId; } });
|
||||
Object.defineProperty(exports, "sortChatConversationSummaries", { enumerable: true, get: function () { return chatUtils_1.sortChatConversationSummaries; } });
|
||||
Object.defineProperty(exports, "uploadChatComposerFile", { enumerable: true, get: function () { return chatUtils_1.uploadChatComposerFile; } });
|
||||
Object.defineProperty(exports, "upsertChatMessage", { enumerable: true, get: function () { return chatUtils_1.upsertChatMessage; } });
|
||||
Object.defineProperty(exports, "updateChatConversationRoom", { enumerable: true, get: function () { return chatUtils_1.updateChatConversationRoom; } });
|
||||
var useChatConnection_1 = require("./useChatConnection");
|
||||
Object.defineProperty(exports, "getSharedChatRuntimeSnapshot", { enumerable: true, get: function () { return useChatConnection_1.getSharedChatRuntimeSnapshot; } });
|
||||
Object.defineProperty(exports, "setSharedChatRuntimeSnapshot", { enumerable: true, get: function () { return useChatConnection_1.setSharedChatRuntimeSnapshot; } });
|
||||
Object.defineProperty(exports, "subscribeChatConnection", { enumerable: true, get: function () { return useChatConnection_1.subscribeChatConnection; } });
|
||||
Object.defineProperty(exports, "useChatConnection", { enumerable: true, get: function () { return useChatConnection_1.useChatConnection; } });
|
||||
var useErrorLogs_1 = require("./useErrorLogs");
|
||||
Object.defineProperty(exports, "useErrorLogs", { enumerable: true, get: function () { return useErrorLogs_1.useErrorLogs; } });
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
fetchChatRuntimeJobDetail,
|
||||
fetchChatRuntimeSnapshot,
|
||||
getStoredChatSessionLastTypeId,
|
||||
isMissingRequestMessage,
|
||||
isPreparingChatReplyText,
|
||||
getChatClientSessionId,
|
||||
markChatConversationResponsesRead,
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
const AUTO_DETECTED_PREVIEW_URL_PATTERN = /(https?:\/\/[^\s<>)\]]+|\/[A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]+)/g;
|
||||
const AUTO_DETECTED_PREVIEW_URL_PATTERN =
|
||||
/(https?:\/\/[^\s<>)\]]+|\/(?:[A-Za-z0-9._~%-][A-Za-z0-9._~:/?#[\]@!$&'*+,;=%-]*))/g;
|
||||
const LOCAL_RESOURCE_PREFIXES = ['/api/chat/resources/', '/public/.codex_chat/', '/.codex_chat/'] as const;
|
||||
const PREVIEWABLE_FILE_EXTENSION_PATTERN =
|
||||
/\.(png|jpe?g|gif|webp|svg|bmp|ico|mp4|webm|mov|m4v|ogg|md|markdown|diff|patch|ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml|txt|log|csv|pdf)$/i;
|
||||
|
||||
function stripCodeFenceBlocks(text: string) {
|
||||
return String(text ?? '').replace(/```[\s\S]*?```/g, '');
|
||||
}
|
||||
|
||||
function trimAutoDetectedUrl(value: string) {
|
||||
return String(value ?? '').trim().replace(/[`\])}>.,;!?]+$/g, '');
|
||||
}
|
||||
|
||||
function isLikelyLocalPreviewUrl(value: string) {
|
||||
if (LOCAL_RESOURCE_PREFIXES.some((prefix) => value.startsWith(prefix))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathname = value.split(/[?#]/, 1)[0] ?? '';
|
||||
return PREVIEWABLE_FILE_EXTENSION_PATTERN.test(pathname);
|
||||
}
|
||||
|
||||
export function extractAutoDetectedPreviewUrls(text: string) {
|
||||
const normalized = String(text ?? '');
|
||||
const normalized = stripCodeFenceBlocks(text);
|
||||
const urls: string[] = [];
|
||||
|
||||
for (const match of normalized.matchAll(AUTO_DETECTED_PREVIEW_URL_PATTERN)) {
|
||||
const value = match[0]?.trim();
|
||||
const value = trimAutoDetectedUrl(match[0] ?? '');
|
||||
|
||||
if (!value) {
|
||||
continue;
|
||||
@@ -19,6 +40,10 @@ export function extractAutoDetectedPreviewUrls(text: string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith('/') && !isLikelyLocalPreviewUrl(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
urls.push(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ function normalizeUrl(value: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const malformedResourceMatch = normalized.match(/^https?:\/(api\/chat\/resources\/.+)$/i);
|
||||
if (malformedResourceMatch?.[1]) {
|
||||
return `/${malformedResourceMatch[1]}`;
|
||||
}
|
||||
|
||||
if (/^(?:https?:\/\/|\/)/i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
@@ -23,11 +28,43 @@ function normalizeUrl(value: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function decodeUrlComponentSafely(value: string) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLinkCardUrlAndActionLabel(rawUrl: string, rawActionLabel?: string) {
|
||||
let resolvedUrl = normalizeText(rawUrl);
|
||||
let resolvedActionLabel = normalizeText(rawActionLabel);
|
||||
|
||||
if (!resolvedActionLabel) {
|
||||
const decodedUrl = decodeUrlComponentSafely(resolvedUrl);
|
||||
const dividerIndex = decodedUrl.lastIndexOf('|');
|
||||
|
||||
if (dividerIndex > 0 && dividerIndex < decodedUrl.length - 1) {
|
||||
resolvedUrl = decodedUrl.slice(0, dividerIndex).trim();
|
||||
resolvedActionLabel = decodedUrl.slice(dividerIndex + 1).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: normalizeUrl(resolvedUrl),
|
||||
actionLabel: resolvedActionLabel || null,
|
||||
};
|
||||
}
|
||||
|
||||
function hasKnownFileExtension(url: string) {
|
||||
const pathname = url.split('?')[0] ?? '';
|
||||
return /\.[a-z0-9]{1,8}$/i.test(pathname);
|
||||
}
|
||||
|
||||
function isInternalResourceUrl(url: string) {
|
||||
return RESOURCE_PATH_PREFIXES.some((prefix) => url.startsWith(prefix));
|
||||
}
|
||||
|
||||
function isStructuredLinkCardCandidate(url: string) {
|
||||
const normalized = normalizeUrl(url);
|
||||
|
||||
@@ -35,15 +72,11 @@ function isStructuredLinkCardCandidate(url: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RESOURCE_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
|
||||
if (isInternalResourceUrl(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(normalized)) {
|
||||
return !hasKnownFileExtension(normalized);
|
||||
}
|
||||
|
||||
return !hasKnownFileExtension(normalized);
|
||||
return /^https?:\/\//i.test(normalized) && !hasKnownFileExtension(normalized);
|
||||
}
|
||||
|
||||
function buildFallbackLinkTitle(url: string) {
|
||||
@@ -88,8 +121,7 @@ function buildLinkCardPart(rawBody: string): ChatMessagePart | null {
|
||||
|
||||
const [rawTitle, rawUrl, rawActionLabel] = segments;
|
||||
const title = normalizeText(rawTitle);
|
||||
const url = normalizeUrl(rawUrl);
|
||||
const actionLabel = normalizeText(rawActionLabel) || null;
|
||||
const { url, actionLabel } = resolveLinkCardUrlAndActionLabel(rawUrl, rawActionLabel);
|
||||
|
||||
if (!title || !url) {
|
||||
return null;
|
||||
@@ -154,6 +186,14 @@ export function extractChatMessageParts(text: string) {
|
||||
|
||||
if (!pushPart(buildLinkCardPart(matched[1] ?? ''))) {
|
||||
keptLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const latestPart = parts.at(-1);
|
||||
if (latestPart && isInternalResourceUrl(latestPart.url)) {
|
||||
parts.pop();
|
||||
seenLinkKeys.delete(`${latestPart.type}:${latestPart.title}:${latestPart.url}:${latestPart.actionLabel ?? ''}`);
|
||||
keptLines.push(latestPart.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,117 @@ export type PreviewItem = {
|
||||
source: 'message' | 'context';
|
||||
};
|
||||
|
||||
const CHAT_RESOURCE_INTERNAL_SEGMENTS = new Set(['resource', 'uploads', 'source', 'src']);
|
||||
const CHAT_RESOURCE_HIDDEN_FILE_NAMES = new Set(['.env']);
|
||||
const CHAT_API_RESOURCE_MARKER = '/api/chat/resources/';
|
||||
const RESOURCE_STRIP_ALLOWED_KINDS = new Set<PreviewKind>([
|
||||
'image',
|
||||
'video',
|
||||
'markdown',
|
||||
'code',
|
||||
'diff',
|
||||
'document',
|
||||
'pdf',
|
||||
'file',
|
||||
]);
|
||||
|
||||
function normalizePreviewUrl(value: string) {
|
||||
return normalizeChatResourceUrl(value);
|
||||
}
|
||||
|
||||
function parsePreviewUrl(url: string) {
|
||||
try {
|
||||
return new URL(url, typeof window !== 'undefined' ? window.location.origin : 'https://local.invalid');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractInternalChatResourcePath(pathname: string) {
|
||||
const normalizedPathname = String(pathname ?? '').trim();
|
||||
|
||||
if (!normalizedPathname) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (normalizedPathname.includes('/.codex_chat/')) {
|
||||
const markerIndex = normalizedPathname.lastIndexOf('/.codex_chat/');
|
||||
return normalizedPathname.slice(markerIndex + 1);
|
||||
}
|
||||
|
||||
const apiMarkerIndex = normalizedPathname.lastIndexOf(CHAT_API_RESOURCE_MARKER);
|
||||
if (apiMarkerIndex >= 0) {
|
||||
return normalizedPathname.slice(apiMarkerIndex + CHAT_API_RESOURCE_MARKER.length).replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function hasVisibleFileExtension(fileName: string) {
|
||||
return /\.[a-z0-9]{1,16}$/i.test(fileName);
|
||||
}
|
||||
|
||||
function hasSupportedPreviewFileExtension(fileName: string) {
|
||||
return /\.(png|jpe?g|gif|webp|svg|bmp|ico|mp4|webm|mov|m4v|ogg|md|markdown|diff|patch|ts|tsx|js|jsx|json|css|scss|html|xml|java|kt|sql|sh|py|go|rs|c|cpp|h|hpp|yml|yaml|txt|log|csv|pdf)$/i.test(
|
||||
fileName,
|
||||
);
|
||||
}
|
||||
|
||||
function isMarkdownResourceFile(fileName: string) {
|
||||
return /\.(md|markdown)$/i.test(fileName);
|
||||
}
|
||||
|
||||
function shouldHideInternalChatResource(url: string) {
|
||||
const parsed = parsePreviewUrl(url);
|
||||
const pathname = parsed?.pathname ?? '';
|
||||
const internalResourcePath = extractInternalChatResourcePath(pathname);
|
||||
const normalizedInternalResourcePath = internalResourcePath.toLowerCase();
|
||||
|
||||
if (!normalizedInternalResourcePath.startsWith('.codex_chat/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const segments = internalResourcePath.split('/').filter(Boolean);
|
||||
const lastSegment = segments.at(-1)?.trim() ?? '';
|
||||
const normalizedLastSegment = lastSegment.toLowerCase();
|
||||
const resourceSegmentIndex = segments.findIndex((segment) => segment === 'resource');
|
||||
const nextSegment = resourceSegmentIndex >= 0 ? segments[resourceSegmentIndex + 1]?.toLowerCase() ?? '' : '';
|
||||
|
||||
if (!lastSegment || pathname.endsWith('/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CHAT_RESOURCE_INTERNAL_SEGMENTS.has(normalizedLastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasVisibleFileExtension(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CHAT_RESOURCE_INTERNAL_SEGMENTS.has(nextSegment) && !hasVisibleFileExtension(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedLastSegment.startsWith('.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CHAT_RESOURCE_HIDDEN_FILE_NAMES.has(normalizedLastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (resourceSegmentIndex >= 0 && nextSegment === 'src' && !isMarkdownResourceFile(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hasSupportedPreviewFileExtension(lastSegment)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPreviewRouteUrl(url: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
@@ -118,15 +225,23 @@ export function extractPreviewItems(messages: ChatMessage[]) {
|
||||
)
|
||||
.map((part) => part.url);
|
||||
const matches = [
|
||||
...extractAutoDetectedPreviewUrls(message.text),
|
||||
...extractHiddenPreviewUrls(message.text),
|
||||
...structuredLinkUrls,
|
||||
...extractAutoDetectedPreviewUrls(message.text),
|
||||
];
|
||||
|
||||
matches.forEach((matchedUrl) => {
|
||||
const normalizedUrl = normalizePreviewUrl(matchedUrl);
|
||||
const kind = classifyPreviewKind(normalizedUrl);
|
||||
|
||||
if (shouldHideInternalChatResource(normalizedUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RESOURCE_STRIP_ALLOWED_KINDS.has(kind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen.has(normalizedUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
2
src/app/main/mainChatPanel/types.js
Normal file
2
src/app/main/mainChatPanel/types.js
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
@@ -44,9 +44,11 @@ export type ChatViewContext = {
|
||||
export type ChatConversationSummary = {
|
||||
sessionId: string;
|
||||
clientId: string | null;
|
||||
isDraftOnly?: boolean;
|
||||
title: string;
|
||||
chatTypeId: string | null;
|
||||
lastChatTypeId: string | null;
|
||||
generalSectionName: string | null;
|
||||
contextLabel: string | null;
|
||||
contextDescription: string | null;
|
||||
notifyOffline: boolean;
|
||||
@@ -56,7 +58,9 @@ export type ChatConversationSummary = {
|
||||
currentJobMessage: string | null;
|
||||
currentQueueSize: number;
|
||||
currentStatusUpdatedAt: string | null;
|
||||
lastRequestPreview: string;
|
||||
lastMessagePreview: string;
|
||||
lastResponsePreview: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastMessageAt: string | null;
|
||||
|
||||
495
src/app/main/mainChatPanel/useChatConnection.js
Normal file
495
src/app/main/mainChatPanel/useChatConnection.js
Normal file
@@ -0,0 +1,495 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getChatConnectionSnapshot = getChatConnectionSnapshot;
|
||||
exports.subscribeChatConnection = subscribeChatConnection;
|
||||
exports.getSharedChatRuntimeSnapshot = getSharedChatRuntimeSnapshot;
|
||||
exports.setSharedChatRuntimeSnapshot = setSharedChatRuntimeSnapshot;
|
||||
exports.useChatConnection = useChatConnection;
|
||||
var react_1 = require("react");
|
||||
var chatUtils_1 = require("./chatUtils");
|
||||
var tokenAccess_1 = require("../tokenAccess");
|
||||
var DISCONNECT_UI_DELAY_MS = 1500;
|
||||
var PRESENCE_PING_INTERVAL_MS = 20000;
|
||||
var sharedChatConnection = {
|
||||
connectionState: 'connecting',
|
||||
connectionErrorDetail: '',
|
||||
runtimeSnapshot: null,
|
||||
socketRef: { current: null },
|
||||
reconnectTimerId: null,
|
||||
disconnectUiTimerId: null,
|
||||
connectTimeoutId: null,
|
||||
sessionId: '',
|
||||
currentContext: null,
|
||||
setMessages: null,
|
||||
onMessageEvent: undefined,
|
||||
onJobEvent: undefined,
|
||||
onRuntimeEvent: undefined,
|
||||
onRuntimeDetailEvent: undefined,
|
||||
onActivityEvent: undefined,
|
||||
lastEventId: 0,
|
||||
websocketUrl: '',
|
||||
subscribers: new Set(),
|
||||
pingSubscriberCount: 0,
|
||||
consumerCount: 0,
|
||||
pingIntervalId: null,
|
||||
visibilityHandlerInstalled: false,
|
||||
pageShowHandlerInstalled: false,
|
||||
focusHandlerInstalled: false,
|
||||
onlineHandlerInstalled: false,
|
||||
hasConnectedOnce: false,
|
||||
suppressDisconnectNotification: false,
|
||||
lastBackgroundAt: null,
|
||||
};
|
||||
function emitSharedState() {
|
||||
sharedChatConnection.subscribers.forEach(function (listener) {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
function getSnapshot() {
|
||||
return {
|
||||
connectionState: sharedChatConnection.connectionState,
|
||||
connectionErrorDetail: sharedChatConnection.connectionErrorDetail,
|
||||
runtimeSnapshot: sharedChatConnection.runtimeSnapshot,
|
||||
};
|
||||
}
|
||||
function getChatConnectionSnapshot() {
|
||||
return getSnapshot();
|
||||
}
|
||||
function subscribeChatConnection(listener) {
|
||||
sharedChatConnection.subscribers.add(listener);
|
||||
return function () {
|
||||
sharedChatConnection.subscribers.delete(listener);
|
||||
};
|
||||
}
|
||||
function getSharedChatRuntimeSnapshot() {
|
||||
return sharedChatConnection.runtimeSnapshot;
|
||||
}
|
||||
function setSharedChatRuntimeSnapshot(snapshot) {
|
||||
if (sharedChatConnection.runtimeSnapshot === snapshot) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.runtimeSnapshot = snapshot;
|
||||
emitSharedState();
|
||||
}
|
||||
function setSharedConnectionState(nextState) {
|
||||
if (sharedChatConnection.connectionState === nextState) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.connectionState = nextState;
|
||||
emitSharedState();
|
||||
}
|
||||
function setSharedConnectionError(detail) {
|
||||
if (sharedChatConnection.connectionErrorDetail === detail) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.connectionErrorDetail = detail;
|
||||
emitSharedState();
|
||||
}
|
||||
function clearReconnectTimer() {
|
||||
if (sharedChatConnection.reconnectTimerId !== null) {
|
||||
window.clearTimeout(sharedChatConnection.reconnectTimerId);
|
||||
sharedChatConnection.reconnectTimerId = null;
|
||||
}
|
||||
}
|
||||
function clearDisconnectUiTimer() {
|
||||
if (sharedChatConnection.disconnectUiTimerId !== null) {
|
||||
window.clearTimeout(sharedChatConnection.disconnectUiTimerId);
|
||||
sharedChatConnection.disconnectUiTimerId = null;
|
||||
}
|
||||
}
|
||||
function clearConnectTimeout() {
|
||||
if (sharedChatConnection.connectTimeoutId !== null) {
|
||||
window.clearTimeout(sharedChatConnection.connectTimeoutId);
|
||||
sharedChatConnection.connectTimeoutId = null;
|
||||
}
|
||||
}
|
||||
function sendContextUpdate(context) {
|
||||
if (context === void 0) { context = sharedChatConnection.currentContext; }
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN || !context) {
|
||||
return;
|
||||
}
|
||||
var liveVisibilityState = typeof document !== 'undefined' && document.visibilityState === 'hidden' ? 'hidden' : 'visible';
|
||||
var livePageUrl = typeof window !== 'undefined' ? window.location.href : context.pageUrl;
|
||||
socket.send(JSON.stringify({
|
||||
type: 'context:update',
|
||||
payload: {
|
||||
pageId: context.pageId,
|
||||
pageTitle: context.pageTitle,
|
||||
topMenu: context.topMenu,
|
||||
focusedComponentId: context.focusedComponentId,
|
||||
pageUrl: livePageUrl,
|
||||
isStandaloneMode: context.isStandaloneMode,
|
||||
pageVisibilityState: liveVisibilityState,
|
||||
chatTypeId: context.chatTypeId,
|
||||
chatTypeLabel: context.chatTypeLabel,
|
||||
chatTypeDescription: context.chatTypeDescription,
|
||||
},
|
||||
}));
|
||||
}
|
||||
function sendPresencePing() {
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
type: 'presence:ping',
|
||||
payload: {
|
||||
at: Date.now(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
function ensureSharedSocket() {
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
connectSharedSocket();
|
||||
}
|
||||
function sendEventReceived(eventId) {
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN || !Number.isFinite(eventId) || eventId <= 0) {
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
type: 'event:received',
|
||||
payload: {
|
||||
eventId: eventId,
|
||||
},
|
||||
}));
|
||||
}
|
||||
function stopPresenceMonitoring() {
|
||||
if (sharedChatConnection.pingIntervalId !== null) {
|
||||
window.clearInterval(sharedChatConnection.pingIntervalId);
|
||||
sharedChatConnection.pingIntervalId = null;
|
||||
}
|
||||
if (sharedChatConnection.visibilityHandlerInstalled) {
|
||||
window.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
sharedChatConnection.visibilityHandlerInstalled = false;
|
||||
}
|
||||
if (sharedChatConnection.pageShowHandlerInstalled) {
|
||||
window.removeEventListener('pageshow', handlePageShow);
|
||||
sharedChatConnection.pageShowHandlerInstalled = false;
|
||||
}
|
||||
if (sharedChatConnection.focusHandlerInstalled) {
|
||||
window.removeEventListener('focus', handleWindowFocus);
|
||||
sharedChatConnection.focusHandlerInstalled = false;
|
||||
}
|
||||
if (sharedChatConnection.onlineHandlerInstalled) {
|
||||
window.removeEventListener('online', handleWindowOnline);
|
||||
sharedChatConnection.onlineHandlerInstalled = false;
|
||||
}
|
||||
}
|
||||
function handleVisibilityChange() {
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
||||
sharedChatConnection.lastBackgroundAt = Date.now();
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
sendPresencePing();
|
||||
return;
|
||||
}
|
||||
ensureSharedSocket();
|
||||
sendPresencePing();
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
}
|
||||
function handlePageShow() {
|
||||
ensureSharedSocket();
|
||||
sendPresencePing();
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
}
|
||||
function handleWindowFocus() {
|
||||
ensureSharedSocket();
|
||||
sendPresencePing();
|
||||
}
|
||||
function handleWindowOnline() {
|
||||
ensureSharedSocket();
|
||||
}
|
||||
function startPresenceMonitoring() {
|
||||
if (sharedChatConnection.pingSubscriberCount <= 0 || sharedChatConnection.connectionState !== 'connected') {
|
||||
stopPresenceMonitoring();
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.lastBackgroundAt = null;
|
||||
sendPresencePing();
|
||||
if (sharedChatConnection.pingIntervalId === null) {
|
||||
sharedChatConnection.pingIntervalId = window.setInterval(function () {
|
||||
sendPresencePing();
|
||||
}, PRESENCE_PING_INTERVAL_MS);
|
||||
}
|
||||
if (!sharedChatConnection.visibilityHandlerInstalled) {
|
||||
window.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
sharedChatConnection.visibilityHandlerInstalled = true;
|
||||
}
|
||||
if (!sharedChatConnection.pageShowHandlerInstalled) {
|
||||
window.addEventListener('pageshow', handlePageShow);
|
||||
sharedChatConnection.pageShowHandlerInstalled = true;
|
||||
}
|
||||
if (!sharedChatConnection.focusHandlerInstalled) {
|
||||
window.addEventListener('focus', handleWindowFocus);
|
||||
sharedChatConnection.focusHandlerInstalled = true;
|
||||
}
|
||||
if (!sharedChatConnection.onlineHandlerInstalled) {
|
||||
window.addEventListener('online', handleWindowOnline);
|
||||
sharedChatConnection.onlineHandlerInstalled = true;
|
||||
}
|
||||
}
|
||||
function scheduleReconnect() {
|
||||
if (sharedChatConnection.reconnectTimerId !== null || !sharedChatConnection.sessionId) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.reconnectTimerId = window.setTimeout(function () {
|
||||
sharedChatConnection.reconnectTimerId = null;
|
||||
connectSharedSocket();
|
||||
}, chatUtils_1.CHAT_CONNECTION.reconnectDelayMs);
|
||||
}
|
||||
function handleSharedDisconnect(message, detail) {
|
||||
setSharedConnectionError(detail !== null && detail !== void 0 ? detail : '');
|
||||
clearDisconnectUiTimer();
|
||||
if (sharedChatConnection.connectionState !== 'connected') {
|
||||
setSharedConnectionState('disconnected');
|
||||
}
|
||||
else {
|
||||
sharedChatConnection.disconnectUiTimerId = window.setTimeout(function () {
|
||||
var _a;
|
||||
sharedChatConnection.disconnectUiTimerId = null;
|
||||
if (((_a = sharedChatConnection.socketRef.current) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
setSharedConnectionState('disconnected');
|
||||
}, DISCONNECT_UI_DELAY_MS);
|
||||
}
|
||||
if (message) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
function disconnectSharedSocket() {
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
stopPresenceMonitoring();
|
||||
var socket = sharedChatConnection.socketRef.current;
|
||||
sharedChatConnection.suppressDisconnectNotification = true;
|
||||
sharedChatConnection.socketRef.current = null;
|
||||
socket === null || socket === void 0 ? void 0 : socket.close();
|
||||
}
|
||||
function releaseSharedConnectionConsumer() {
|
||||
sharedChatConnection.consumerCount = Math.max(0, sharedChatConnection.consumerCount - 1);
|
||||
if (sharedChatConnection.consumerCount > 0) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.currentContext = null;
|
||||
sharedChatConnection.setMessages = null;
|
||||
sharedChatConnection.onMessageEvent = undefined;
|
||||
sharedChatConnection.onJobEvent = undefined;
|
||||
sharedChatConnection.onRuntimeEvent = undefined;
|
||||
sharedChatConnection.onRuntimeDetailEvent = undefined;
|
||||
setSharedChatRuntimeSnapshot(null);
|
||||
disconnectSharedSocket();
|
||||
setSharedConnectionError('');
|
||||
setSharedConnectionState('disconnected');
|
||||
}
|
||||
function connectSharedSocket() {
|
||||
if (!sharedChatConnection.sessionId || !sharedChatConnection.setMessages) {
|
||||
return;
|
||||
}
|
||||
if (!(0, tokenAccess_1.hasRegisteredAccessTokenAccess)()) {
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
stopPresenceMonitoring();
|
||||
setSharedConnectionError('등록된 접근 토큰이 없어 채팅 연결을 시작하지 않았습니다.');
|
||||
setSharedConnectionState('disconnected');
|
||||
return;
|
||||
}
|
||||
var currentSocket = sharedChatConnection.socketRef.current;
|
||||
if (currentSocket && (currentSocket.readyState === WebSocket.OPEN || currentSocket.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
clearReconnectTimer();
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
if (sharedChatConnection.connectionState !== 'connected') {
|
||||
setSharedConnectionState('connecting');
|
||||
}
|
||||
sharedChatConnection.websocketUrl = (0, chatUtils_1.resolveChatWebSocketUrl)(sharedChatConnection.sessionId, sharedChatConnection.lastEventId);
|
||||
var socket;
|
||||
try {
|
||||
socket = new WebSocket(sharedChatConnection.websocketUrl);
|
||||
}
|
||||
catch (_a) {
|
||||
handleSharedDisconnect("\uC6CC\uD06C\uC11C\uBC84 WebSocket \uC8FC\uC18C\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC0C1: ".concat(sharedChatConnection.websocketUrl || '/ws/chat', " \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD569\uB2C8\uB2E4."), 'WebSocket 객체를 생성하지 못했습니다. 대상 주소 형식과 환경변수를 확인해 주세요.');
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.socketRef.current = socket;
|
||||
sharedChatConnection.suppressDisconnectNotification = false;
|
||||
var disconnectHandled = false;
|
||||
var reportDisconnect = function (message, closeEvent) {
|
||||
if (disconnectHandled) {
|
||||
return;
|
||||
}
|
||||
disconnectHandled = true;
|
||||
var wasSuppressed = sharedChatConnection.suppressDisconnectNotification;
|
||||
if (sharedChatConnection.socketRef.current === socket) {
|
||||
sharedChatConnection.socketRef.current = null;
|
||||
}
|
||||
sharedChatConnection.suppressDisconnectNotification = false;
|
||||
if (wasSuppressed) {
|
||||
setSharedConnectionError('');
|
||||
return;
|
||||
}
|
||||
if ((closeEvent === null || closeEvent === void 0 ? void 0 : closeEvent.code) === 1008) {
|
||||
clearReconnectTimer();
|
||||
setSharedConnectionError('등록된 접근 토큰이 없어 채팅 연결이 차단되었습니다.');
|
||||
setSharedConnectionState('disconnected');
|
||||
return;
|
||||
}
|
||||
if ((closeEvent === null || closeEvent === void 0 ? void 0 : closeEvent.code) === 1000 && !message) {
|
||||
setSharedConnectionError('');
|
||||
handleSharedDisconnect();
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
void (0, chatUtils_1.diagnoseConnectionFailure)(sharedChatConnection.websocketUrl, closeEvent).then(function (detail) {
|
||||
handleSharedDisconnect(message, detail);
|
||||
});
|
||||
};
|
||||
sharedChatConnection.connectTimeoutId = window.setTimeout(function () {
|
||||
if (sharedChatConnection.socketRef.current !== socket || socket.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
sharedChatConnection.socketRef.current = null;
|
||||
socket.close();
|
||||
reportDisconnect("\uC6CC\uD06C\uC11C\uBC84 \uC5F0\uACB0 \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uB300\uC0C1: ".concat(sharedChatConnection.websocketUrl || '/ws/chat', " \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC5F0\uACB0\uD569\uB2C8\uB2E4."));
|
||||
}, chatUtils_1.CHAT_CONNECTION.connectTimeoutMs);
|
||||
socket.addEventListener('open', function () {
|
||||
clearConnectTimeout();
|
||||
clearDisconnectUiTimer();
|
||||
sharedChatConnection.hasConnectedOnce = true;
|
||||
sharedChatConnection.suppressDisconnectNotification = false;
|
||||
setSharedConnectionState('connected');
|
||||
setSharedConnectionError('');
|
||||
sendContextUpdate(sharedChatConnection.currentContext);
|
||||
startPresenceMonitoring();
|
||||
});
|
||||
socket.addEventListener('message', function (event) {
|
||||
var _a, _b;
|
||||
var setMessages = sharedChatConnection.setMessages;
|
||||
if (!setMessages) {
|
||||
return;
|
||||
}
|
||||
void (0, chatUtils_1.handleChatServerEvent)({
|
||||
eventData: String(event.data),
|
||||
currentPageUrl: (_b = (_a = sharedChatConnection.currentContext) === null || _a === void 0 ? void 0 : _a.pageUrl) !== null && _b !== void 0 ? _b : '',
|
||||
expectedSessionId: sharedChatConnection.sessionId,
|
||||
setMessages: setMessages,
|
||||
onMessageEvent: sharedChatConnection.onMessageEvent,
|
||||
onJobEvent: sharedChatConnection.onJobEvent,
|
||||
onRuntimeEvent: sharedChatConnection.onRuntimeEvent,
|
||||
onRuntimeDetailEvent: sharedChatConnection.onRuntimeDetailEvent,
|
||||
onActivityEvent: sharedChatConnection.onActivityEvent,
|
||||
onEventReceived: function (eventId) {
|
||||
sharedChatConnection.lastEventId = eventId;
|
||||
(0, chatUtils_1.persistLastReceivedChatEventId)(sharedChatConnection.sessionId, eventId);
|
||||
sendEventReceived(eventId);
|
||||
},
|
||||
});
|
||||
try {
|
||||
var parsedEvent = JSON.parse(String(event.data));
|
||||
if ((parsedEvent === null || parsedEvent === void 0 ? void 0 : parsedEvent.type) === 'chat:runtime') {
|
||||
setSharedChatRuntimeSnapshot(parsedEvent.payload);
|
||||
}
|
||||
}
|
||||
catch (_c) {
|
||||
// ignore malformed payloads here; detailed parsing is already handled downstream
|
||||
}
|
||||
});
|
||||
socket.addEventListener('close', function (event) {
|
||||
clearConnectTimeout();
|
||||
stopPresenceMonitoring();
|
||||
reportDisconnect(event.code === 1000 ? undefined : '워크서버 연결이 끊어졌습니다. 자동으로 다시 연결합니다.', event);
|
||||
});
|
||||
socket.addEventListener('error', function () {
|
||||
clearConnectTimeout();
|
||||
stopPresenceMonitoring();
|
||||
reportDisconnect('워크서버 WebSocket 연결에 실패했습니다. 자동으로 다시 연결합니다.');
|
||||
});
|
||||
}
|
||||
function ensureSharedConnection(options) {
|
||||
var sessionChanged = sharedChatConnection.sessionId !== options.sessionId;
|
||||
sharedChatConnection.currentContext = options.currentContext;
|
||||
sharedChatConnection.setMessages = options.setMessages;
|
||||
sharedChatConnection.onMessageEvent = options.onMessageEvent;
|
||||
sharedChatConnection.onJobEvent = options.onJobEvent;
|
||||
sharedChatConnection.onRuntimeEvent = options.onRuntimeEvent;
|
||||
sharedChatConnection.onRuntimeDetailEvent = options.onRuntimeDetailEvent;
|
||||
sharedChatConnection.onActivityEvent = options.onActivityEvent;
|
||||
if (sessionChanged) {
|
||||
sharedChatConnection.sessionId = options.sessionId;
|
||||
sharedChatConnection.lastEventId = (0, chatUtils_1.getLastReceivedChatEventId)(options.sessionId);
|
||||
sharedChatConnection.hasConnectedOnce = false;
|
||||
disconnectSharedSocket();
|
||||
}
|
||||
connectSharedSocket();
|
||||
}
|
||||
function useChatConnection(_a) {
|
||||
var sessionId = _a.sessionId, currentContext = _a.currentContext, setMessages = _a.setMessages, onMessageEvent = _a.onMessageEvent, onJobEvent = _a.onJobEvent, onRuntimeEvent = _a.onRuntimeEvent, onRuntimeDetailEvent = _a.onRuntimeDetailEvent, onActivityEvent = _a.onActivityEvent;
|
||||
var _b = (0, react_1.useState)(function () { return getSnapshot(); }), snapshot = _b[0], setSnapshot = _b[1];
|
||||
(0, react_1.useEffect)(function () {
|
||||
sharedChatConnection.consumerCount += 1;
|
||||
return function () {
|
||||
releaseSharedConnectionConsumer();
|
||||
};
|
||||
}, []);
|
||||
(0, react_1.useEffect)(function () {
|
||||
var handleSnapshotChange = function () {
|
||||
setSnapshot(getSnapshot());
|
||||
};
|
||||
var unsubscribe = subscribeChatConnection(handleSnapshotChange);
|
||||
ensureSharedConnection({
|
||||
sessionId: sessionId,
|
||||
currentContext: currentContext,
|
||||
setMessages: setMessages,
|
||||
onMessageEvent: onMessageEvent,
|
||||
onJobEvent: onJobEvent,
|
||||
onRuntimeEvent: onRuntimeEvent,
|
||||
onRuntimeDetailEvent: onRuntimeDetailEvent,
|
||||
onActivityEvent: onActivityEvent,
|
||||
});
|
||||
handleSnapshotChange();
|
||||
return function () {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [sessionId, setMessages]);
|
||||
(0, react_1.useEffect)(function () {
|
||||
sharedChatConnection.currentContext = currentContext;
|
||||
sharedChatConnection.setMessages = setMessages;
|
||||
sharedChatConnection.onMessageEvent = onMessageEvent;
|
||||
sharedChatConnection.onJobEvent = onJobEvent;
|
||||
sharedChatConnection.onRuntimeEvent = onRuntimeEvent;
|
||||
sharedChatConnection.onRuntimeDetailEvent = onRuntimeDetailEvent;
|
||||
sharedChatConnection.onActivityEvent = onActivityEvent;
|
||||
sendContextUpdate(currentContext);
|
||||
}, [
|
||||
currentContext,
|
||||
onMessageEvent,
|
||||
onJobEvent,
|
||||
onRuntimeEvent,
|
||||
onRuntimeDetailEvent,
|
||||
onActivityEvent,
|
||||
setMessages,
|
||||
]);
|
||||
(0, react_1.useEffect)(function () {
|
||||
sharedChatConnection.pingSubscriberCount += 1;
|
||||
startPresenceMonitoring();
|
||||
return function () {
|
||||
sharedChatConnection.pingSubscriberCount = Math.max(0, sharedChatConnection.pingSubscriberCount - 1);
|
||||
if (sharedChatConnection.pingSubscriberCount === 0) {
|
||||
stopPresenceMonitoring();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
return {
|
||||
connectionState: snapshot.connectionState,
|
||||
connectionErrorDetail: snapshot.connectionErrorDetail,
|
||||
socketRef: sharedChatConnection.socketRef,
|
||||
};
|
||||
}
|
||||
@@ -483,6 +483,8 @@ function connectSharedSocket() {
|
||||
|
||||
if (closeEvent?.code === 1000 && !message) {
|
||||
setSharedConnectionError('');
|
||||
handleSharedDisconnect();
|
||||
scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
116
src/app/main/mainChatPanel/useErrorLogs.js
Normal file
116
src/app/main/mainChatPanel/useErrorLogs.js
Normal file
@@ -0,0 +1,116 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useErrorLogs = useErrorLogs;
|
||||
var react_1 = require("react");
|
||||
var errorLogApi_1 = require("../errorLogApi");
|
||||
var errorLogUtils_1 = require("./errorLogUtils");
|
||||
function useErrorLogs(_a) {
|
||||
var _this = this;
|
||||
var activeView = _a.activeView, hasAccess = _a.hasAccess;
|
||||
var _b = (0, react_1.useState)([]), errorLogs = _b[0], setErrorLogs = _b[1];
|
||||
var _c = (0, react_1.useState)(null), selectedErrorLogId = _c[0], setSelectedErrorLogId = _c[1];
|
||||
var _d = (0, react_1.useState)(false), isLoadingErrorLogs = _d[0], setIsLoadingErrorLogs = _d[1];
|
||||
var _e = (0, react_1.useState)(''), errorLogLoadError = _e[0], setErrorLogLoadError = _e[1];
|
||||
var _f = (0, react_1.useState)(''), activeErrorResourceUrl = _f[0], setActiveErrorResourceUrl = _f[1];
|
||||
var _g = (0, react_1.useState)(false), isErrorDetailExpanded = _g[0], setIsErrorDetailExpanded = _g[1];
|
||||
var loadErrorLogs = function () { return __awaiter(_this, void 0, void 0, function () {
|
||||
var items_1, error_1;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!hasAccess || isLoadingErrorLogs) {
|
||||
return [2 /*return*/];
|
||||
}
|
||||
setIsLoadingErrorLogs(true);
|
||||
setErrorLogLoadError('');
|
||||
_a.label = 1;
|
||||
case 1:
|
||||
_a.trys.push([1, 3, 4, 5]);
|
||||
return [4 /*yield*/, (0, errorLogApi_1.fetchErrorLogs)(50)];
|
||||
case 2:
|
||||
items_1 = _a.sent();
|
||||
setErrorLogs(items_1);
|
||||
setSelectedErrorLogId(function (current) { var _a, _b; return (_b = current !== null && current !== void 0 ? current : (_a = items_1[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null; });
|
||||
return [3 /*break*/, 5];
|
||||
case 3:
|
||||
error_1 = _a.sent();
|
||||
setErrorLogLoadError(error_1 instanceof Error ? error_1.message : '에러 로그를 불러오지 못했습니다.');
|
||||
return [3 /*break*/, 5];
|
||||
case 4:
|
||||
setIsLoadingErrorLogs(false);
|
||||
return [7 /*endfinally*/];
|
||||
case 5: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
(0, react_1.useEffect)(function () {
|
||||
if (activeView !== 'errors' || !hasAccess) {
|
||||
return;
|
||||
}
|
||||
void loadErrorLogs();
|
||||
}, [activeView, hasAccess]);
|
||||
var selectedErrorLog = (0, react_1.useMemo)(function () { var _a, _b; return (_b = (_a = errorLogs.find(function (item) { return item.id === selectedErrorLogId; })) !== null && _a !== void 0 ? _a : errorLogs[0]) !== null && _b !== void 0 ? _b : null; }, [errorLogs, selectedErrorLogId]);
|
||||
var selectedErrorLogReferenceSummary = (0, react_1.useMemo)(function () { return (selectedErrorLog ? (0, errorLogUtils_1.buildErrorReferenceSummary)(selectedErrorLog) : null); }, [selectedErrorLog]);
|
||||
var activeErrorResource = (0, react_1.useMemo)(function () {
|
||||
var _a, _b;
|
||||
return (_a = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources.find(function (resource) { return resource.url === activeErrorResourceUrl; })) !== null && _a !== void 0 ? _a : (0, errorLogUtils_1.getDefaultErrorResource)((_b = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources) !== null && _b !== void 0 ? _b : []);
|
||||
}, [activeErrorResourceUrl, selectedErrorLogReferenceSummary]);
|
||||
var errorSourceSummary = (0, react_1.useMemo)(function () { return (0, errorLogUtils_1.buildErrorSourceSummary)(errorLogs); }, [errorLogs]);
|
||||
(0, react_1.useEffect)(function () {
|
||||
var _a, _b, _c;
|
||||
var nextUrl = (_c = (_b = (0, errorLogUtils_1.getDefaultErrorResource)((_a = selectedErrorLogReferenceSummary === null || selectedErrorLogReferenceSummary === void 0 ? void 0 : selectedErrorLogReferenceSummary.resources) !== null && _a !== void 0 ? _a : [])) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : '';
|
||||
setActiveErrorResourceUrl(nextUrl);
|
||||
}, [selectedErrorLog === null || selectedErrorLog === void 0 ? void 0 : selectedErrorLog.id, selectedErrorLogReferenceSummary]);
|
||||
return {
|
||||
errorLogs: errorLogs,
|
||||
selectedErrorLog: selectedErrorLog,
|
||||
selectedErrorLogId: selectedErrorLogId,
|
||||
selectedErrorLogReferenceSummary: selectedErrorLogReferenceSummary,
|
||||
activeErrorResource: activeErrorResource,
|
||||
errorSourceSummary: errorSourceSummary,
|
||||
isLoadingErrorLogs: isLoadingErrorLogs,
|
||||
errorLogLoadError: errorLogLoadError,
|
||||
activeErrorResourceUrl: activeErrorResourceUrl,
|
||||
isErrorDetailExpanded: isErrorDetailExpanded,
|
||||
setSelectedErrorLogId: setSelectedErrorLogId,
|
||||
setActiveErrorResourceUrl: setActiveErrorResourceUrl,
|
||||
setIsErrorDetailExpanded: setIsErrorDetailExpanded,
|
||||
loadErrorLogs: loadErrorLogs,
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export type MainViewInitialNavigation = {
|
||||
export function resolveInitialNavigation(): MainViewInitialNavigation {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
activeTopMenu: 'plans',
|
||||
activeTopMenu: 'chat',
|
||||
selectedPlanMenu: 'all',
|
||||
selectedPlayMenu: 'layout',
|
||||
initialSelectedPlanId: null,
|
||||
@@ -26,7 +26,7 @@ export function resolveInitialNavigation(): MainViewInitialNavigation {
|
||||
const requestedTopMenu: TopMenuKey =
|
||||
topMenuParam === 'docs' || topMenuParam === 'apis' || topMenuParam === 'plans' || topMenuParam === 'chat' || topMenuParam === 'play'
|
||||
? topMenuParam
|
||||
: 'plans';
|
||||
: 'chat';
|
||||
const planFilterParam = params.get('planFilter');
|
||||
const selectedPlanMenu =
|
||||
planFilterParam === 'release'
|
||||
@@ -45,6 +45,10 @@ export function resolveInitialNavigation(): MainViewInitialNavigation {
|
||||
selectedPlayMenu:
|
||||
playSectionParam === 'layout'
|
||||
? 'layout'
|
||||
: playSectionParam === 'test'
|
||||
? 'test'
|
||||
: playSectionParam === 'cbt'
|
||||
? 'cbt'
|
||||
: playSectionParam === 'layout-record' && playLayoutIdParam
|
||||
? resolveSavedLayoutMenuKey(playLayoutIdParam)
|
||||
: 'layout',
|
||||
|
||||
@@ -153,6 +153,18 @@ export function buildMainViewSearchOptions({
|
||||
},
|
||||
onSelectWindow,
|
||||
},
|
||||
{
|
||||
id: 'page:chat:resources',
|
||||
label: 'Codex Live / 리소스 관리',
|
||||
group: 'Page',
|
||||
keywords: ['codex live', 'resource', 'resources', 'file', 'files', '리소스', '파일', '파일 시스템'],
|
||||
onSelect: () => {
|
||||
setActiveTopMenu('chat');
|
||||
setSelectedChatMenu('resources');
|
||||
setFocusedComponentId(null);
|
||||
},
|
||||
onSelectWindow,
|
||||
},
|
||||
{
|
||||
id: 'page:chat:errors',
|
||||
label: '앱로그 / 에러 로그',
|
||||
@@ -178,6 +190,18 @@ export function buildMainViewSearchOptions({
|
||||
setFocusedComponentId(null);
|
||||
},
|
||||
onSelectWindow,
|
||||
},
|
||||
{
|
||||
id: 'page:chat:manage-defaults',
|
||||
label: '채팅 관리 / 기본 유형 관리',
|
||||
group: 'Page',
|
||||
keywords: ['chat manage', 'default type', 'default context', '기본 유형', '기본 context', '채팅 관리'],
|
||||
onSelect: () => {
|
||||
setActiveTopMenu('chat');
|
||||
setSelectedChatMenu('manage-defaults');
|
||||
setFocusedComponentId(null);
|
||||
},
|
||||
onSelectWindow,
|
||||
} satisfies SearchKeywordOption,
|
||||
]
|
||||
: []),
|
||||
|
||||
83
src/app/main/notificationIdentity.js
Normal file
83
src/app/main/notificationIdentity.js
Normal file
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PWA_NOTIFICATION_TOKEN_STORAGE_KEY = exports.NOTIFICATION_DEVICE_ID_STORAGE_KEY = void 0;
|
||||
exports.buildScopedPwaNotificationTargetId = buildScopedPwaNotificationTargetId;
|
||||
exports.getSavedNotificationDeviceId = getSavedNotificationDeviceId;
|
||||
exports.getSavedPwaNotificationToken = getSavedPwaNotificationToken;
|
||||
exports.setSavedPwaNotificationToken = setSavedPwaNotificationToken;
|
||||
exports.clearNotificationIdentity = clearNotificationIdentity;
|
||||
exports.getAutomationNotificationPreferenceTarget = getAutomationNotificationPreferenceTarget;
|
||||
var clientIdentity_1 = require("./clientIdentity");
|
||||
exports.NOTIFICATION_DEVICE_ID_STORAGE_KEY = 'work-server.notification.device-id';
|
||||
exports.PWA_NOTIFICATION_TOKEN_STORAGE_KEY = 'work-server.notification.pwa-token';
|
||||
function buildScopedPwaNotificationTargetId(token, clientId) {
|
||||
return [token.trim(), clientId.trim()].filter(Boolean).join('::client::');
|
||||
}
|
||||
function getSavedNotificationDeviceId() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
var clientId = (0, clientIdentity_1.getOrCreateClientId)();
|
||||
if (clientId) {
|
||||
window.localStorage.setItem(exports.NOTIFICATION_DEVICE_ID_STORAGE_KEY, clientId);
|
||||
return clientId;
|
||||
}
|
||||
var saved = window.localStorage.getItem(exports.NOTIFICATION_DEVICE_ID_STORAGE_KEY);
|
||||
if (saved) {
|
||||
return saved;
|
||||
}
|
||||
var generated = "web-".concat(Date.now());
|
||||
window.localStorage.setItem(exports.NOTIFICATION_DEVICE_ID_STORAGE_KEY, generated);
|
||||
return generated;
|
||||
}
|
||||
function getSavedPwaNotificationToken() {
|
||||
var _a, _b;
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return (_b = (_a = window.localStorage.getItem(exports.PWA_NOTIFICATION_TOKEN_STORAGE_KEY)) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
|
||||
}
|
||||
function setSavedPwaNotificationToken(token) {
|
||||
var _a;
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
var normalizedToken = (_a = token === null || token === void 0 ? void 0 : token.trim()) !== null && _a !== void 0 ? _a : '';
|
||||
if (normalizedToken) {
|
||||
window.localStorage.setItem(exports.PWA_NOTIFICATION_TOKEN_STORAGE_KEY, normalizedToken);
|
||||
}
|
||||
else {
|
||||
window.localStorage.removeItem(exports.PWA_NOTIFICATION_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
function clearNotificationIdentity() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(exports.NOTIFICATION_DEVICE_ID_STORAGE_KEY);
|
||||
window.localStorage.removeItem(exports.PWA_NOTIFICATION_TOKEN_STORAGE_KEY);
|
||||
(0, clientIdentity_1.clearClientId)();
|
||||
}
|
||||
function getAutomationNotificationPreferenceTarget() {
|
||||
var pwaToken = getSavedPwaNotificationToken();
|
||||
var clientId = getSavedNotificationDeviceId();
|
||||
if (pwaToken && clientId) {
|
||||
return {
|
||||
targetKind: 'ios-token-client',
|
||||
targetId: buildScopedPwaNotificationTargetId(pwaToken, clientId),
|
||||
};
|
||||
}
|
||||
if (pwaToken) {
|
||||
return {
|
||||
targetKind: 'ios-token',
|
||||
targetId: pwaToken,
|
||||
};
|
||||
}
|
||||
if (!clientId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
targetKind: 'client',
|
||||
targetId: clientId,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ChatDefaultContextManagementPage } from '../ChatDefaultContextManagementPage';
|
||||
import { ResourceManagementPage } from '../ResourceManagementPage';
|
||||
import { ChatTypeManagementPage } from '../ChatTypeManagementPage';
|
||||
import { MainChatPanel } from '../MainChatPanel';
|
||||
import { ChatSourceChangesPage } from '../ChatSourceChangesPage';
|
||||
@@ -10,6 +12,10 @@ export function ChatPage() {
|
||||
<div className="app-main-panel">
|
||||
{selectedChatMenu === 'manage' ? (
|
||||
<ChatTypeManagementPage />
|
||||
) : selectedChatMenu === 'manage-defaults' ? (
|
||||
<ChatDefaultContextManagementPage />
|
||||
) : selectedChatMenu === 'resources' ? (
|
||||
<ResourceManagementPage />
|
||||
) : selectedChatMenu === 'changes' ? (
|
||||
<ChatSourceChangesPage />
|
||||
) : (
|
||||
|
||||
@@ -9,20 +9,22 @@ export function DocsPage() {
|
||||
const { selectedDocsMenu, selectedDocs } = useMainLayoutContext();
|
||||
|
||||
return (
|
||||
<div className="app-main-panel">
|
||||
<div className="app-main-panel docs-page">
|
||||
<Card
|
||||
title={`Docs / ${getDocsSectionLabel(selectedDocsMenu)}`}
|
||||
extra={<Text code>{selectedDocs.length} docs</Text>}
|
||||
className="app-main-card"
|
||||
className="app-main-card docs-page__card"
|
||||
bordered={false}
|
||||
>
|
||||
<Space direction="vertical" size={16} className="app-main-stack">
|
||||
{selectedDocs.map((document) => (
|
||||
<div key={document.id} id={`document-preview-${document.id}`} data-focus-id={`doc:${document.id}`}>
|
||||
<MarkdownPreviewCard document={document} />
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<div className="docs-page__scroll">
|
||||
<Space direction="vertical" size={16} className="app-main-stack docs-page__stack">
|
||||
{selectedDocs.map((document) => (
|
||||
<div key={document.id} id={`document-preview-${document.id}`} data-focus-id={`doc:${document.id}`}>
|
||||
<MarkdownPreviewCard document={document} />
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { LayoutPlaygroundView } from '../../../views/play/LayoutPlaygroundView';
|
||||
import { CbtPlayAppView } from '../../../views/play/apps/cbt/CbtPlayAppView';
|
||||
import { TestPlayAppView } from '../../../views/play/apps/test/TestPlayAppView';
|
||||
import { FeatureMenuLayoutPage } from '../../../features/layout/feature-menu';
|
||||
import { MemoLayoutPage } from '../../../features/layout/memo';
|
||||
import { useMainLayoutContext } from '../layout/MainLayoutContext';
|
||||
import { resolveSavedLayoutIdFromMenuKey } from '../routes';
|
||||
@@ -10,13 +13,23 @@ export function PlayPage() {
|
||||
? savedLayouts.find((layout) => layout.id === selectedSavedLayoutId) ?? null
|
||||
: null;
|
||||
const isMemoLayout = selectedSavedLayout?.name === '메모';
|
||||
const isFeatureMenuLayout = selectedSavedLayout?.name === '기능설명 관리';
|
||||
const panelClassName = selectedSavedLayoutId ? 'app-main-panel app-main-panel--play app-main-panel--play-saved' : 'app-main-panel app-main-panel--play';
|
||||
|
||||
return (
|
||||
<div className={panelClassName}>
|
||||
{selectedPlayMenu === 'layout' ? <LayoutPlaygroundView onSavedLayoutsChange={setSavedLayouts} /> : null}
|
||||
{selectedPlayMenu === 'test' ? <TestPlayAppView /> : null}
|
||||
{selectedPlayMenu === 'cbt' ? <CbtPlayAppView /> : null}
|
||||
{selectedSavedLayoutId && isMemoLayout ? <MemoLayoutPage layoutId={selectedSavedLayoutId} /> : null}
|
||||
{selectedSavedLayoutId && !isMemoLayout ? (
|
||||
{selectedSavedLayoutId && isFeatureMenuLayout ? (
|
||||
<FeatureMenuLayoutPage
|
||||
layoutId={selectedSavedLayoutId}
|
||||
savedLayouts={savedLayouts}
|
||||
onSavedLayoutsChange={setSavedLayouts}
|
||||
/>
|
||||
) : null}
|
||||
{selectedSavedLayoutId && !isMemoLayout && !isFeatureMenuLayout ? (
|
||||
<LayoutPlaygroundView savedLayoutViewId={selectedSavedLayoutId} onSavedLayoutsChange={setSavedLayouts} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
277
src/app/main/resourceManagerApi.ts
Normal file
277
src/app/main/resourceManagerApi.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { appendClientIdHeader } from './clientIdentity';
|
||||
import { getRegisteredAccessToken, isAllowedRegistrationToken } from './tokenAccess';
|
||||
|
||||
export type ResourceManagerEntryType = 'file' | 'directory';
|
||||
|
||||
export type ResourceManagerTreeNode = {
|
||||
name: string;
|
||||
path: string;
|
||||
type: ResourceManagerEntryType;
|
||||
extension: string | null;
|
||||
size: number | null;
|
||||
modifiedAt: string;
|
||||
previewUrl: string | null;
|
||||
children?: ResourceManagerTreeNode[];
|
||||
};
|
||||
|
||||
export type ResourceManagerTreeRoot = {
|
||||
label: string;
|
||||
rootPath: string;
|
||||
tree: ResourceManagerTreeNode;
|
||||
};
|
||||
|
||||
export type ResourceManagerDirectoryEntry = {
|
||||
name: string;
|
||||
path: string;
|
||||
type: ResourceManagerEntryType;
|
||||
extension: string | null;
|
||||
size: number | null;
|
||||
modifiedAt: string;
|
||||
previewUrl: string | null;
|
||||
};
|
||||
|
||||
export type ResourceManagerDirectoryDetail = {
|
||||
path: string;
|
||||
items: ResourceManagerDirectoryEntry[];
|
||||
};
|
||||
|
||||
export type ResourceManagerFileDetail = {
|
||||
name: string;
|
||||
path: string;
|
||||
extension: string | null;
|
||||
size: number;
|
||||
modifiedAt: string;
|
||||
mimeType: string;
|
||||
previewUrl: string;
|
||||
isTextEditable: boolean;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
class ResourceManagerApiError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = 'ResourceManagerApiError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveApiBaseUrl() {
|
||||
if (import.meta.env.VITE_WORK_SERVER_URL) {
|
||||
return import.meta.env.VITE_WORK_SERVER_URL;
|
||||
}
|
||||
|
||||
return '/api';
|
||||
}
|
||||
|
||||
function resolveFallbackBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
const isLocalHost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';
|
||||
|
||||
if (!isLocalHost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallbackUrl = new URL(window.location.origin);
|
||||
fallbackUrl.port = '3100';
|
||||
fallbackUrl.pathname = '/api';
|
||||
fallbackUrl.search = '';
|
||||
fallbackUrl.hash = '';
|
||||
return fallbackUrl.toString().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
const RESOURCE_MANAGER_API_BASE_URL = resolveApiBaseUrl();
|
||||
const RESOURCE_MANAGER_API_FALLBACK_BASE_URL =
|
||||
!import.meta.env.VITE_WORK_SERVER_URL && RESOURCE_MANAGER_API_BASE_URL === '/api'
|
||||
? resolveFallbackBaseUrl()
|
||||
: null;
|
||||
|
||||
function appendPreviewAccessToken(previewUrl: string | null) {
|
||||
if (!previewUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = getRegisteredAccessToken();
|
||||
|
||||
if (!token) {
|
||||
return previewUrl;
|
||||
}
|
||||
|
||||
const separator = previewUrl.includes('?') ? '&' : '?';
|
||||
return `${previewUrl}${separator}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
function normalizeTreeNode(node: ResourceManagerTreeNode): ResourceManagerTreeNode {
|
||||
return {
|
||||
...node,
|
||||
previewUrl: appendPreviewAccessToken(node.previewUrl),
|
||||
children: node.children?.map((child) => normalizeTreeNode(child)),
|
||||
};
|
||||
}
|
||||
|
||||
async function requestOnce<T>(baseUrl: string, path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = appendClientIdHeader(init?.headers);
|
||||
const hasBody = init?.body !== undefined && init.body !== null;
|
||||
const method = init?.method?.toUpperCase() ?? 'GET';
|
||||
const token = getRegisteredAccessToken();
|
||||
|
||||
if (!isAllowedRegistrationToken(token)) {
|
||||
throw new ResourceManagerApiError('권한 토큰 등록 후에만 리소스 관리를 사용할 수 있습니다.', 403);
|
||||
}
|
||||
|
||||
if (hasBody && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
if (token && !headers.has('X-Access-Token')) {
|
||||
headers.set('X-Access-Token', token);
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(text) as { message?: string };
|
||||
throw new ResourceManagerApiError(payload.message || '리소스 관리 요청에 실패했습니다.', response.status);
|
||||
} catch {
|
||||
throw new ResourceManagerApiError(text || '리소스 관리 요청에 실패했습니다.', response.status);
|
||||
}
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
try {
|
||||
return await requestOnce<T>(RESOURCE_MANAGER_API_BASE_URL, path, init);
|
||||
} catch (error) {
|
||||
const shouldRetryWithFallback =
|
||||
RESOURCE_MANAGER_API_FALLBACK_BASE_URL &&
|
||||
RESOURCE_MANAGER_API_FALLBACK_BASE_URL !== RESOURCE_MANAGER_API_BASE_URL &&
|
||||
(error instanceof ResourceManagerApiError
|
||||
? error.status === 404 || error.status === 408 || error.status === 502
|
||||
: error instanceof Error && /404|Failed to fetch|NetworkError/i.test(error.message));
|
||||
|
||||
if (!shouldRetryWithFallback) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return requestOnce<T>(RESOURCE_MANAGER_API_FALLBACK_BASE_URL, path, init);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchResourceManagerTree() {
|
||||
const response = await request<{ ok: boolean; item: ResourceManagerTreeRoot }>('/resource-manager/tree');
|
||||
return {
|
||||
...response.item,
|
||||
tree: normalizeTreeNode(response.item.tree),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchResourceManagerDirectory(targetPath = '') {
|
||||
const query = new URLSearchParams({ path: targetPath });
|
||||
const response = await request<{ ok: boolean; item: ResourceManagerDirectoryDetail }>(
|
||||
`/resource-manager/directory?${query.toString()}`,
|
||||
);
|
||||
return {
|
||||
...response.item,
|
||||
items: response.item.items.map((item) => ({
|
||||
...item,
|
||||
previewUrl: appendPreviewAccessToken(item.previewUrl),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchResourceManagerFile(targetPath: string) {
|
||||
const query = new URLSearchParams({ path: targetPath });
|
||||
const response = await request<{ ok: boolean; item: ResourceManagerFileDetail }>(`/resource-manager/file?${query.toString()}`);
|
||||
return {
|
||||
...response.item,
|
||||
previewUrl: appendPreviewAccessToken(response.item.previewUrl) ?? response.item.previewUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createResourceManagerDirectory(parentPath: string, name: string) {
|
||||
await request('/resource-manager/directories', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ parentPath, name }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createResourceManagerFile(parentPath: string, name: string, content = '') {
|
||||
await request('/resource-manager/files', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ parentPath, name, content }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveResourceManagerFile(targetPath: string, content: string) {
|
||||
await request('/resource-manager/files/content', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ path: targetPath, content }),
|
||||
});
|
||||
}
|
||||
|
||||
async function readFileAsBase64(file: File) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
|
||||
bytes.forEach((byte) => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export async function uploadResourceManagerFile(parentPath: string, file: File) {
|
||||
const contentBase64 = await readFileAsBase64(file);
|
||||
await request('/resource-manager/files/upload', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
parentPath,
|
||||
fileName: file.name,
|
||||
contentBase64,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function copyResourceManagerItem(targetPath: string, targetDirectoryPath: string, nextName?: string) {
|
||||
await request('/resource-manager/items/copy', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
path: targetPath,
|
||||
targetDirectoryPath,
|
||||
nextName: nextName?.trim() || null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function moveResourceManagerItem(targetPath: string, targetDirectoryPath: string, nextName?: string) {
|
||||
await request('/resource-manager/items/move', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
path: targetPath,
|
||||
targetDirectoryPath,
|
||||
nextName: nextName?.trim() || null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteResourceManagerItem(targetPath: string) {
|
||||
const query = new URLSearchParams({ path: targetPath });
|
||||
await request(`/resource-manager/items?${query.toString()}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
@@ -18,8 +18,8 @@ export type PlanSectionKey =
|
||||
| 'automation-type'
|
||||
| 'automation-context'
|
||||
| 'server-command';
|
||||
export type ChatSectionKey = 'live' | 'changes' | 'errors' | 'manage';
|
||||
export type PlaySectionKey = 'layout';
|
||||
export type ChatSectionKey = 'live' | 'changes' | 'resources' | 'errors' | 'manage' | 'manage-defaults';
|
||||
export type PlaySectionKey = 'layout' | 'test' | 'cbt';
|
||||
export type PlaySidebarKey = PlaySectionKey | `layout-record:${string}`;
|
||||
|
||||
export const DOCS_DEFAULT_FOLDER = 'worklogs';
|
||||
@@ -56,6 +56,8 @@ export const PLAN_SIDEBAR_LABELS: Record<PlanSectionKey, string> = {
|
||||
|
||||
export const PLAY_SIDEBAR_LABELS: Record<PlaySectionKey, string> = {
|
||||
layout: 'Layout Editor',
|
||||
test: 'Test App',
|
||||
cbt: 'CBT',
|
||||
};
|
||||
|
||||
export const PLAN_MENU_ANCHOR_IDS: Partial<Record<PlanSectionKey, string>> = {
|
||||
@@ -89,7 +91,7 @@ export function resolveSavedLayoutIdFromMenuKey(key: PlaySidebarKey) {
|
||||
export function resolvePlaySidebarLabel(selectedPlayMenu: PlaySidebarKey, savedLayouts: Array<{ id: string; name: string }>) {
|
||||
const savedLayoutId = resolveSavedLayoutIdFromMenuKey(selectedPlayMenu);
|
||||
|
||||
if (selectedPlayMenu === 'layout') {
|
||||
if (selectedPlayMenu === 'layout' || selectedPlayMenu === 'test' || selectedPlayMenu === 'cbt') {
|
||||
return PLAY_SIDEBAR_LABELS[selectedPlayMenu];
|
||||
}
|
||||
|
||||
@@ -247,6 +249,7 @@ export function buildChatMenuItems(hasAccess = true, unreadCount = 0): MenuProps
|
||||
children: [
|
||||
{ key: 'live', label: renderChatUnreadLabel('Codex Live', unreadCount) },
|
||||
{ key: 'changes', label: '변경 이력' },
|
||||
{ key: 'resources', label: '리소스 관리' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -261,7 +264,10 @@ export function buildChatMenuItems(hasAccess = true, unreadCount = 0): MenuProps
|
||||
key: 'chat-manage-group',
|
||||
icon: <MessageOutlined />,
|
||||
label: '채팅 관리',
|
||||
children: [{ key: 'manage', label: '유형 권한 관리' }],
|
||||
children: [
|
||||
{ key: 'manage', label: '유형 권한 관리' },
|
||||
{ key: 'manage-defaults', label: '기본 유형 관리' },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -288,6 +294,18 @@ export function buildPlayMenuItems(savedLayouts: Array<{ id: string; name: strin
|
||||
: [{ key: 'saved-layout-empty', label: '저장된 레이아웃 없음', disabled: true }]),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'play-apps-group',
|
||||
label: 'Apps',
|
||||
children: [
|
||||
{ key: 'test', label: 'Test App' },
|
||||
{
|
||||
key: 'play-apps-general-group',
|
||||
label: '일반',
|
||||
children: [{ key: 'cbt', label: 'CBT' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -298,7 +316,7 @@ export function resolvePlanOpenKeys() {
|
||||
}
|
||||
|
||||
export function resolvePlayOpenKeys() {
|
||||
return ['play-group', 'play-layout-group'];
|
||||
return ['play-group', 'play-layout-group', 'play-apps-group', 'play-apps-general-group'];
|
||||
}
|
||||
|
||||
export function resolvePlanQuickFilterMenu(filter: 'working' | 'release-pending-main' | 'automation-failed') {
|
||||
@@ -364,16 +382,22 @@ export function resolveCurrentPageDescriptor(params: {
|
||||
}
|
||||
|
||||
if (topMenu === 'chat') {
|
||||
const title =
|
||||
chatMenu === 'errors'
|
||||
? '앱로그 / 에러 로그'
|
||||
: chatMenu === 'changes'
|
||||
? 'Codex Live / 변경 이력'
|
||||
: chatMenu === 'resources'
|
||||
? 'Codex Live / 리소스 관리'
|
||||
: chatMenu === 'manage'
|
||||
? '채팅 관리 / 유형 권한 관리'
|
||||
: chatMenu === 'manage-defaults'
|
||||
? '채팅 관리 / 기본 유형 관리'
|
||||
: 'Codex Live / Codex Live';
|
||||
|
||||
return {
|
||||
id: `app-log:${chatMenu}`,
|
||||
title:
|
||||
chatMenu === 'errors'
|
||||
? '앱로그 / 에러 로그'
|
||||
: chatMenu === 'changes'
|
||||
? 'Codex Live / 변경 이력'
|
||||
: chatMenu === 'manage'
|
||||
? '채팅 관리 / 유형 권한 관리'
|
||||
: 'Codex Live / Codex Live',
|
||||
title,
|
||||
topMenu,
|
||||
section: chatMenu,
|
||||
};
|
||||
@@ -396,7 +420,7 @@ export function resolveTopMenuPath(menu: HeaderTopMenuKey, currentDocsFolder: st
|
||||
return buildPlayPath('layout');
|
||||
}
|
||||
|
||||
return buildPlansPath('all');
|
||||
return buildChatPath('live');
|
||||
}
|
||||
|
||||
export function createPageWindowId(topMenu: TopMenuKey, section: string) {
|
||||
|
||||
63
src/app/main/tokenAccess.js
Normal file
63
src/app/main/tokenAccess.js
Normal file
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
var _a;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ALLOWED_REGISTRATION_TOKEN = exports.TOKEN_ACCESS_SYNC_EVENT = exports.TOKEN_ACCESS_STORAGE_KEY = void 0;
|
||||
exports.isAllowedRegistrationToken = isAllowedRegistrationToken;
|
||||
exports.getRegisteredAccessToken = getRegisteredAccessToken;
|
||||
exports.hasRegisteredAccessTokenAccess = hasRegisteredAccessTokenAccess;
|
||||
exports.setRegisteredAccessToken = setRegisteredAccessToken;
|
||||
exports.useTokenAccess = useTokenAccess;
|
||||
var react_1 = require("react");
|
||||
exports.TOKEN_ACCESS_STORAGE_KEY = 'work-app.token-access.registered-token';
|
||||
exports.TOKEN_ACCESS_SYNC_EVENT = 'work-app:token-access-changed';
|
||||
exports.ALLOWED_REGISTRATION_TOKEN = ((_a = import.meta.env.VITE_ALLOWED_REGISTRATION_TOKEN) === null || _a === void 0 ? void 0 : _a.trim()) || 'usr_7f3a9c2d8e1b4a6f';
|
||||
function normalizeToken(value) {
|
||||
var _a;
|
||||
return (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
|
||||
}
|
||||
function isAllowedRegistrationToken(token) {
|
||||
return normalizeToken(token) === exports.ALLOWED_REGISTRATION_TOKEN;
|
||||
}
|
||||
function getRegisteredAccessToken() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return normalizeToken(window.localStorage.getItem(exports.TOKEN_ACCESS_STORAGE_KEY));
|
||||
}
|
||||
function hasRegisteredAccessTokenAccess() {
|
||||
return isAllowedRegistrationToken(getRegisteredAccessToken());
|
||||
}
|
||||
function setRegisteredAccessToken(token) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
var normalizedToken = normalizeToken(token);
|
||||
if (normalizedToken) {
|
||||
window.localStorage.setItem(exports.TOKEN_ACCESS_STORAGE_KEY, normalizedToken);
|
||||
}
|
||||
else {
|
||||
window.localStorage.removeItem(exports.TOKEN_ACCESS_STORAGE_KEY);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(exports.TOKEN_ACCESS_SYNC_EVENT));
|
||||
}
|
||||
function useTokenAccess() {
|
||||
var _a = (0, react_1.useState)(function () { return getRegisteredAccessToken(); }), registeredToken = _a[0], setRegisteredToken = _a[1];
|
||||
(0, react_1.useEffect)(function () {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
var syncRegisteredToken = function () {
|
||||
setRegisteredToken(getRegisteredAccessToken());
|
||||
};
|
||||
window.addEventListener('storage', syncRegisteredToken);
|
||||
window.addEventListener(exports.TOKEN_ACCESS_SYNC_EVENT, syncRegisteredToken);
|
||||
return function () {
|
||||
window.removeEventListener('storage', syncRegisteredToken);
|
||||
window.removeEventListener(exports.TOKEN_ACCESS_SYNC_EVENT, syncRegisteredToken);
|
||||
};
|
||||
}, []);
|
||||
return {
|
||||
registeredToken: registeredToken,
|
||||
hasAccess: isAllowedRegistrationToken(registeredToken),
|
||||
};
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export type MainSidebarProps = {
|
||||
|
||||
export type MainContentProps = {
|
||||
contentExpanded: boolean;
|
||||
sidebarOverlayActive?: boolean;
|
||||
onToggleContentExpanded: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
87
src/app/main/viewportCssVars.ts
Normal file
87
src/app/main/viewportCssVars.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
const APP_VIEWPORT_HEIGHT_VAR = '--app-viewport-height';
|
||||
const APP_VIEWPORT_WIDTH_VAR = '--app-viewport-width';
|
||||
const APP_VISUAL_VIEWPORT_HEIGHT_VAR = '--app-visual-viewport-height';
|
||||
const APP_VISUAL_VIEWPORT_WIDTH_VAR = '--app-visual-viewport-width';
|
||||
|
||||
function roundViewportSize(value: number) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function getLayoutViewportSize() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const documentElement = document.documentElement;
|
||||
const fallbackHeight = documentElement?.clientHeight ?? 0;
|
||||
const fallbackWidth = documentElement?.clientWidth ?? 0;
|
||||
|
||||
return {
|
||||
height: roundViewportSize(Math.max(window.innerHeight || 0, fallbackHeight)),
|
||||
width: roundViewportSize(Math.max(window.innerWidth || 0, fallbackWidth)),
|
||||
};
|
||||
}
|
||||
|
||||
function getVisualViewportSize() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
return {
|
||||
height: roundViewportSize(viewport?.height ?? window.innerHeight ?? 0),
|
||||
width: roundViewportSize(viewport?.width ?? window.innerWidth ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
export function applyViewportCssVars() {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const layoutViewport = getLayoutViewportSize();
|
||||
const visualViewport = getVisualViewportSize();
|
||||
const resolvedHeight = Math.max(layoutViewport.height, visualViewport.height);
|
||||
|
||||
document.documentElement.style.setProperty(APP_VIEWPORT_HEIGHT_VAR, `${resolvedHeight}px`);
|
||||
document.documentElement.style.setProperty(APP_VIEWPORT_WIDTH_VAR, `${layoutViewport.width}px`);
|
||||
document.documentElement.style.setProperty(APP_VISUAL_VIEWPORT_HEIGHT_VAR, `${visualViewport.height}px`);
|
||||
document.documentElement.style.setProperty(APP_VISUAL_VIEWPORT_WIDTH_VAR, `${visualViewport.width}px`);
|
||||
}
|
||||
|
||||
export function bindViewportCssVars() {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
let frameId = 0;
|
||||
const viewport = window.visualViewport;
|
||||
|
||||
const sync = () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
frameId = window.requestAnimationFrame(() => {
|
||||
applyViewportCssVars();
|
||||
});
|
||||
};
|
||||
|
||||
sync();
|
||||
window.addEventListener('resize', sync);
|
||||
window.addEventListener('orientationchange', sync);
|
||||
viewport?.addEventListener('resize', sync);
|
||||
viewport?.addEventListener('scroll', sync);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
window.removeEventListener('resize', sync);
|
||||
window.removeEventListener('orientationchange', sync);
|
||||
viewport?.removeEventListener('resize', sync);
|
||||
viewport?.removeEventListener('scroll', sync);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user