feat: update main chat and system chat UI

This commit is contained in:
2026-05-25 17:26:37 +09:00
parent fb5ec649cd
commit f59522ffc4
120 changed files with 43262 additions and 3325 deletions

View File

@@ -1,11 +1,21 @@
import { getRegisteredAccessToken } from '../tokenAccess';
import { buildPreviewRuntimeUrl, resolvePreviewAppOrigin } from '../previewRuntime';
const CHAT_EXTERNAL_LINK_OPENED_AT_KEY = 'ai-code-app.chat.external-link-opened-at';
const CHAT_EXTERNAL_LINK_TTL_MS = 15_000;
const EXTERNAL_WINDOW_TARGET_PREFIX = 'ai-code-app.external-window';
const UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS = 600;
type LinkNavigationEvent = {
preventDefault?: () => void;
stopPropagation?: () => void;
};
type OpenExternalLinkOptions = {
event?: LinkNavigationEvent;
onUnsupportedStandalone?: (url: string) => void;
};
function canUseSessionStorage() {
return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';
}
@@ -26,6 +36,165 @@ function clearExternalLinkOpenTimestamp() {
window.sessionStorage.removeItem(CHAT_EXTERNAL_LINK_OPENED_AT_KEY);
}
function isStandaloneDisplayMode() {
if (typeof window === 'undefined') {
return false;
}
return (
window.matchMedia?.('(display-mode: standalone)').matches === true ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true
);
}
function isAppleMobileStandaloneMode() {
if (typeof window === 'undefined') {
return false;
}
const navigatorValue = window.navigator as Navigator & { standalone?: boolean };
const userAgent = navigatorValue.userAgent ?? '';
const platform = navigatorValue.platform ?? '';
const maxTouchPoints = typeof navigatorValue.maxTouchPoints === 'number' ? navigatorValue.maxTouchPoints : 0;
const isAppleMobileUserAgent = /iPhone|iPad|iPod/iu.test(userAgent);
const isTouchMac = platform === 'MacIntel' && maxTouchPoints > 1;
return isStandaloneDisplayMode() && (isAppleMobileUserAgent || isTouchMac);
}
function buildExternalWindowTarget() {
return `${EXTERNAL_WINDOW_TARGET_PREFIX}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function clickExternalAnchor(url: string, target: string) {
if (typeof document === 'undefined') {
return;
}
const anchor = document.createElement('a');
anchor.href = url;
anchor.target = target;
anchor.rel = 'noopener noreferrer';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
function buildPreviewRuntimeFallbackUrl(url: string) {
if (typeof window === 'undefined') {
return null;
}
try {
const targetUrl = new URL(url, window.location.origin);
const previewOrigin = resolvePreviewAppOrigin();
if (targetUrl.origin !== window.location.origin && targetUrl.origin !== previewOrigin) {
return null;
}
const previewRuntimeUrl = buildPreviewRuntimeUrl(
targetUrl.pathname,
targetUrl.search,
getRegisteredAccessToken(),
null,
'mobile',
);
if (!targetUrl.hash) {
return previewRuntimeUrl;
}
const previewUrl = new URL(previewRuntimeUrl);
previewUrl.hash = targetUrl.hash;
return previewUrl.toString();
} catch {
return null;
}
}
function canFallbackToSameTab(url: string) {
if (typeof window === 'undefined') {
return false;
}
try {
const targetUrl = new URL(url, window.location.origin);
const previewOrigin = resolvePreviewAppOrigin();
return targetUrl.origin === window.location.origin || targetUrl.origin === previewOrigin;
} catch {
return false;
}
}
function openSameTabFallback(url: string) {
if (typeof window === 'undefined' || !canFallbackToSameTab(url)) {
return false;
}
window.location.assign(url);
return true;
}
function openPreviewRuntimeFallback(url: string) {
if (typeof window === 'undefined') {
return false;
}
const previewRuntimeUrl = buildPreviewRuntimeFallbackUrl(url);
if (!previewRuntimeUrl) {
return false;
}
window.location.assign(previewRuntimeUrl);
return true;
}
function scheduleUnsupportedStandaloneFallback(url: string, callback?: (url: string) => void) {
if (typeof window === 'undefined' || typeof document === 'undefined' || !isAppleMobileStandaloneMode()) {
return;
}
window.setTimeout(() => {
const pageStillVisible = document.visibilityState !== 'hidden';
const pageStillFocused = typeof document.hasFocus !== 'function' || document.hasFocus();
if (pageStillVisible && pageStillFocused) {
if (openPreviewRuntimeFallback(url)) {
return;
}
if (openSameTabFallback(url)) {
return;
}
callback?.(url);
}
}, UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS);
}
function schedulePopupBlockedFallback(url: string, callback?: (url: string) => void) {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}
window.setTimeout(() => {
const pageStillVisible = document.visibilityState !== 'hidden';
const pageStillFocused = typeof document.hasFocus !== 'function' || document.hasFocus();
if (!pageStillVisible || !pageStillFocused) {
return;
}
if (openSameTabFallback(url)) {
return;
}
callback?.(url);
}, UNSUPPORTED_STANDALONE_FALLBACK_DELAY_MS);
}
export function shouldSkipForegroundResyncAfterExternalLink() {
if (!canUseSessionStorage()) {
return false;
@@ -42,24 +211,29 @@ export function shouldSkipForegroundResyncAfterExternalLink() {
return Number.isFinite(openedAt) && Date.now() - openedAt <= CHAT_EXTERNAL_LINK_TTL_MS;
}
export function openChatExternalLink(url: string, event?: LinkNavigationEvent) {
event?.preventDefault?.();
event?.stopPropagation?.();
export function openExternalLinkInNewWindow(url: string, options: OpenExternalLinkOptions = {}) {
options.event?.preventDefault?.();
options.event?.stopPropagation?.();
if (typeof window === 'undefined') {
return;
}
persistExternalLinkOpenTimestamp(Date.now());
const openedWindow = window.open(url, '_blank', 'noopener,noreferrer');
const target = buildExternalWindowTarget();
const openedWindow = window.open(url, target, 'noopener,noreferrer');
if (openedWindow) {
return;
}
const anchor = document.createElement('a');
anchor.href = url;
anchor.target = '_blank';
anchor.rel = 'noopener noreferrer';
anchor.click();
clickExternalAnchor(url, target);
scheduleUnsupportedStandaloneFallback(url, options.onUnsupportedStandalone);
schedulePopupBlockedFallback(url, options.onUnsupportedStandalone);
}
export function openChatExternalLink(url: string, event?: LinkNavigationEvent) {
openExternalLinkInNewWindow(url, {
event,
});
}