From b6f5686e7dbb0578d55bcd08e60a56d039faca5e Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Thu, 26 Jun 2025 16:56:53 -0700 Subject: [PATCH 1/4] added announcement modal support --- ui/desktop/src/App.tsx | 2 + .../src/components/AnnouncementModal.tsx | 175 ++++++++++++++++++ ui/desktop/src/components/ui/BaseModal.tsx | 10 +- ui/desktop/src/json.d.ts | 5 + ui/desktop/src/updates.ts | 1 + 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 ui/desktop/src/components/AnnouncementModal.tsx diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index fe0a055a11e7..14c72ac5692d 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -12,6 +12,7 @@ import { extractExtensionName } from './components/settings/extensions/utils'; import { GoosehintsModal } from './components/GoosehintsModal'; import { type ExtensionConfig } from './extensions'; import { type Recipe } from './recipe'; +import AnnouncementModal from './components/AnnouncementModal'; import ChatView from './components/ChatView'; import SuspenseLoader from './suspense-loader'; @@ -610,6 +611,7 @@ export default function App() { setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} /> )} + ); } diff --git a/ui/desktop/src/components/AnnouncementModal.tsx b/ui/desktop/src/components/AnnouncementModal.tsx new file mode 100644 index 000000000000..ad437af3c657 --- /dev/null +++ b/ui/desktop/src/components/AnnouncementModal.tsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from 'react'; +import { BaseModal } from './ui/BaseModal'; +import MarkdownContent from './MarkdownContent'; +import { ANNOUNCEMENTS_ENABLED } from '../updates'; +import packageJson from '../../package.json'; +import { getAnnouncementContent } from '../../announcements/content'; +import { Button } from './ui/button'; + +interface AnnouncementMeta { + id: string; + version: string; + title: string; + file: string; +} + +// Simple version comparison function for semantic versioning (x.y.z) +// Returns: -1 if a < b, 0 if a === b, 1 if a > b +function compareVersions(a: string, b: string): number { + const parseVersion = (version: string) => version.split('.').map((part) => parseInt(part, 10)); + + const versionA = parseVersion(a); + const versionB = parseVersion(b); + + for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) { + const partA = versionA[i] || 0; + const partB = versionB[i] || 0; + + if (partA < partB) return -1; + if (partA > partB) return 1; + } + + return 0; +} + +export default function AnnouncementModal() { + const [showAnnouncementModal, setShowAnnouncementModal] = useState(false); + const [combinedAnnouncementContent, setCombinedAnnouncementContent] = useState( + null + ); + const [unseenAnnouncements, setUnseenAnnouncements] = useState([]); + + // Load announcements and check for unseen ones + useEffect(() => { + const loadAnnouncements = async () => { + // Only proceed if announcements are enabled + if (!ANNOUNCEMENTS_ENABLED) { + return; + } + + try { + // Load the announcements index + const indexModule = await import('../../announcements/index.json'); + const announcements = indexModule.default as AnnouncementMeta[]; + + // Get current app version + const currentVersion = packageJson.version; + + // Filter announcements to only include those for current version or earlier + const applicableAnnouncements = announcements.filter((announcement) => { + // Simple version comparison - assumes semantic versioning + const announcementVersion = announcement.version; + return compareVersions(announcementVersion, currentVersion) <= 0; + }); + + // Get list of seen announcement IDs + const seenAnnouncementIds = JSON.parse( + localStorage.getItem('seenAnnouncementIds') || '[]' + ) as string[]; + + // Find ALL unseen announcements (in order) + const unseenAnnouncementsList = applicableAnnouncements.filter( + (announcement) => !seenAnnouncementIds.includes(announcement.id) + ); + + if (unseenAnnouncementsList.length > 0) { + // Load content for all unseen announcements + const contentPromises = unseenAnnouncementsList.map(async (announcement) => { + const content = getAnnouncementContent(announcement.file); + return { announcement, content }; + }); + + const loadedAnnouncements = await Promise.all(contentPromises); + const validAnnouncements = loadedAnnouncements.filter(({ content }) => content); + + if (validAnnouncements.length > 0) { + // Combine all announcement content with separators + const combinedContent = validAnnouncements + .map(({ content }) => content) + .join('\n\n---\n\n'); + + setUnseenAnnouncements(validAnnouncements.map(({ announcement }) => announcement)); + setCombinedAnnouncementContent(combinedContent); + setShowAnnouncementModal(true); + } + } + } catch (error) { + console.log('No announcements found or failed to load:', error); + } + }; + + loadAnnouncements(); + }, []); + + const handleCloseAnnouncement = () => { + if (unseenAnnouncements.length === 0) return; + + // Get existing seen announcement IDs + const seenAnnouncementIds = JSON.parse( + localStorage.getItem('seenAnnouncementIds') || '[]' + ) as string[]; + + // Add all unseen announcement IDs to the seen list + const newSeenIds = [...seenAnnouncementIds]; + unseenAnnouncements.forEach((announcement) => { + if (!newSeenIds.includes(announcement.id)) { + newSeenIds.push(announcement.id); + } + }); + + localStorage.setItem('seenAnnouncementIds', JSON.stringify(newSeenIds)); + setShowAnnouncementModal(false); + }; + + // For development/testing: Add a way to reset announcements + // This can be called from browser console: window.resetAnnouncements() + useEffect(() => { + interface WindowWithReset { + resetAnnouncements?: () => void; + resetSpecificAnnouncement?: (id: string) => void; + } + const windowWithReset = window as WindowWithReset & typeof window; + + // Reset all announcements + windowWithReset.resetAnnouncements = () => { + localStorage.removeItem('seenAnnouncementIds'); + window.location.reload(); + }; + + // Reset a specific announcement by ID + windowWithReset.resetSpecificAnnouncement = (id: string) => { + const seenIds = JSON.parse(localStorage.getItem('seenAnnouncementIds') || '[]') as string[]; + const filteredIds = seenIds.filter((seenId) => seenId !== id); + localStorage.setItem('seenAnnouncementIds', JSON.stringify(filteredIds)); + window.location.reload(); + }; + }, []); + + // Don't render anything if there are no announcements to show + if (!combinedAnnouncementContent || unseenAnnouncements.length === 0) { + return null; + } + + return ( + + + + } + > +
+
+ +
+
+
+ ); +} diff --git a/ui/desktop/src/components/ui/BaseModal.tsx b/ui/desktop/src/components/ui/BaseModal.tsx index 1550c822ec5b..a518e5d16482 100644 --- a/ui/desktop/src/components/ui/BaseModal.tsx +++ b/ui/desktop/src/components/ui/BaseModal.tsx @@ -8,7 +8,7 @@ export function BaseModal({ actions, }: { isOpen: boolean; - title: string; + title?: string; children: React.ReactNode; actions: React.ReactNode; // Buttons for actions }) { @@ -19,9 +19,11 @@ export function BaseModal({
{/* Header */} -
-

{title}

-
+ {title && ( +
+

{title}

+
+ )} {/* Content */} {children &&
{children}
} diff --git a/ui/desktop/src/json.d.ts b/ui/desktop/src/json.d.ts index 5bb9ca45aa2c..02b4714e659a 100644 --- a/ui/desktop/src/json.d.ts +++ b/ui/desktop/src/json.d.ts @@ -38,6 +38,11 @@ declare module '*.mp4' { export default value; } +declare module '*.md?raw' { + const value: string; + export default value; +} + // Extend CSS properties to include Electron-specific properties declare namespace React { interface CSSProperties { diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts index 3029a850030d..3ec4002a691b 100644 --- a/ui/desktop/src/updates.ts +++ b/ui/desktop/src/updates.ts @@ -1,2 +1,3 @@ export const UPDATES_ENABLED = true; export const COST_TRACKING_ENABLED = true; +export const ANNOUNCEMENTS_ENABLED = false; From a8b319e396fc5a33e1ef46373b0e37c3b5de7942 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Thu, 26 Jun 2025 16:57:29 -0700 Subject: [PATCH 2/4] added announcement modal support --- ui/desktop/announcements/content.ts | 7 +++++++ ui/desktop/announcements/index.json | 1 + 2 files changed, 8 insertions(+) create mode 100644 ui/desktop/announcements/content.ts create mode 100644 ui/desktop/announcements/index.json diff --git a/ui/desktop/announcements/content.ts b/ui/desktop/announcements/content.ts new file mode 100644 index 000000000000..4b20e4865d1d --- /dev/null +++ b/ui/desktop/announcements/content.ts @@ -0,0 +1,7 @@ +// Map of announcement file names to their content +export const announcementContents: Record = {}; + +// Helper function to get announcement content by filename +export function getAnnouncementContent(filename: string): string | null { + return announcementContents[filename] || null; +} diff --git a/ui/desktop/announcements/index.json b/ui/desktop/announcements/index.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/ui/desktop/announcements/index.json @@ -0,0 +1 @@ +[] From 4c38cee2fd743d8ac63e94ac1fe95a9d0c83b7a5 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Thu, 26 Jun 2025 17:14:07 -0700 Subject: [PATCH 3/4] add back title --- .../src/components/AnnouncementModal.tsx | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/ui/desktop/src/components/AnnouncementModal.tsx b/ui/desktop/src/components/AnnouncementModal.tsx index ad437af3c657..21a7f338c59e 100644 --- a/ui/desktop/src/components/AnnouncementModal.tsx +++ b/ui/desktop/src/components/AnnouncementModal.tsx @@ -121,30 +121,6 @@ export default function AnnouncementModal() { setShowAnnouncementModal(false); }; - // For development/testing: Add a way to reset announcements - // This can be called from browser console: window.resetAnnouncements() - useEffect(() => { - interface WindowWithReset { - resetAnnouncements?: () => void; - resetSpecificAnnouncement?: (id: string) => void; - } - const windowWithReset = window as WindowWithReset & typeof window; - - // Reset all announcements - windowWithReset.resetAnnouncements = () => { - localStorage.removeItem('seenAnnouncementIds'); - window.location.reload(); - }; - - // Reset a specific announcement by ID - windowWithReset.resetSpecificAnnouncement = (id: string) => { - const seenIds = JSON.parse(localStorage.getItem('seenAnnouncementIds') || '[]') as string[]; - const filteredIds = seenIds.filter((seenId) => seenId !== id); - localStorage.setItem('seenAnnouncementIds', JSON.stringify(filteredIds)); - window.location.reload(); - }; - }, []); - // Don't render anything if there are no announcements to show if (!combinedAnnouncementContent || unseenAnnouncements.length === 0) { return null; @@ -153,6 +129,11 @@ export default function AnnouncementModal() { return (
} > -
+