chore: test deploy snapshot
This commit is contained in:
@@ -54,6 +54,8 @@ const envSchema = z.object({
|
||||
WEB_PUSH_VAPID_PUBLIC_KEY: z.string().optional(),
|
||||
WEB_PUSH_VAPID_PRIVATE_KEY: z.string().optional(),
|
||||
WEB_PUSH_SUBJECT: z.string().default('mailto:how2ice@naver.com'),
|
||||
OPENAI_API_KEY: z.string().optional(),
|
||||
OPENAI_ORGANIZATION_ID: z.string().optional(),
|
||||
APNS_KEY_ID: z.string().optional(),
|
||||
APNS_TEAM_ID: z.string().optional(),
|
||||
APNS_BUNDLE_ID: z.string().optional(),
|
||||
|
||||
@@ -2855,6 +2855,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
mode: z.enum(['queue', 'direct']).optional(),
|
||||
parentRequestId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||
sessionId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||
codexModel: z.string().trim().max(120).optional().nullable(),
|
||||
}).parse(request.body ?? {});
|
||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||
@@ -2951,6 +2952,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
requestOrigin: 'composer',
|
||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||
parentRequestId: resolvedParentRequestId,
|
||||
contextOverride: Object.prototype.hasOwnProperty.call(payload, 'codexModel')
|
||||
? { codexModel: payload.codexModel ?? null }
|
||||
: undefined,
|
||||
clientId: shareSnapshot.targetRequest.requesterClientId ?? shareSnapshot.conversation?.clientId ?? null,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
resolveChatContextAppOrigin,
|
||||
resolveChatContextAppDomain,
|
||||
rewriteCodexOutputWithChatResources,
|
||||
sanitizeChatContextOverride,
|
||||
summarizeActivityProgressLine,
|
||||
shouldSendOfflineChatNotification,
|
||||
shouldUseAgenticCodexReply,
|
||||
@@ -127,6 +128,29 @@ test('filterInactiveOfflineNotificationClientIds excludes only actively viewing
|
||||
);
|
||||
});
|
||||
|
||||
test('sanitizeChatContextOverride drops undefined codexModel without touching explicit null', () => {
|
||||
assert.deepEqual(
|
||||
sanitizeChatContextOverride({
|
||||
codexModel: undefined,
|
||||
chatTypeId: 'general-request',
|
||||
} as any),
|
||||
{
|
||||
chatTypeId: 'general-request',
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
sanitizeChatContextOverride({
|
||||
codexModel: null,
|
||||
chatTypeId: 'general-request',
|
||||
}),
|
||||
{
|
||||
codexModel: null,
|
||||
chatTypeId: 'general-request',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldSendOfflineChatNotification blocks chat push when app setting disables room notifications', () => {
|
||||
assert.equal(
|
||||
shouldSendOfflineChatNotification({
|
||||
|
||||
@@ -111,6 +111,25 @@ type ChatContext = {
|
||||
customContextContent?: string | null;
|
||||
};
|
||||
|
||||
export function sanitizeChatContextOverride(contextOverride?: Partial<ChatContext> | null) {
|
||||
if (!contextOverride) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedOverride = {
|
||||
...contextOverride,
|
||||
};
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(normalizedOverride, 'codexModel')
|
||||
&& normalizedOverride.codexModel === undefined
|
||||
) {
|
||||
delete normalizedOverride.codexModel;
|
||||
}
|
||||
|
||||
return normalizedOverride;
|
||||
}
|
||||
|
||||
type ChatPromptContextRef = {
|
||||
key: 'prompt_parent_question';
|
||||
promptTitle: string;
|
||||
@@ -5674,6 +5693,7 @@ export class ChatService {
|
||||
) {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const trimmedText = text.trim();
|
||||
const normalizedContextOverride = sanitizeChatContextOverride(options?.contextOverride);
|
||||
|
||||
if (!normalizedSessionId || !trimmedText) {
|
||||
return null;
|
||||
@@ -5700,7 +5720,10 @@ export class ChatService {
|
||||
topMenu: '',
|
||||
focusedComponentId: null,
|
||||
pageUrl: '',
|
||||
codexModel: conversation?.codexModel ?? null,
|
||||
codexModel:
|
||||
Object.prototype.hasOwnProperty.call(normalizedContextOverride ?? {}, 'codexModel')
|
||||
? normalizedContextOverride?.codexModel ?? null
|
||||
: conversation?.codexModel ?? null,
|
||||
chatTypeId: parentRequest?.chatTypeId ?? conversation?.chatTypeId ?? conversation?.lastChatTypeId ?? null,
|
||||
chatTypeLabel: parentRequest?.chatTypeLabel ?? conversation?.contextLabel ?? '',
|
||||
chatTypeDescription: conversation?.contextDescription ?? '',
|
||||
@@ -5713,7 +5736,7 @@ export class ChatService {
|
||||
options?.mode === 'direct' ? 'direct' : 'queue',
|
||||
{
|
||||
...baseContext,
|
||||
...(options?.contextOverride ?? {}),
|
||||
...(normalizedContextOverride ?? {}),
|
||||
},
|
||||
{
|
||||
omitPromptHistory: options?.omitPromptHistory,
|
||||
|
||||
BIN
public/..codex?
Normal file
BIN
public/..codex?
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
@@ -8594,13 +8594,19 @@ export function MainChatPanel({
|
||||
composerAssistActions={composerAssistActions}
|
||||
composerAssistModalTitle={composerAssistModalTitle}
|
||||
chatTypeOptions={chatTypeOptions}
|
||||
codexModelOptions={[
|
||||
{
|
||||
codexModelOptions={
|
||||
isSharedRoomsPresentation
|
||||
? CODEX_MODEL_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: `${option.label} · ${option.description}`,
|
||||
description: option.description,
|
||||
}))
|
||||
: [{
|
||||
value: effectiveCodexModel,
|
||||
label: `${effectiveCodexParticipants[0]?.name ?? 'Codex'} · ${resolveCodexModelLabel(effectiveCodexModel)}`,
|
||||
description: '채팅방 Context 참가자 기준',
|
||||
},
|
||||
]}
|
||||
}]
|
||||
}
|
||||
previewItems={previewItems.map((item) => ({ id: item.id, label: item.label, url: item.url, kind: item.kind }))}
|
||||
isResourceStripOpen={isResourceStripOpen}
|
||||
showResourceStrip={!isRoomsMode}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReloadOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Alert, App, Button, Checkbox, Form, Input, InputNumber, Spin, Tabs, Typography } from 'antd';
|
||||
import { Alert, App, Button, Checkbox, Form, Input, InputNumber, Radio, Select, Spin, Tabs, Typography } from 'antd';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { fetchWebPushConfig } from './notificationApi';
|
||||
import { NOTIFICATION_DEVICE_ID_STORAGE_KEY, getSavedNotificationDeviceId } from './notificationIdentity';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ensureWebPushSubscriptionRegistered,
|
||||
syncExistingWebPushSubscriptionRegistration,
|
||||
} from './webPushRegistration';
|
||||
import { CODEX_MODEL_OPTIONS, normalizeCodexModel } from './codexModelOptions';
|
||||
import { isPreviewRuntime } from './previewRuntime';
|
||||
import './SharedAppSettingsPage.css';
|
||||
|
||||
@@ -319,6 +320,46 @@ function buildStorageItemKey(entry: StorageEntry) {
|
||||
return `${entry.scope}:${entry.storageKey}`;
|
||||
}
|
||||
|
||||
function formatRoomModelByRoomValue(value: AppConfig['chat']['defaultModelByRoom']) {
|
||||
return JSON.stringify(value || {}, null, 2);
|
||||
}
|
||||
|
||||
function parseRoomModelByRoomValue(raw: string) {
|
||||
const trimmed = raw.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('채팅방별 모델 설정은 JSON 객체 형태여야 합니다.');
|
||||
}
|
||||
|
||||
return Object.entries(parsed).reduce<Record<string, string>>((accumulator, [sessionId, modelValue]) => {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
|
||||
if (!normalizedSessionId) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
const normalizedModel =
|
||||
typeof modelValue === 'string'
|
||||
? normalizeCodexModel(modelValue)
|
||||
: modelValue == null
|
||||
? ''
|
||||
: normalizeCodexModel(String(modelValue));
|
||||
|
||||
if (!normalizedModel) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
accumulator[normalizedSessionId] = normalizedModel;
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps) {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<SharedAppSettingsFormValue>();
|
||||
@@ -326,6 +367,7 @@ export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [savedConfig, setSavedConfig] = useState<AppConfig>(DEFAULT_APP_CONFIG);
|
||||
const [roomModelByRoomText, setRoomModelByRoomText] = useState(() => formatRoomModelByRoomValue(DEFAULT_APP_CONFIG.chat.defaultModelByRoom));
|
||||
const [activeTabKey, setActiveTabKey] = useState('general');
|
||||
const [storageGroupKey, setStorageGroupKey] = useState(STORAGE_GROUPS[0]?.key ?? 'share-chat');
|
||||
const [storageDrafts, setStorageDrafts] = useState<Record<string, string>>({});
|
||||
@@ -406,6 +448,7 @@ export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps
|
||||
const nextConfig = payload.config ?? DEFAULT_APP_CONFIG;
|
||||
setSavedConfig(nextConfig);
|
||||
form.setFieldsValue(nextConfig);
|
||||
setRoomModelByRoomText(formatRoomModelByRoomValue(nextConfig.chat.defaultModelByRoom));
|
||||
await syncWebPushStatus();
|
||||
loadStorageDrafts();
|
||||
} catch (error) {
|
||||
@@ -425,12 +468,22 @@ export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const saved = await saveAppConfigToServer(values, {
|
||||
const normalizedValues: SharedAppSettingsFormValue = {
|
||||
...values,
|
||||
chat: {
|
||||
...values.chat,
|
||||
defaultModelByRoom: parseRoomModelByRoomValue(roomModelByRoomText),
|
||||
defaultModelGlobal: normalizeCodexModel(values.chat.defaultModelGlobal),
|
||||
},
|
||||
};
|
||||
|
||||
const saved = await saveAppConfigToServer(normalizedValues, {
|
||||
shareToken,
|
||||
skipAutomationNotifications: true,
|
||||
});
|
||||
setSavedConfig(saved);
|
||||
form.setFieldsValue(saved);
|
||||
setRoomModelByRoomText(formatRoomModelByRoomValue(saved.chat.defaultModelByRoom));
|
||||
message.success('개인설정을 저장했습니다.');
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '앱 설정 저장에 실패했습니다.');
|
||||
@@ -438,7 +491,7 @@ export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[form, message, shareToken],
|
||||
[form, message, roomModelByRoomText, shareToken],
|
||||
);
|
||||
|
||||
const handleRequestNotificationPermission = useCallback(async () => {
|
||||
@@ -649,6 +702,30 @@ export function SharedAppSettingsPage({ shareToken }: SharedAppSettingsPageProps
|
||||
<Form.Item label="통합 검색 단축키" name={['gestureShortcuts', 'openSearch']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="모델 적용 범위" name={['chat', 'defaultModelScope']}>
|
||||
<Radio.Group
|
||||
options={[
|
||||
{ label: '전체', value: 'all' },
|
||||
{ label: '채팅방별', value: 'room' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="전체 기본 모델" name={['chat', 'defaultModelGlobal']}>
|
||||
<Select
|
||||
options={CODEX_MODEL_OPTIONS.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="채팅방별 마지막 모델(JSON)">
|
||||
<Input.TextArea
|
||||
rows={8}
|
||||
value={roomModelByRoomText}
|
||||
onChange={(event) => setRoomModelByRoomText(event.target.value)}
|
||||
placeholder='{}'
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { normalizeCodexModel } from './codexModelOptions';
|
||||
import { appendClientIdHeader } from './clientIdentity';
|
||||
import { getAutomationNotificationPreferenceTarget } from './notificationIdentity';
|
||||
import { isPreviewRuntime } from './previewRuntime';
|
||||
@@ -13,10 +14,14 @@ let cachedRawConfig: string | null = null;
|
||||
|
||||
export type AutomationScheduleType = 'interval' | 'daily' | 'weekly';
|
||||
export type WeeklyScheduleDay = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
|
||||
export type CodexModelScope = 'all' | 'room';
|
||||
export type PlanCostTimeUnit = 'hour' | 'minute' | 'second';
|
||||
|
||||
export type AppConfig = {
|
||||
chat: {
|
||||
defaultModelScope: CodexModelScope;
|
||||
defaultModelGlobal: string;
|
||||
defaultModelByRoom: Record<string, string>;
|
||||
maxContextMessages: number;
|
||||
maxContextChars: number;
|
||||
codexLiveMaxExecutionSeconds: number;
|
||||
@@ -76,6 +81,9 @@ export type AppConfig = {
|
||||
|
||||
export const DEFAULT_APP_CONFIG: AppConfig = {
|
||||
chat: {
|
||||
defaultModelScope: 'all',
|
||||
defaultModelGlobal: 'gpt-5.4',
|
||||
defaultModelByRoom: {},
|
||||
maxContextMessages: 12,
|
||||
maxContextChars: 3200,
|
||||
codexLiveMaxExecutionSeconds: 600,
|
||||
@@ -282,6 +290,32 @@ function normalizeRestartReservationCompletionDelaySeconds(value: number | undef
|
||||
return Math.min(300, Math.max(1, Math.round(value)));
|
||||
}
|
||||
|
||||
function normalizeCodexModelScope(value: string | undefined): AppConfig['chat']['defaultModelScope'] {
|
||||
if (value === 'room') {
|
||||
return 'room';
|
||||
}
|
||||
|
||||
return 'all';
|
||||
}
|
||||
|
||||
function normalizeChatDefaultModelByRoom(value: unknown): Record<string, string> {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(value).reduce<Record<string, string>>((accumulator, [sessionId, modelValue]) => {
|
||||
const normalizedSessionId = sessionId.trim();
|
||||
const modelText = typeof modelValue === 'string' ? modelValue : '';
|
||||
|
||||
if (!normalizedSessionId || !modelText.trim()) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
accumulator[normalizedSessionId] = normalizeCodexModel(modelText);
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeBooleanValue(value: boolean | undefined, fallback: boolean) {
|
||||
if (typeof value !== 'boolean') {
|
||||
return fallback;
|
||||
@@ -300,6 +334,9 @@ function normalizeConfig(raw?: Partial<AppConfig>): AppConfig {
|
||||
|
||||
return {
|
||||
chat: {
|
||||
defaultModelScope: normalizeCodexModelScope(chat?.defaultModelScope),
|
||||
defaultModelGlobal: normalizeCodexModel(chat?.defaultModelGlobal ?? DEFAULT_APP_CONFIG.chat.defaultModelGlobal),
|
||||
defaultModelByRoom: normalizeChatDefaultModelByRoom(chat?.defaultModelByRoom),
|
||||
maxContextMessages: normalizeChatContextMessageLimit(
|
||||
chat?.maxContextMessages,
|
||||
DEFAULT_APP_CONFIG.chat.maxContextMessages,
|
||||
|
||||
@@ -62,6 +62,7 @@ import { extractHiddenPreviewUrls, stripHiddenPreviewTags } from './previewMarke
|
||||
import { normalizeChatResourceUrl } from './chatResourceUrl';
|
||||
import { copyPreviewContent, copyText, isExecutionFailureMessage, isMissingRequestMessage, sharePreviewLink } from './chatUtils';
|
||||
import { ChatLinkCardPreview } from './ChatLinkCardPreview';
|
||||
import { normalizeCodexModel } from '../codexModelOptions';
|
||||
import { ChatActivityChecklist, buildChatActivityChecklistEntries } from './ChatActivityChecklist';
|
||||
import { describeExecutorCommand } from './executorActivitySummary';
|
||||
import { buildComposerFilePickKey } from './composerFilePickKey';
|
||||
@@ -105,6 +106,26 @@ type CodexModelOption = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
type ChatConversationRequestWithModel = ChatConversationRequest & {
|
||||
codexModel?: string | null;
|
||||
};
|
||||
|
||||
function resolveRequestModelTokenUsage(request: ChatConversationRequest) {
|
||||
return Math.max(
|
||||
0,
|
||||
Math.round(
|
||||
Number(
|
||||
request.usageSnapshot?.tokenTotals?.total ?? request.usageSnapshot?.totalTokens ?? request.totalTokens ?? 0,
|
||||
) || 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRequestCodexModel(request: ChatConversationRequest) {
|
||||
const requestWithModel = request as ChatConversationRequestWithModel;
|
||||
return normalizeCodexModel(requestWithModel.codexModel?.trim() || '');
|
||||
}
|
||||
|
||||
type PreviewOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -2951,6 +2972,7 @@ type ChatConversationViewProps = {
|
||||
composerAssistModalTitle?: string;
|
||||
chatTypeOptions: ChatTypeOption[];
|
||||
codexModelOptions: CodexModelOption[];
|
||||
sharedComposerModelTokenLimits?: Record<string, number | null>;
|
||||
previewItems: PreviewOption[];
|
||||
isResourceStripOpen: boolean;
|
||||
showResourceStrip?: boolean;
|
||||
@@ -3508,6 +3530,7 @@ export function ChatConversationView({
|
||||
composerAssistModalTitle,
|
||||
chatTypeOptions,
|
||||
codexModelOptions,
|
||||
sharedComposerModelTokenLimits,
|
||||
previewItems,
|
||||
isResourceStripOpen,
|
||||
showResourceStrip = true,
|
||||
@@ -3974,6 +3997,46 @@ export function ChatConversationView({
|
||||
const isChatTypeReadonly = isChatTypeSelectionLocked;
|
||||
const selectedChatTypeOption = chatTypeOptions.find((option) => option.value === selectedChatTypeId) ?? null;
|
||||
const sharedComposerChatTypeLabel = selectedChatTypeOption?.label?.trim() || '컨텍스트 없음';
|
||||
const normalizedSelectedCodexModel = normalizeCodexModel(selectedCodexModel);
|
||||
const selectedCodexModelTokenLimit =
|
||||
sharedComposerModelTokenLimits?.[normalizedSelectedCodexModel] ??
|
||||
sharedComposerModelTokenLimits?.[selectedCodexModel] ??
|
||||
null;
|
||||
const normalizedSelectedCodexModelTokenLimit = Number.isFinite(selectedCodexModelTokenLimit)
|
||||
? Math.max(0, Math.round(Number(selectedCodexModelTokenLimit)))
|
||||
: null;
|
||||
const sharedComposerModelUsedTokens = useMemo(() => {
|
||||
if (!useSharedComposerChrome) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
|
||||
requestStateMap.forEach((request) => {
|
||||
if (resolveRequestCodexModel(request) !== normalizedSelectedCodexModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
total += resolveRequestModelTokenUsage(request);
|
||||
});
|
||||
|
||||
return total;
|
||||
}, [requestStateMap, normalizedSelectedCodexModel, useSharedComposerChrome]);
|
||||
const sharedComposerModelRemainingTokensText = useMemo(() => {
|
||||
if (normalizedSelectedCodexModelTokenLimit === null) {
|
||||
return `이번 세션 사용량 ${sharedComposerModelUsedTokens.toLocaleString('ko-KR')} 토큰`;
|
||||
}
|
||||
|
||||
const remainingTokens = Math.max(0, normalizedSelectedCodexModelTokenLimit - sharedComposerModelUsedTokens);
|
||||
|
||||
return `잔여 ${remainingTokens.toLocaleString('ko-KR')} / ${normalizedSelectedCodexModelTokenLimit.toLocaleString(
|
||||
'ko-KR',
|
||||
)} 토큰`;
|
||||
}, [normalizedSelectedCodexModelTokenLimit, sharedComposerModelUsedTokens]);
|
||||
const sharedComposerModelTokenLimitLabel =
|
||||
sharedComposerModelRemainingTokensText && sharedComposerModelRemainingTokensText.length > 0
|
||||
? sharedComposerModelRemainingTokensText
|
||||
: '토큰 정보 없음';
|
||||
const pendingManualCompletionActionKeySet = useMemo(
|
||||
() => new Set(pendingManualCompletionActionKeys),
|
||||
[pendingManualCompletionActionKeys],
|
||||
@@ -7973,6 +8036,26 @@ export function ChatConversationView({
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div className="app-chat-panel__composer-type">
|
||||
<Select
|
||||
value={selectedCodexModel}
|
||||
placeholder="Codex 모델 선택"
|
||||
options={codexModelOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: (
|
||||
<div className="app-chat-panel__type-option">
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
getPopupContainer={(triggerNode) => triggerNode.closest('.app-chat-panel') ?? document.body}
|
||||
disabled={isComposerDisabled || codexModelOptions.length === 0}
|
||||
onChange={onSelectCodexModel}
|
||||
/>
|
||||
</div>
|
||||
<div className="app-chat-panel__composer-model-token" title={sharedComposerModelTokenLimitLabel}>
|
||||
<span>{sharedComposerModelTokenLimitLabel}</span>
|
||||
</div>
|
||||
<div className="app-chat-panel__composer-actions app-chat-panel__composer-actions--shared">
|
||||
<div className="app-chat-panel__composer-action-buttons">
|
||||
<Button
|
||||
|
||||
@@ -3020,6 +3020,7 @@ export async function submitChatShareMessage(
|
||||
sessionId?: string | null;
|
||||
mode?: 'queue' | 'direct';
|
||||
parentRequestId?: string | null;
|
||||
codexModel?: string | null;
|
||||
},
|
||||
) {
|
||||
return requestChatApi<{ ok: boolean; queuedRequestId: string }>(
|
||||
@@ -3031,6 +3032,7 @@ export async function submitChatShareMessage(
|
||||
sessionId: options?.sessionId?.trim() || undefined,
|
||||
mode: options?.mode === 'direct' ? 'direct' : 'queue',
|
||||
parentRequestId: options?.parentRequestId?.trim() || undefined,
|
||||
codexModel: options?.codexModel?.trim() || undefined,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -230,8 +230,56 @@
|
||||
padding-inline: 0;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.app-chat-panel--rooms-shared .app-chat-panel__composer-type {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.app-chat-panel--rooms-shared .app-chat-panel__composer-type--readonly {
|
||||
flex: 0 0 180px;
|
||||
}
|
||||
|
||||
.app-chat-panel--rooms-shared .app-chat-panel__composer-model-token {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: #334155;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid rgba(148, 163, 184, 0.42);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(241, 245, 249, 0.9) 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
|
||||
.app-chat-panel--rooms-shared .app-chat-panel__composer-type {
|
||||
flex: 1 1 150px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-chat-panel--rooms-shared .app-chat-panel__composer-model-token {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
color: #334155;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid rgba(148, 163, 184, 0.42);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(241, 245, 249, 0.9) 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
.app-chat-panel--rooms.app-chat-panel--rooms-shared .ant-card-body {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -926,6 +926,32 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chat-share-page__initial-load-alert.ant-alert .ant-alert-content,
|
||||
.chat-share-page__initial-load-alert.ant-alert .ant-alert-message,
|
||||
.chat-share-page__initial-load-alert.ant-alert .ant-alert-description {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.chat-share-page__initial-load-alert.ant-alert .ant-alert-action {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.chat-share-page__initial-load-alert.ant-alert .ant-alert-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-share-page__initial-load-alert.ant-alert .ant-alert-action {
|
||||
margin-top: 8px;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-share-page__live-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -3105,6 +3131,71 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-utility-button.ant-btn {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-utility-button.ant-btn .anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-menu.ant-dropdown-menu {
|
||||
min-width: 220px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-menu .ant-dropdown-menu-item {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-dropdown {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-menu-model {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-model-item {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-model-title {
|
||||
color: #0f172a;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-model-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-model-select.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-model-token {
|
||||
color: #334155;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-attachment-model-token--session {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.chat-share-page__composer-entry-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -58,8 +58,9 @@ import {
|
||||
submitChatSharePrompt,
|
||||
uploadChatShareComposerFile,
|
||||
type ChatShareRoomSummary,
|
||||
type ChatShareSnapshot,
|
||||
type ChatShareSnapshot
|
||||
} from '../mainChatPanel/chatUtils';
|
||||
import { DEFAULT_CODEX_MODEL, CODEX_MODEL_OPTIONS, normalizeCodexModel } from '../codexModelOptions';
|
||||
import { extractAttachmentPreviewUrls, extractChatMessageParts } from '../mainChatPanel/messageParts';
|
||||
import { stripHiddenPreviewTags } from '../mainChatPanel/previewMarkers';
|
||||
import { extractPreviewItems, type PreviewItem } from '../mainChatPanel/previewItems';
|
||||
@@ -1891,6 +1892,10 @@ function resolveRequestUsageTokens(request: ChatConversationRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRequestCodexModel(request: ChatConversationRequest) {
|
||||
return normalizeCodexModel(request.codexModel?.trim() || '');
|
||||
}
|
||||
|
||||
function resolveTokenUsageWindowSummary(
|
||||
requests: ChatConversationRequest[],
|
||||
periodKey: TokenUsagePeriodKey,
|
||||
@@ -3900,6 +3905,10 @@ function ShareRequestCard({
|
||||
const resolvedAnswerText = answerText.trim() || resolveShareRequestFallbackAnswerText(request);
|
||||
const shouldRenderQuestion = mode !== 'answer-only';
|
||||
const shouldRenderFullAnswer = mode === 'full' || mode === 'answer-only';
|
||||
const snapshotRefreshInFlightViewRef = useRef<'initial' | 'full' | null>(null);
|
||||
const snapshotMismatchRefreshKeyRef = useRef('');
|
||||
const snapshotRetryRefreshKeyRef = useRef('');
|
||||
const roomSwitchPerfRunIdRef = useRef('');
|
||||
const isRequestStillRunning = isRequestInFlight(request.status);
|
||||
const responseMessages = useMemo(
|
||||
() => relatedMessages.filter((message) => message.author !== 'user'),
|
||||
@@ -4201,6 +4210,8 @@ export function ChatSharePage() {
|
||||
const snapshotRefreshAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const snapshotRefreshInFlightRoomSessionIdRef = useRef('');
|
||||
const snapshotRefreshInFlightViewRef = useRef<'initial' | 'full' | null>(null);
|
||||
const snapshotMismatchRefreshKeyRef = useRef('');
|
||||
const snapshotRetryRefreshKeyRef = useRef('');
|
||||
const roomSwitchPerfRunIdRef = useRef('');
|
||||
const pendingSnapshotPerfStageRef = useRef<{ runId: string; stage: 'cache' | 'initial' | 'full' } | null>(null);
|
||||
const pendingSilentRefreshRef = useRef(false);
|
||||
@@ -4233,6 +4244,7 @@ export function ChatSharePage() {
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const [expandMode, setExpandMode] = useState<ShareExpandMode>('pending');
|
||||
const [latestRequestId, setLatestRequestId] = useState('');
|
||||
const [sharedComposerCodexModel, setSharedComposerCodexModel] = useState(DEFAULT_CODEX_MODEL);
|
||||
const [pendingPromptSelections, setPendingPromptSelections] = useState<Record<string, PendingSharePromptSelection>>({});
|
||||
const [pendingPromptCompletionRequestIds, setPendingPromptCompletionRequestIds] = useState<string[]>([]);
|
||||
const [pendingVerificationCompletionRequestIds, setPendingVerificationCompletionRequestIds] = useState<string[]>([]);
|
||||
@@ -4501,9 +4513,13 @@ export function ChatSharePage() {
|
||||
setSelectedAppEnvironment(nextEnvironment);
|
||||
}
|
||||
}, [selectedAppEnvironment, sortedAllowedPlayAppEntries]);
|
||||
const canManageSharedAppSettings = sharePermissionSet.has('manage') && shareAllowedAppIdSet.has('app-settings');
|
||||
const allowedManagementApps = useMemo(
|
||||
() => SHARE_MANAGEMENT_APP_OPTIONS.filter((option) => shareAllowedAppIdSet.has(option.value)),
|
||||
[shareAllowedAppIdSet],
|
||||
() => SHARE_MANAGEMENT_APP_OPTIONS.filter((option) => (
|
||||
shareAllowedAppIdSet.has(option.value)
|
||||
&& (option.value !== 'app-settings' || canManageSharedAppSettings)
|
||||
)),
|
||||
[canManageSharedAppSettings, shareAllowedAppIdSet],
|
||||
);
|
||||
const currentShareChatTarget = useMemo(
|
||||
() => buildShareChatEnvironmentTarget(snapshot?.share.sharePath ?? null, normalizedToken, selectedAppEnvironment),
|
||||
@@ -5092,9 +5108,12 @@ export function ChatSharePage() {
|
||||
|
||||
if (matchedRequestedRoom) {
|
||||
recordShareRoomSwitchPerfStep(currentPerfRunId, `${requestView}-matched-requested-room`, nextSnapshot);
|
||||
snapshotMismatchRefreshKeyRef.current = '';
|
||||
snapshotRetryRefreshKeyRef.current = '';
|
||||
setIsRoomSwitching(false);
|
||||
} else if (requestedSessionId) {
|
||||
pendingSilentRefreshRef.current = true;
|
||||
snapshotRetryRefreshKeyRef.current = `${requestedSessionId}:${requestView}:retry`;
|
||||
}
|
||||
|
||||
setErrorMessage('');
|
||||
@@ -5141,9 +5160,17 @@ export function ChatSharePage() {
|
||||
setIsRefreshing(false);
|
||||
|
||||
if (pendingSilentRefreshRef.current) {
|
||||
const retryKey = `${requestedSessionId}:${requestView}:retry`;
|
||||
if (snapshotRetryRefreshKeyRef.current === retryKey) {
|
||||
pendingSilentRefreshRef.current = false;
|
||||
snapshotRetryRefreshKeyRef.current = `${requestedSessionId}:${requestView}:done`;
|
||||
return;
|
||||
}
|
||||
|
||||
snapshotRetryRefreshKeyRef.current = retryKey;
|
||||
pendingSilentRefreshRef.current = false;
|
||||
window.setTimeout(() => {
|
||||
void refreshSnapshot({ silent: true });
|
||||
void refreshSnapshot({ silent: true, view: 'full' });
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -5386,7 +5413,7 @@ export function ChatSharePage() {
|
||||
} finally {
|
||||
setPendingShareRuntimeRequestIds((current) => current.filter((item) => item !== normalizedRequestId));
|
||||
}
|
||||
}, [activeShareRoomSessionId, message, modal, normalizedToken, pendingShareRuntimeRequestIds, refreshShareRuntime, refreshSnapshot, sortedRequests]);
|
||||
}, [activeShareRoomSessionId, message, modal, normalizedToken, pendingShareRuntimeRequestIds, refreshShareRuntime, refreshSnapshot, sortedRequests, sharedComposerCodexModel]);
|
||||
const handleResubmitQueuedRequestDirect = useCallback(async (requestId: string) => {
|
||||
const normalizedRequestId = requestId.trim();
|
||||
|
||||
@@ -5439,6 +5466,7 @@ export function ChatSharePage() {
|
||||
sessionId: activeShareRoomSessionId,
|
||||
mode: 'direct',
|
||||
parentRequestId: targetRequest.parentRequestId?.trim() || '',
|
||||
codexModel: sharedComposerCodexModel,
|
||||
});
|
||||
message.success('대기 요청을 취소하고 즉시전송했습니다.');
|
||||
await Promise.all([
|
||||
@@ -5455,7 +5483,7 @@ export function ChatSharePage() {
|
||||
setIsSending(false);
|
||||
setPendingShareRuntimeRequestIds((current) => current.filter((item) => item !== normalizedRequestId));
|
||||
}
|
||||
}, [activeShareRoomSessionId, isSending, message, modal, normalizedToken, pendingShareRuntimeRequestIds, refreshShareRuntime, refreshSnapshot, sortedRequests]);
|
||||
}, [activeShareRoomSessionId, isSending, message, modal, normalizedToken, pendingShareRuntimeRequestIds, refreshShareRuntime, refreshSnapshot, sharedComposerCodexModel, sortedRequests]);
|
||||
const handleSaveSharedRoomSettings = useCallback(async () => {
|
||||
if (!snapshot?.conversation.sessionId) {
|
||||
setIsRoomSettingsOpen(false);
|
||||
@@ -6005,9 +6033,14 @@ export function ChatSharePage() {
|
||||
|
||||
if (restoredTarget) {
|
||||
setProgramReloadKey(0);
|
||||
if (!canLaunchShareProgram(restoredTarget.appId)) {
|
||||
message.warning('이 공유 링크에서는 허용되지 않은 앱입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProgramTarget(restoredTarget);
|
||||
}
|
||||
}, []);
|
||||
}, [canLaunchShareProgram, message]);
|
||||
|
||||
const handleReloadPage = useCallback(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -6371,11 +6404,20 @@ export function ChatSharePage() {
|
||||
|
||||
const requestedSessionId = requestedRoomSessionId.trim();
|
||||
const hasMatchingSnapshot = doesShareSnapshotMatchRequestedRoom(snapshot, requestedSessionId);
|
||||
const mismatchRefreshKey = requestedSessionId ? `snapshot-mismatch:${requestedSessionId}` : ''
|
||||
|
||||
if (hasMatchingSnapshot) {
|
||||
if (snapshotMismatchRefreshKeyRef.current) {
|
||||
snapshotMismatchRefreshKeyRef.current = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!requestedSessionId || snapshotMismatchRefreshKeyRef.current === mismatchRefreshKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
snapshotMismatchRefreshKeyRef.current = mismatchRefreshKey;
|
||||
void refreshSnapshot({ silent: true, view: 'initial' });
|
||||
}, [normalizedToken, refreshSnapshot, requestedRoomSessionId, snapshot]);
|
||||
useEffect(() => {
|
||||
@@ -7039,6 +7081,81 @@ export function ChatSharePage() {
|
||||
const shareKind = snapshot?.share.kind ?? 'request-bundle';
|
||||
const isPromptShare = shareKind === 'prompt';
|
||||
|
||||
const handlePersistSharedComposerModel = useCallback(async (nextCodexModel: string) => {
|
||||
const normalizedModel = normalizeCodexModel(nextCodexModel);
|
||||
const normalizedScope = appConfig.chat.defaultModelScope;
|
||||
const normalizedRoomSessionId = selectedShareRoomSessionId.trim();
|
||||
const currentByRoom = appConfig.chat.defaultModelByRoom;
|
||||
|
||||
if (normalizedScope === 'room' && normalizedRoomSessionId) {
|
||||
if ((currentByRoom[normalizedRoomSessionId] ?? '') === normalizedModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextByRoom = {
|
||||
...currentByRoom,
|
||||
[normalizedRoomSessionId]: normalizedModel,
|
||||
};
|
||||
|
||||
const optimisticConfig = {
|
||||
...appConfig,
|
||||
chat: {
|
||||
...appConfig.chat,
|
||||
defaultModelByRoom: nextByRoom,
|
||||
},
|
||||
};
|
||||
|
||||
setStoredAppConfig(optimisticConfig);
|
||||
|
||||
if (!normalizedToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const savedConfig = await saveAppConfigToServer(optimisticConfig, {
|
||||
shareToken: normalizedToken,
|
||||
skipAutomationNotifications: true,
|
||||
});
|
||||
setStoredAppConfig(savedConfig);
|
||||
} catch {
|
||||
// Keep local preference update even if server sync fails.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (appConfig.chat.defaultModelGlobal === normalizedModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optimisticConfig = {
|
||||
...appConfig,
|
||||
chat: {
|
||||
...appConfig.chat,
|
||||
defaultModelGlobal: normalizedModel,
|
||||
},
|
||||
};
|
||||
|
||||
setStoredAppConfig(optimisticConfig);
|
||||
|
||||
if (!normalizedToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const savedConfig = await saveAppConfigToServer(optimisticConfig, {
|
||||
shareToken: normalizedToken,
|
||||
skipAutomationNotifications: true,
|
||||
});
|
||||
setStoredAppConfig(savedConfig);
|
||||
} catch {
|
||||
// Keep local preference update even if server sync fails.
|
||||
}
|
||||
}, [
|
||||
appConfig,
|
||||
normalizedToken,
|
||||
selectedShareRoomSessionId,
|
||||
]);
|
||||
|
||||
const handleSendMessageByMode = useCallback(async (mode: 'queue' | 'direct') => {
|
||||
const outgoingText = buildOutgoingShareMessageText(draftText, composerAttachments).trim();
|
||||
const resolvedParentRequestId =
|
||||
@@ -7050,6 +7167,7 @@ export function ChatSharePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCodexModel = normalizeCodexModel(sharedComposerCodexModel);
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
@@ -7057,7 +7175,9 @@ export function ChatSharePage() {
|
||||
sessionId: selectedShareRoomSessionId,
|
||||
mode,
|
||||
parentRequestId: resolvedParentRequestId,
|
||||
codexModel: nextCodexModel,
|
||||
});
|
||||
void handlePersistSharedComposerModel(nextCodexModel);
|
||||
setDraftText('');
|
||||
setComposerAttachments([]);
|
||||
setReplyReferenceRequestId('');
|
||||
@@ -7092,6 +7212,7 @@ export function ChatSharePage() {
|
||||
}, [
|
||||
composerAttachments,
|
||||
draftText,
|
||||
handlePersistSharedComposerModel,
|
||||
isSending,
|
||||
isUploadingComposerAttachment,
|
||||
message,
|
||||
@@ -7101,6 +7222,7 @@ export function ChatSharePage() {
|
||||
selectedShareRoomSessionId,
|
||||
shareKind,
|
||||
snapshot?.targetRequest.requestId,
|
||||
sharedComposerCodexModel,
|
||||
]);
|
||||
|
||||
const handleSendMessage = useCallback(async () => {
|
||||
@@ -7499,6 +7621,24 @@ export function ChatSharePage() {
|
||||
[handleUploadPromptAttachment, isUploadingComposerAttachment, message],
|
||||
);
|
||||
|
||||
const handleOpenComposerAttachmentInput = useCallback(() => {
|
||||
if (isSending || isUploadingComposerAttachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
composerAttachmentInputRef.current?.click();
|
||||
}, [isSending, isUploadingComposerAttachment]);
|
||||
|
||||
const handleShareComposerAttachmentMenuClick = useCallback(
|
||||
({ key }: { key: string }) => {
|
||||
if (key === 'composer-attachment-files') {
|
||||
handleOpenComposerAttachmentInput();
|
||||
}
|
||||
},
|
||||
[handleOpenComposerAttachmentInput],
|
||||
);
|
||||
|
||||
|
||||
const handleComposerPaste = useCallback(
|
||||
(event: ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const files = resolveShareComposerPasteFiles(event.clipboardData);
|
||||
@@ -8255,6 +8395,54 @@ export function ChatSharePage() {
|
||||
|
||||
return candidates.find((value) => value?.trim())?.trim() || 'Codex Live';
|
||||
}, [currentRequest?.chatTypeLabel, snapshot?.targetRequest?.chatTypeLabel, sortedRequests]);
|
||||
const shareComposerResolvedCodexModel = useMemo(() => {
|
||||
const normalizedRoomSessionId = selectedShareRoomSessionId.trim();
|
||||
const chatModelScope = appConfig.chat.defaultModelScope;
|
||||
const roomModelText =
|
||||
chatModelScope === 'room' ? appConfig.chat.defaultModelByRoom[normalizedRoomSessionId]?.trim() ?? '' : '';
|
||||
|
||||
if (chatModelScope === 'room' && normalizedRoomSessionId && roomModelText) {
|
||||
return normalizeCodexModel(roomModelText);
|
||||
}
|
||||
|
||||
if (appConfig.chat.defaultModelGlobal?.trim()) {
|
||||
return normalizeCodexModel(appConfig.chat.defaultModelGlobal);
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
currentRequest?.codexModel,
|
||||
snapshot?.targetRequest?.codexModel,
|
||||
...sortedRequests.map((request) => request.codexModel),
|
||||
];
|
||||
|
||||
return normalizeCodexModel(candidates.find((value) => value?.trim())?.trim());
|
||||
}, [
|
||||
appConfig.chat.defaultModelByRoom,
|
||||
appConfig.chat.defaultModelGlobal,
|
||||
appConfig.chat.defaultModelScope,
|
||||
currentRequest?.codexModel,
|
||||
selectedShareRoomSessionId,
|
||||
sortedRequests,
|
||||
snapshot?.targetRequest?.codexModel,
|
||||
]);
|
||||
useEffect(() => {
|
||||
setSharedComposerCodexModel((current) => {
|
||||
const nextModel = normalizeCodexModel(shareComposerResolvedCodexModel);
|
||||
return normalizeCodexModel(current) === nextModel ? current : nextModel;
|
||||
});
|
||||
}, [shareComposerResolvedCodexModel]);
|
||||
const shareComposerModelUsedTokens = useMemo(() => {
|
||||
const normalizedModel = normalizeCodexModel(sharedComposerCodexModel);
|
||||
|
||||
return sortedRequests.reduce((accumulator, request) => {
|
||||
if (resolveRequestCodexModel(request) !== normalizedModel) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
return accumulator + resolveRequestUsageTokens(request);
|
||||
}, 0);
|
||||
}, [sharedComposerCodexModel, sortedRequests]);
|
||||
const shareComposerModelTokenLabel = useMemo(() => '이번 세션 사용량 ' + formatTokenCount(shareComposerModelUsedTokens) + ' 토큰', [shareComposerModelUsedTokens]);
|
||||
const shareMenuLabel = useMemo(() => {
|
||||
const candidates = [
|
||||
snapshot?.conversation.requestBadgeLabel,
|
||||
@@ -8652,8 +8840,7 @@ export function ChatSharePage() {
|
||||
restoreProgramReturnSnapshot(programTarget?.restoreSnapshot);
|
||||
setProgramTarget(null);
|
||||
}, [programTarget?.restoreSnapshot, restoreProgramReturnSnapshot]);
|
||||
const canLaunchShareProgram = useCallback(
|
||||
(appId?: ShareProgramTarget['appId']) => {
|
||||
function canLaunchShareProgram(appId?: ShareProgramTarget['appId']) {
|
||||
if (!appId) {
|
||||
return true;
|
||||
}
|
||||
@@ -8662,10 +8849,12 @@ export function ChatSharePage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (appId === 'app-settings') {
|
||||
return canManageSharedAppSettings;
|
||||
}
|
||||
|
||||
return shareAllowedAppIdSet.has(appId);
|
||||
},
|
||||
[shareAllowedAppIdSet],
|
||||
);
|
||||
}
|
||||
const recordShareAppLaunch = useCallback((appId?: string) => {
|
||||
if (!appId) {
|
||||
return;
|
||||
@@ -9030,6 +9219,8 @@ export function ChatSharePage() {
|
||||
const sevenDayRemaining = tokenUsageSevenDaySummary.remainingTokens;
|
||||
const fiveHourRemaining = tokenUsageFiveHourSummary.remainingTokens;
|
||||
const currentAvailableTokens = resolveSmallestFiniteNumber(sevenDayRemaining, fiveHourRemaining);
|
||||
const formatUsagePercentLabel = (value: number | null) =>
|
||||
value == null ? '미표시' : String(Math.round(value)) + '%';
|
||||
const axisLimit = sevenDayLimit > 0 ? sevenDayLimit : Math.max(fiveHourLimit, currentAvailableTokens ?? 0);
|
||||
const overallLabel = sevenDayLimit > 0 ? `${formatTokenCount(sevenDayLimit)} 토큰` : '무제한';
|
||||
const sevenDayRemainingLabel = sevenDayRemaining == null ? '무제한' : `${formatTokenCount(sevenDayRemaining)} 토큰`;
|
||||
@@ -9049,6 +9240,19 @@ export function ChatSharePage() {
|
||||
|
||||
return {
|
||||
currentAvailableLabel,
|
||||
currentPercentLabel: formatUsagePercentLabel(
|
||||
resolveSmallestFiniteNumber(tokenUsageSevenDaySummary.percentage, tokenUsageFiveHourSummary.percentage),
|
||||
),
|
||||
sevenDayUsagePercentLabel: formatUsagePercentLabel(
|
||||
tokenUsageSevenDaySummary.percentage == null
|
||||
? null
|
||||
: Math.max(0, Math.min(100, tokenUsageSevenDaySummary.percentage)),
|
||||
),
|
||||
fiveHourUsagePercentLabel: formatUsagePercentLabel(
|
||||
tokenUsageFiveHourSummary.percentage == null
|
||||
? null
|
||||
: Math.max(0, Math.min(100, tokenUsageFiveHourSummary.percentage)),
|
||||
),
|
||||
overallLabel,
|
||||
sevenDayRemainingLabel,
|
||||
fiveHourRemainingLabel,
|
||||
@@ -9211,7 +9415,7 @@ export function ChatSharePage() {
|
||||
</span>
|
||||
<span className="chat-share-page__settings-item-description">
|
||||
{selectedTokenUsageSetting
|
||||
? `지금 사용 가능 ${tokenUsageOverview.currentAvailableLabel}`
|
||||
? '지금 사용 가능 ' + tokenUsageOverview.currentAvailableLabel + ' (5시간 ' + tokenUsageOverview.fiveHourUsagePercentLabel + ' / 1주일 ' + tokenUsageOverview.sevenDayUsagePercentLabel + ')'
|
||||
: '공유 링크 생성 시 선택된 토큰 설정이 없습니다.'}
|
||||
</span>
|
||||
<span className="chat-share-page__settings-item-description">
|
||||
@@ -9249,7 +9453,7 @@ export function ChatSharePage() {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(shareAllowedAppIdSet.has('app-settings')
|
||||
...(canManageSharedAppSettings
|
||||
? [
|
||||
{
|
||||
key: 'conversation-personal-settings',
|
||||
@@ -9499,6 +9703,64 @@ export function ChatSharePage() {
|
||||
programTarget,
|
||||
sourceGroupDetail,
|
||||
]);
|
||||
const shareComposerModelTokenAvailableLabel = useMemo(() => {
|
||||
return tokenUsageOverview.currentAvailableLabel;
|
||||
}, [tokenUsageOverview.currentAvailableLabel]);
|
||||
const handleSharedComposerModelChange = useCallback((nextCodexModel: string) => {
|
||||
setSharedComposerCodexModel(normalizeCodexModel(nextCodexModel));
|
||||
}, []);
|
||||
const shareComposerAttachmentItems = useMemo((): MenuProps['items'] => [
|
||||
{
|
||||
key: 'composer-attachment-files',
|
||||
label: (
|
||||
<span className="chat-share-page__settings-item">
|
||||
<span className="chat-share-page__settings-item-title">파일 첨부</span>
|
||||
<span className="chat-share-page__settings-item-description">컴퓨터에서 파일을 선택해 공유 채팅에 첨부합니다.</span>
|
||||
</span>
|
||||
),
|
||||
icon: <FileTextOutlined />,
|
||||
},
|
||||
{
|
||||
key: 'composer-attachment-model',
|
||||
className: 'chat-share-page__composer-attachment-menu-model',
|
||||
label: (
|
||||
<div
|
||||
className="chat-share-page__composer-attachment-model-item"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="chat-share-page__composer-attachment-model-title">모델 설정</div>
|
||||
<Select
|
||||
size="middle"
|
||||
value={sharedComposerCodexModel}
|
||||
aria-label="코덱스 모델"
|
||||
options={CODEX_MODEL_OPTIONS.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
}))}
|
||||
onChange={handleSharedComposerModelChange}
|
||||
disabled={isSending}
|
||||
className="chat-share-page__composer-attachment-model-select"
|
||||
popupMatchSelectWidth={220}
|
||||
/>
|
||||
<span className="chat-share-page__composer-attachment-model-token">{`잔여 ${shareComposerModelTokenAvailableLabel}`}</span>
|
||||
<span className="chat-share-page__composer-attachment-model-token">{`5시간 ${tokenUsageOverview.fiveHourUsagePercentLabel} 사용`}</span>
|
||||
<span className="chat-share-page__composer-attachment-model-token">{`1주일 ${tokenUsageOverview.sevenDayUsagePercentLabel} 사용`}</span>
|
||||
<span className="chat-share-page__composer-attachment-model-token chat-share-page__composer-attachment-model-token--session">{shareComposerModelTokenLabel}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [
|
||||
handleSharedComposerModelChange,
|
||||
isSending,
|
||||
sharedComposerCodexModel,
|
||||
shareComposerModelTokenAvailableLabel,
|
||||
shareComposerModelTokenLabel,
|
||||
tokenUsageOverview.fiveHourUsagePercentLabel,
|
||||
tokenUsageOverview.sevenDayUsagePercentLabel,
|
||||
]);
|
||||
|
||||
const shareExpandModeMenuItems = useMemo<MenuProps['items']>(
|
||||
() => [
|
||||
{
|
||||
@@ -10171,16 +10433,29 @@ export function ChatSharePage() {
|
||||
/>
|
||||
<div className="chat-share-page__composer-topline">
|
||||
<div className="app-chat-panel__composer-utility-buttons">
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
aria-label="파일 첨부"
|
||||
title="파일 첨부"
|
||||
onClick={() => {
|
||||
composerAttachmentInputRef.current?.click();
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: shareComposerAttachmentItems,
|
||||
onClick: handleShareComposerAttachmentMenuClick,
|
||||
}}
|
||||
overlayClassName="chat-share-page__composer-attachment-menu"
|
||||
disabled={isSending || isUploadingComposerAttachment}
|
||||
className="chat-share-page__composer-attachment-dropdown"
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label="첨부 옵션"
|
||||
title="첨부 옵션"
|
||||
loading={isUploadingComposerAttachment}
|
||||
className="chat-share-page__composer-utility-button"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="app-chat-panel__composer-type chat-share-page__composer-type-readonly">
|
||||
<Select
|
||||
|
||||
@@ -273,6 +273,16 @@
|
||||
.e-reader__panel-header-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.e-reader__panel-header > h2 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.e-reader__icon-button {
|
||||
@@ -560,6 +570,7 @@
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.e-reader__panel-body--locked {
|
||||
@@ -618,6 +629,7 @@
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.e-reader__library-sort-row {
|
||||
@@ -632,12 +644,18 @@
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--reader-ink);
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.e-reader__library-sort-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@@ -649,9 +667,16 @@
|
||||
}
|
||||
|
||||
.e-reader__library-sort-trigger .anticon {
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e-reader__library-sort-trigger span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.e-reader__bottom-sheet {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -984,7 +1009,8 @@
|
||||
font-size: 14px;
|
||||
line-height: 1.42;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: keep-all;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
@@ -997,6 +1023,14 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e-reader__book-card > span,
|
||||
.e-reader__book-card p,
|
||||
.e-reader__library-status {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.e-reader__book-card-date {
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
|
||||
Reference in New Issue
Block a user