feat(landing): enhance base URL handling and improve image paths
- Introduced baseURL configuration to dynamically set asset paths in the landing components. - Updated AppLogo and HeroSection components to use baseURL for logo image sources. - Refactored ScreenshotsSection to utilize a publicPath function for consistent image path handling. - Improved LanguageSwitcher to synchronize the i18n locale with the store on mount. - Enhanced TaskCommentInput to handle file uploads more robustly, including validation for empty files and improved error handling. - Adjusted MessageComposer to conditionally support attachments based on team status.
This commit is contained in:
parent
5cf9751b41
commit
0bc8bf1fe9
18 changed files with 100 additions and 108 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const { baseURL } = useRuntimeConfig().app;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink to="/" class="app-logo">
|
<NuxtLink to="/" class="app-logo">
|
||||||
<img
|
<img
|
||||||
src="/logo-192.png"
|
:src="`${baseURL}logo-192.png`"
|
||||||
alt="Claude Agent Teams"
|
alt="Claude Agent Teams"
|
||||||
class="app-logo__img"
|
class="app-logo__img"
|
||||||
width="36"
|
width="36"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ const switchLocalePath = useSwitchLocalePath();
|
||||||
const props = defineProps<{ fullWidth?: boolean; compact?: boolean; iconOnly?: boolean }>();
|
const props = defineProps<{ fullWidth?: boolean; compact?: boolean; iconOnly?: boolean }>();
|
||||||
const localeStore = useLocaleStore();
|
const localeStore = useLocaleStore();
|
||||||
|
|
||||||
|
// Sync store with actual i18n locale on mount (handles SSG hydration)
|
||||||
|
onMounted(() => {
|
||||||
|
if (locale.value && locale.value !== localeStore.current) {
|
||||||
|
localeStore.setLocale(locale.value as string, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const flagIconMap: Record<string, string> = {
|
const flagIconMap: Record<string, string> = {
|
||||||
en: "circle-flags:us",
|
en: "circle-flags:us",
|
||||||
ru: "circle-flags:ru"
|
ru: "circle-flags:ru"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { mdiRobotOutline, mdiViewDashboardOutline, mdiOpenSourceInitiative } fro
|
||||||
|
|
||||||
const { content } = useLandingContent();
|
const { content } = useLandingContent();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { baseURL } = useRuntimeConfig().app;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -13,7 +14,7 @@ const { t } = useI18n();
|
||||||
<v-col cols="12" md="6" class="hero-section__content">
|
<v-col cols="12" md="6" class="hero-section__content">
|
||||||
<h1 class="hero-section__title">
|
<h1 class="hero-section__title">
|
||||||
<img
|
<img
|
||||||
src="/logo-192.png"
|
:src="`${baseURL}logo-192.png`"
|
||||||
alt=""
|
alt=""
|
||||||
class="hero-section__logo"
|
class="hero-section__logo"
|
||||||
width="56"
|
width="56"
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,22 @@ import { register } from 'swiper/element/bundle';
|
||||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiArrowExpand } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiArrowExpand } from '@mdi/js';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { baseURL } = useRuntimeConfig().app;
|
||||||
|
|
||||||
register();
|
register();
|
||||||
|
|
||||||
|
const publicPath = (path: string) => `${baseURL}${path.replace(/^\//, '')}`;
|
||||||
|
|
||||||
const screenshots = [
|
const screenshots = [
|
||||||
{ src: '/screenshots/1.jpg', alt: 'Kanban board with agent tasks' },
|
{ src: publicPath('/screenshots/1.jpg'), alt: 'Kanban board with agent tasks' },
|
||||||
{ src: '/screenshots/2.jpg', alt: 'Agent team communication' },
|
{ src: publicPath('/screenshots/2.jpg'), alt: 'Agent team communication' },
|
||||||
{ src: '/screenshots/3.png', alt: 'Code review diff view' },
|
{ src: publicPath('/screenshots/3.png'), alt: 'Code review diff view' },
|
||||||
{ src: '/screenshots/4.png', alt: 'Team management dashboard' },
|
{ src: publicPath('/screenshots/4.png'), alt: 'Team management dashboard' },
|
||||||
{ src: '/screenshots/5.png', alt: 'Live process monitoring' },
|
{ src: publicPath('/screenshots/5.png'), alt: 'Live process monitoring' },
|
||||||
{ src: '/screenshots/6.png', alt: 'Session context analysis' },
|
{ src: publicPath('/screenshots/6.png'), alt: 'Session context analysis' },
|
||||||
{ src: '/screenshots/7.png', alt: 'Cross-team messaging' },
|
{ src: publicPath('/screenshots/7.png'), alt: 'Cross-team messaging' },
|
||||||
{ src: '/screenshots/8.png', alt: 'Task details and comments' },
|
{ src: publicPath('/screenshots/8.png'), alt: 'Task details and comments' },
|
||||||
{ src: '/screenshots/9.png', alt: 'Built-in code editor' },
|
{ src: publicPath('/screenshots/9.png'), alt: 'Built-in code editor' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const swiperRef = ref<HTMLElement | null>(null);
|
const swiperRef = ref<HTMLElement | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,19 @@ export const useLocation = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const initLocale = () => {
|
const initLocale = () => {
|
||||||
|
// Sync store with actual i18n locale (already resolved from route by nuxt-i18n)
|
||||||
|
const currentLocale = i18n?.locale?.value || "en";
|
||||||
|
|
||||||
if (cookie.value) {
|
if (cookie.value) {
|
||||||
localeStore.setLocale(cookie.value, false);
|
// Cookie exists — sync store, but don't override route-based locale
|
||||||
if (i18n?.setLocale) {
|
localeStore.setLocale(currentLocale, false);
|
||||||
i18n.setLocale(cookie.value);
|
if (cookie.value !== currentLocale) {
|
||||||
} else if (i18n?.locale?.value) {
|
cookie.value = currentLocale;
|
||||||
i18n.locale.value = cookie.value;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No cookie — detect from browser and set
|
||||||
const detected = getBrowserLocale();
|
const detected = getBrowserLocale();
|
||||||
localeStore.setLocale(detected, false);
|
localeStore.setLocale(detected, false);
|
||||||
if (i18n?.setLocale) {
|
if (i18n?.setLocale) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ declare const process: any;
|
||||||
const siteUrl = process.env.NUXT_PUBLIC_SITE_URL || "https://claude-agent-teams.dev";
|
const siteUrl = process.env.NUXT_PUBLIC_SITE_URL || "https://claude-agent-teams.dev";
|
||||||
const githubRepo = process.env.NUXT_PUBLIC_GITHUB_REPO || "777genius/claude_agent_teams_ui";
|
const githubRepo = process.env.NUXT_PUBLIC_GITHUB_REPO || "777genius/claude_agent_teams_ui";
|
||||||
const githubReleasesUrl = `https://github.com/${githubRepo}/releases`;
|
const githubReleasesUrl = `https://github.com/${githubRepo}/releases`;
|
||||||
|
const baseURL = process.env.NUXT_APP_BASE_URL || "/";
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2026-01-19",
|
compatibilityDate: "2026-01-19",
|
||||||
|
|
@ -15,9 +16,10 @@ export default defineNuxtConfig({
|
||||||
inlineSSRStyles: false
|
inlineSSRStyles: false
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
baseURL,
|
||||||
head: {
|
head: {
|
||||||
link: [
|
link: [
|
||||||
{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" },
|
{ rel: "icon", type: "image/x-icon", href: `${baseURL}favicon.ico` },
|
||||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" },
|
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" },
|
||||||
{ rel: "preload", href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap", as: "style" },
|
{ rel: "preload", href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap", as: "style" },
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
|
@ -1,7 +1,6 @@
|
||||||
import { createHash } from 'crypto';
|
|
||||||
|
|
||||||
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
|
import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
|
||||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
|
|
||||||
import { isImageMime } from '@renderer/utils/attachmentUtils';
|
|
||||||
|
|
||||||
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
||||||
|
import { isImageMime } from '@renderer/utils/attachmentUtils';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import { AttachmentThumbnail } from './AttachmentThumbnail';
|
import { AttachmentThumbnail } from './AttachmentThumbnail';
|
||||||
import { ImageLightbox } from './ImageLightbox';
|
import { ImageLightbox } from './ImageLightbox';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
||||||
import { formatFileSize, isImageMime } from '@renderer/utils/attachmentUtils';
|
import { formatFileSize, isImageMime } from '@renderer/utils/attachmentUtils';
|
||||||
import { Ban, X } from 'lucide-react';
|
import { Ban, X } from 'lucide-react';
|
||||||
|
|
||||||
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
|
||||||
|
|
||||||
import { AttachmentThumbnail } from './AttachmentThumbnail';
|
import { AttachmentThumbnail } from './AttachmentThumbnail';
|
||||||
|
|
||||||
import type { AttachmentPayload } from '@shared/types';
|
import type { AttachmentPayload } from '@shared/types';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { AlertCircle, X } from 'lucide-react';
|
|
||||||
|
|
||||||
import { isImageMime } from '@renderer/utils/attachmentUtils';
|
import { isImageMime } from '@renderer/utils/attachmentUtils';
|
||||||
|
import { AlertCircle, X } from 'lucide-react';
|
||||||
|
|
||||||
import { AttachmentPreviewItem } from './AttachmentPreviewItem';
|
import { AttachmentPreviewItem } from './AttachmentPreviewItem';
|
||||||
import { ImageLightbox } from './ImageLightbox';
|
import { ImageLightbox } from './ImageLightbox';
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ export const SendMessageDialog = ({
|
||||||
addFiles,
|
addFiles,
|
||||||
removeAttachment,
|
removeAttachment,
|
||||||
clearAttachments,
|
clearAttachments,
|
||||||
|
clearError: clearAttachmentError,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
handleDrop,
|
handleDrop,
|
||||||
} = useAttachments({ persistenceKey: `sendMessage:${teamName}:attachments` });
|
} = useAttachments({ persistenceKey: `sendMessage:${teamName}:attachments` });
|
||||||
|
|
@ -417,6 +418,7 @@ export const SendMessageDialog = ({
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
onRemove={removeAttachment}
|
onRemove={removeAttachment}
|
||||||
error={attachmentError ?? fileRestrictionError}
|
error={attachmentError ?? fileRestrictionError}
|
||||||
|
onDismissError={clearAttachmentError}
|
||||||
disabled={attachmentsBlocked}
|
disabled={attachmentsBlocked}
|
||||||
disabledHint="File attachments are only supported when sending to the team lead while the team is online. Remove attachments or switch recipient."
|
disabledHint="File attachments are only supported when sending to the team lead while the team is online. Remove attachments or switch recipient."
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -88,12 +88,14 @@ export const TaskCommentInput = ({
|
||||||
!addingComment;
|
!addingComment;
|
||||||
|
|
||||||
const addFiles = useCallback(
|
const addFiles = useCallback(
|
||||||
(files: FileList | File[]) => {
|
async (files: FileList | File[]) => {
|
||||||
setAttachError(null);
|
setAttachError(null);
|
||||||
const fileArray = Array.from(files);
|
const fileArray = Array.from(files);
|
||||||
|
|
||||||
|
// 1. Separate unsupported files → path prepend
|
||||||
|
const supported: File[] = [];
|
||||||
for (const file of fileArray) {
|
for (const file of fileArray) {
|
||||||
if (categorizeFile(file) === 'unsupported') {
|
if (categorizeFile(file) === 'unsupported') {
|
||||||
// Insert absolute file path into comment text for unsupported types
|
|
||||||
const filePath = (file as { path?: string }).path;
|
const filePath = (file as { path?: string }).path;
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const current = draft.value;
|
const current = draft.value;
|
||||||
|
|
@ -101,37 +103,51 @@ export const TaskCommentInput = ({
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (file.size === 0) {
|
||||||
|
setAttachError(`File "${file.name}" is empty`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
setAttachError(
|
setAttachError(
|
||||||
`File too large: ${(file.size / (1024 * 1024)).toFixed(1)} MB (max 20 MB)`
|
`File too large: ${(file.size / (1024 * 1024)).toFixed(1)} MB (max 20 MB)`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const reader = new FileReader();
|
supported.push(file);
|
||||||
reader.onload = () => {
|
}
|
||||||
const result = reader.result as string;
|
|
||||||
const base64 = result.split(',')[1];
|
if (supported.length === 0) return;
|
||||||
if (!base64) return;
|
|
||||||
const id = crypto.randomUUID();
|
// 2. Read all files sequentially to avoid race condition with MAX_ATTACHMENTS
|
||||||
setPendingAttachments((prev) => {
|
for (const file of supported) {
|
||||||
if (prev.length >= MAX_ATTACHMENTS) {
|
const result = await new Promise<string | null>((resolve) => {
|
||||||
setAttachError(`Maximum ${MAX_ATTACHMENTS} attachments per comment`);
|
const reader = new FileReader();
|
||||||
return prev;
|
reader.onload = () => resolve(reader.result as string);
|
||||||
}
|
reader.onerror = () => resolve(null);
|
||||||
return [
|
reader.readAsDataURL(file);
|
||||||
...prev,
|
});
|
||||||
{
|
if (!result) continue;
|
||||||
id,
|
const base64 = result.split(',')[1];
|
||||||
filename: file.name,
|
if (!base64) continue;
|
||||||
mimeType: getEffectiveMimeType(file),
|
|
||||||
base64Data: base64,
|
const id = crypto.randomUUID();
|
||||||
previewUrl: result,
|
setPendingAttachments((prev) => {
|
||||||
size: file.size,
|
if (prev.length >= MAX_ATTACHMENTS) {
|
||||||
},
|
setAttachError(`Maximum ${MAX_ATTACHMENTS} attachments per comment`);
|
||||||
];
|
return prev;
|
||||||
});
|
}
|
||||||
};
|
return [
|
||||||
reader.readAsDataURL(file);
|
...prev,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
filename: file.name,
|
||||||
|
mimeType: getEffectiveMimeType(file),
|
||||||
|
base64Data: base64,
|
||||||
|
previewUrl: result,
|
||||||
|
size: file.size,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[draft]
|
[draft]
|
||||||
|
|
@ -194,12 +210,12 @@ export const TaskCommentInput = ({
|
||||||
for (const item of Array.from(items)) {
|
for (const item of Array.from(items)) {
|
||||||
if (item.kind === 'file') {
|
if (item.kind === 'file') {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file && categorizeFile(file) !== 'unsupported') pastedFiles.push(file);
|
if (file) pastedFiles.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pastedFiles.length > 0) {
|
if (pastedFiles.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
addFiles(pastedFiles);
|
void addFiles(pastedFiles);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addFiles]
|
[addFiles]
|
||||||
|
|
@ -318,7 +334,7 @@ export const TaskCommentInput = ({
|
||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.files) addFiles(e.target.files);
|
if (e.target.files) void addFiles(e.target.files);
|
||||||
|
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ export const MessageComposer = ({
|
||||||
// const leadContext = useStore((s) =>
|
// const leadContext = useStore((s) =>
|
||||||
// isLeadAgentRecipient ? s.leadContextByTeam[teamName] : undefined
|
// isLeadAgentRecipient ? s.leadContextByTeam[teamName] : undefined
|
||||||
// );
|
// );
|
||||||
const supportsAttachments = isLeadRecipient && !isCrossTeam;
|
const supportsAttachments = isLeadRecipient && !isCrossTeam && !!isTeamAlive;
|
||||||
const canAttach = supportsAttachments && draft.canAddMore;
|
const canAttach = supportsAttachments && draft.canAddMore;
|
||||||
const attachmentsBlocked = draft.attachments.length > 0 && !supportsAttachments;
|
const attachmentsBlocked = draft.attachments.length > 0 && !supportsAttachments;
|
||||||
const canSend =
|
const canSend =
|
||||||
|
|
@ -370,22 +370,22 @@ export const MessageComposer = ({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragCounterRef.current = 0;
|
dragCounterRef.current = 0;
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
if (!isLeadRecipient) {
|
if (!supportsAttachments) {
|
||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
if (files?.length) {
|
if (files?.length) {
|
||||||
showFileRestrictionError();
|
showFileRestrictionError();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (canAttach) draftHandleDrop(e);
|
draftHandleDrop(e);
|
||||||
},
|
},
|
||||||
[isLeadRecipient, canAttach, draftHandleDrop, showFileRestrictionError]
|
[supportsAttachments, draftHandleDrop, showFileRestrictionError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handlePaste: draftHandlePaste } = draft;
|
const { handlePaste: draftHandlePaste } = draft;
|
||||||
const handlePasteWrapper = useCallback(
|
const handlePasteWrapper = useCallback(
|
||||||
(e: React.ClipboardEvent) => {
|
(e: React.ClipboardEvent) => {
|
||||||
if (!isLeadRecipient) {
|
if (!supportsAttachments) {
|
||||||
const hasFiles = Array.from(e.clipboardData.items).some((item) => item.kind === 'file');
|
const hasFiles = Array.from(e.clipboardData.items).some((item) => item.kind === 'file');
|
||||||
if (hasFiles) {
|
if (hasFiles) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -393,9 +393,9 @@ export const MessageComposer = ({
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (canAttach) draftHandlePaste(e);
|
draftHandlePaste(e);
|
||||||
},
|
},
|
||||||
[isLeadRecipient, canAttach, draftHandlePaste, showFileRestrictionError]
|
[supportsAttachments, draftHandlePaste, showFileRestrictionError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const remaining = MAX_TEXT_LENGTH - trimmed.length;
|
const remaining = MAX_TEXT_LENGTH - trimmed.length;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { draftStorage } from '@renderer/services/draftStorage';
|
import { draftStorage } from '@renderer/services/draftStorage';
|
||||||
import { categorizeFile } from '@shared/constants/attachments';
|
|
||||||
import {
|
import {
|
||||||
fileToAttachmentPayload,
|
fileToAttachmentPayload,
|
||||||
MAX_FILES,
|
MAX_FILES,
|
||||||
MAX_TOTAL_SIZE,
|
MAX_TOTAL_SIZE,
|
||||||
validateAttachment,
|
validateAttachment,
|
||||||
} from '@renderer/utils/attachmentUtils';
|
} from '@renderer/utils/attachmentUtils';
|
||||||
|
import { categorizeFile } from '@shared/constants/attachments';
|
||||||
|
|
||||||
import type { AttachmentPayload } from '@shared/types';
|
import type { AttachmentPayload } from '@shared/types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,13 @@ import {
|
||||||
type ComposerDraftSnapshot,
|
type ComposerDraftSnapshot,
|
||||||
composerDraftStorage,
|
composerDraftStorage,
|
||||||
} from '@renderer/services/composerDraftStorage';
|
} from '@renderer/services/composerDraftStorage';
|
||||||
import { categorizeFile } from '@shared/constants/attachments';
|
|
||||||
import {
|
import {
|
||||||
fileToAttachmentPayload,
|
fileToAttachmentPayload,
|
||||||
MAX_FILES,
|
MAX_FILES,
|
||||||
MAX_TOTAL_SIZE,
|
MAX_TOTAL_SIZE,
|
||||||
validateAttachment,
|
validateAttachment,
|
||||||
} from '@renderer/utils/attachmentUtils';
|
} from '@renderer/utils/attachmentUtils';
|
||||||
|
import { categorizeFile } from '@shared/constants/attachments';
|
||||||
|
|
||||||
import type { InlineChip } from '@renderer/types/inlineChip';
|
import type { InlineChip } from '@renderer/types/inlineChip';
|
||||||
import type { AgentActionMode, AttachmentPayload } from '@shared/types';
|
import type { AgentActionMode, AttachmentPayload } from '@shared/types';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import type { TeamSummary } from '@shared/types';
|
import type { TeamSummary } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -35,29 +35,6 @@ function buildTeamMentionEntries(teams: readonly TeamSummary[]): TeamMentionEntr
|
||||||
.sort(compareTeamMentionEntries);
|
.sort(compareTeamMentionEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function areTeamMentionEntriesEqual(
|
|
||||||
prev: readonly TeamMentionEntry[],
|
|
||||||
next: readonly TeamMentionEntry[]
|
|
||||||
): boolean {
|
|
||||||
if (prev === next) return true;
|
|
||||||
if (prev.length !== next.length) return false;
|
|
||||||
|
|
||||||
for (let i = 0; i < prev.length; i++) {
|
|
||||||
const prevEntry = prev[i];
|
|
||||||
const nextEntry = next[i];
|
|
||||||
if (
|
|
||||||
prevEntry.teamName !== nextEntry.teamName ||
|
|
||||||
prevEntry.displayName !== nextEntry.displayName ||
|
|
||||||
prevEntry.color !== nextEntry.color ||
|
|
||||||
prevEntry.deletedAt !== nextEntry.deletedAt
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTeamMentionMeta(entries: readonly TeamMentionEntry[]): TeamMentionMeta {
|
function buildTeamMentionMeta(entries: readonly TeamMentionEntry[]): TeamMentionMeta {
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return { teamNames: EMPTY_TEAM_NAMES, teamColorByName: EMPTY_TEAM_COLOR_MAP };
|
return { teamNames: EMPTY_TEAM_NAMES, teamColorByName: EMPTY_TEAM_COLOR_MAP };
|
||||||
|
|
@ -84,24 +61,8 @@ function buildTeamMentionMeta(entries: readonly TeamMentionEntry[]): TeamMention
|
||||||
|
|
||||||
export function useStableTeamMentionMeta(teams: readonly TeamSummary[]): TeamMentionMeta {
|
export function useStableTeamMentionMeta(teams: readonly TeamSummary[]): TeamMentionMeta {
|
||||||
const entries = useMemo(() => buildTeamMentionEntries(teams), [teams]);
|
const entries = useMemo(() => buildTeamMentionEntries(teams), [teams]);
|
||||||
const stableRef = useRef<{ entries: readonly TeamMentionEntry[]; value: TeamMentionMeta } | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Intentional ref-as-cache pattern: avoids allocating a new object every
|
const meta = useMemo(() => buildTeamMentionMeta(entries), [entries]);
|
||||||
// render while still returning a referentially-stable value when the
|
|
||||||
// underlying data hasn't changed.
|
|
||||||
|
|
||||||
if (
|
return meta;
|
||||||
stableRef.current === null ||
|
|
||||||
!areTeamMentionEntriesEqual(stableRef.current.entries, entries)
|
|
||||||
) {
|
|
||||||
stableRef.current = {
|
|
||||||
entries,
|
|
||||||
value: buildTeamMentionMeta(entries),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/refs -- stable ref cache pattern
|
|
||||||
return stableRef.current.value;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue