Show release notes on startup (#44)
This commit is contained in:
20
src/hooks/useAppVersion.ts
Normal file
20
src/hooks/useAppVersion.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
|
||||||
|
export function useAppVersion() {
|
||||||
|
const [appVersion, setAppVersion] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchVersion = async () => {
|
||||||
|
try {
|
||||||
|
const version = await IpcClient.getInstance().getAppVersion();
|
||||||
|
setAppVersion(version);
|
||||||
|
} catch (error) {
|
||||||
|
setAppVersion(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchVersion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return appVersion;
|
||||||
|
}
|
||||||
@@ -117,6 +117,7 @@ export const UserSettingsSchema = z.object({
|
|||||||
enableDyadPro: z.boolean().optional(),
|
enableDyadPro: z.boolean().optional(),
|
||||||
dyadProBudget: DyadProBudgetSchema.optional(),
|
dyadProBudget: DyadProBudgetSchema.optional(),
|
||||||
experiments: ExperimentsSchema.optional(),
|
experiments: ExperimentsSchema.optional(),
|
||||||
|
lastShownReleaseNotesVersion: z.string().optional(),
|
||||||
// DEPRECATED.
|
// DEPRECATED.
|
||||||
runtimeMode: RuntimeModeSchema.optional(),
|
runtimeMode: RuntimeModeSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ import { HomeChatInput } from "@/components/chat/HomeChatInput";
|
|||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
import { PrivacyBanner } from "@/components/TelemetryBanner";
|
import { PrivacyBanner } from "@/components/TelemetryBanner";
|
||||||
import { INSPIRATION_PROMPTS } from "@/prompts/inspiration_prompts";
|
import { INSPIRATION_PROMPTS } from "@/prompts/inspiration_prompts";
|
||||||
|
import { useAppVersion } from "@/hooks/useAppVersion";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
||||||
@@ -21,11 +31,40 @@ export default function HomePage() {
|
|||||||
const search = useSearch({ from: "/" });
|
const search = useSearch({ from: "/" });
|
||||||
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
const setSelectedAppId = useSetAtom(selectedAppIdAtom);
|
||||||
const { refreshApps } = useLoadApps();
|
const { refreshApps } = useLoadApps();
|
||||||
const { settings, isAnyProviderSetup } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { streamMessage } = useStreamChat({ hasChatId: false });
|
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
|
const appVersion = useAppVersion();
|
||||||
|
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
|
||||||
|
const [releaseUrl, setReleaseUrl] = useState("");
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateLastVersionLaunched = async () => {
|
||||||
|
if (
|
||||||
|
appVersion &&
|
||||||
|
appVersion.match(/^\d+\.\d+\.\d+$/) &&
|
||||||
|
settings &&
|
||||||
|
settings.lastShownReleaseNotesVersion !== appVersion
|
||||||
|
) {
|
||||||
|
await updateSettings({
|
||||||
|
lastShownReleaseNotesVersion: appVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if release notes exist for this version
|
||||||
|
const url = `https://www.dyad.sh/docs/releases/${appVersion}`;
|
||||||
|
const exists = await checkPageExists(url);
|
||||||
|
if (exists) {
|
||||||
|
setReleaseUrl(url + "?hideHeader=true&theme=" + theme);
|
||||||
|
setReleaseNotesOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateLastVersionLaunched();
|
||||||
|
}, [appVersion, settings, updateSettings]);
|
||||||
|
|
||||||
// Get the appId from search params
|
// Get the appId from search params
|
||||||
const appId = search.appId ? Number(search.appId) : null;
|
const appId = search.appId ? Number(search.appId) : null;
|
||||||
|
|
||||||
@@ -165,6 +204,51 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PrivacyBanner />
|
<PrivacyBanner />
|
||||||
|
|
||||||
|
{/* Release Notes Dialog */}
|
||||||
|
<Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}>
|
||||||
|
<DialogContent className="max-w-4xl bg-(--docs-bg) pr-0 pt-4 pl-4 gap-1">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>What's new in v{appVersion}?</DialogTitle>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute right-10 top-2 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
releaseUrl.replace("?hideHeader=true&theme=" + theme, ""),
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="overflow-auto h-[70vh] flex flex-col ">
|
||||||
|
{releaseUrl && (
|
||||||
|
<div className="flex-1">
|
||||||
|
<iframe
|
||||||
|
src={releaseUrl}
|
||||||
|
className="w-full h-full border-0 rounded-lg"
|
||||||
|
title={`Release notes for v${appVersion}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkPageExists(url: string) {
|
||||||
|
return fetch(url, { mode: "no-cors" })
|
||||||
|
.then(() => {
|
||||||
|
// Promise resolved - resource likely exists
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Promise rejected - resource likely doesn't exist or network error
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { showSuccess, showError } from "@/lib/toast";
|
|||||||
import { AutoApproveSwitch } from "@/components/AutoApproveSwitch";
|
import { AutoApproveSwitch } from "@/components/AutoApproveSwitch";
|
||||||
import { TelemetrySwitch } from "@/components/TelemetrySwitch";
|
import { TelemetrySwitch } from "@/components/TelemetrySwitch";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
import { useAppVersion } from "@/hooks/useAppVersion";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
@@ -16,23 +17,10 @@ export default function SettingsPage() {
|
|||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
|
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
|
||||||
const [isResetting, setIsResetting] = useState(false);
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
const [appVersion, setAppVersion] = useState<string | null>(null);
|
const appVersion = useAppVersion();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Fetch app version
|
|
||||||
const fetchVersion = async () => {
|
|
||||||
try {
|
|
||||||
const version = await IpcClient.getInstance().getAppVersion();
|
|
||||||
setAppVersion(version);
|
|
||||||
} catch (error) {
|
|
||||||
setAppVersion(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchVersion();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleResetEverything = async () => {
|
const handleResetEverything = async () => {
|
||||||
setIsResetting(true);
|
setIsResetting(true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
:root {
|
:root {
|
||||||
|
--docs-bg: #f5f5f5;
|
||||||
|
|
||||||
--default-font-family: 'Geist', sans-serif;
|
--default-font-family: 'Geist', sans-serif;
|
||||||
--default-mono-font-family: 'Geist Mono', monospace;
|
--default-mono-font-family: 'Geist Mono', monospace;
|
||||||
@@ -120,6 +120,9 @@
|
|||||||
--tw-prose-pre-bg: var(--background);
|
--tw-prose-pre-bg: var(--background);
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
|
--docs-bg: #121212;
|
||||||
|
|
||||||
|
|
||||||
--background-lightest: oklch(0.285 0 0);
|
--background-lightest: oklch(0.285 0 0);
|
||||||
--background-lighter: oklch(0.185 0 0);
|
--background-lighter: oklch(0.185 0 0);
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.145 0 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user