Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ui/desktop/announcements/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Map of announcement file names to their content
export const announcementContents: Record<string, string> = {};

// Helper function to get announcement content by filename
export function getAnnouncementContent(filename: string): string | null {
return announcementContents[filename] || null;
}
1 change: 1 addition & 0 deletions ui/desktop/announcements/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
2 changes: 2 additions & 0 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -610,6 +611,7 @@ export default function App() {
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/>
)}
<AnnouncementModal />
</ModelAndProviderProvider>
);
}
156 changes: 156 additions & 0 deletions ui/desktop/src/components/AnnouncementModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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<string | null>(
null
);
const [unseenAnnouncements, setUnseenAnnouncements] = useState<AnnouncementMeta[]>([]);

// 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);
};

// Don't render anything if there are no announcements to show
if (!combinedAnnouncementContent || unseenAnnouncements.length === 0) {
return null;
}

return (
<BaseModal
isOpen={showAnnouncementModal}
title={
unseenAnnouncements.length === 1
? unseenAnnouncements[0].title
: `${unseenAnnouncements.length}`
}
actions={
<div className="flex justify-end pb-4">
<Button
variant="ghost"
onClick={handleCloseAnnouncement}
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-textProminent font-medium text-md"
>
Got it!
</Button>
</div>
}
>
<div className="max-h-96 overflow-y-auto -mx-12">
<div className="px-4 py-10">
<MarkdownContent content={combinedAnnouncementContent} />
</div>
</div>
</BaseModal>
);
}
10 changes: 6 additions & 4 deletions ui/desktop/src/components/ui/BaseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function BaseModal({
actions,
}: {
isOpen: boolean;
title: string;
title?: string;
children: React.ReactNode;
actions: React.ReactNode; // Buttons for actions
}) {
Expand All @@ -19,9 +19,11 @@ export function BaseModal({
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[440px] bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden p-[16px] pt-[24px] pb-0">
<div className="px-8 pb-0 space-y-8">
{/* Header */}
<div className="flex">
<h2 className="text-2xl font-regular dark:text-white text-gray-900">{title}</h2>
</div>
{title && (
<div className="flex">
<h2 className="text-2xl font-regular dark:text-white text-gray-900">{title}</h2>
</div>
)}

{/* Content */}
{children && <div className="px-8">{children}</div>}
Expand Down
5 changes: 5 additions & 0 deletions ui/desktop/src/json.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions ui/desktop/src/updates.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const UPDATES_ENABLED = true;
export const COST_TRACKING_ENABLED = true;
export const ANNOUNCEMENTS_ENABLED = false;
Loading