@@ -13,6 +13,13 @@ import { useEffect, useState } from "react";
|
|||||||
import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog";
|
import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
|
||||||
|
import { UserBudgetInfo } from "@/ipc/ipc_types";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export const TitleBar = () => {
|
export const TitleBar = () => {
|
||||||
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
const [selectedAppId] = useAtom(selectedAppIdAtom);
|
||||||
@@ -64,7 +71,7 @@ export const TitleBar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isDyadPro = !!settings?.providerSettings?.auto?.apiKey?.value;
|
const isDyadPro = !!settings?.providerSettings?.auto?.apiKey?.value;
|
||||||
const isDyadProEnabled = settings?.enableDyadPro;
|
const isDyadProEnabled = Boolean(settings?.enableDyadPro);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -82,25 +89,7 @@ export const TitleBar = () => {
|
|||||||
>
|
>
|
||||||
{displayText}
|
{displayText}
|
||||||
</Button>
|
</Button>
|
||||||
{isDyadPro && (
|
{isDyadPro && <DyadProButton isDyadProEnabled={isDyadProEnabled} />}
|
||||||
<Button
|
|
||||||
data-testid="title-bar-dyad-pro-button"
|
|
||||||
onClick={() => {
|
|
||||||
navigate({
|
|
||||||
to: providerSettingsRoute.id,
|
|
||||||
params: { provider: "auto" },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
variant="outline"
|
|
||||||
className={cn(
|
|
||||||
"ml-4 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white",
|
|
||||||
!isDyadProEnabled && "bg-zinc-600 dark:bg-zinc-600",
|
|
||||||
)}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{showWindowControls && <WindowsControls />}
|
{showWindowControls && <WindowsControls />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,3 +181,58 @@ function WindowsControls() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DyadProButton({
|
||||||
|
isDyadProEnabled,
|
||||||
|
}: {
|
||||||
|
isDyadProEnabled: boolean;
|
||||||
|
}) {
|
||||||
|
const { navigate } = useRouter();
|
||||||
|
const { userBudget } = useUserBudgetInfo();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-testid="title-bar-dyad-pro-button"
|
||||||
|
onClick={() => {
|
||||||
|
navigate({
|
||||||
|
to: providerSettingsRoute.id,
|
||||||
|
params: { provider: "auto" },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"ml-4 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white",
|
||||||
|
!isDyadProEnabled && "bg-zinc-600 dark:bg-zinc-600",
|
||||||
|
)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"}
|
||||||
|
{userBudget && <AICreditStatus userBudget={userBudget} />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AICreditStatus({ userBudget }: { userBudget: UserBudgetInfo }) {
|
||||||
|
const remaining = Math.round(
|
||||||
|
userBudget.totalCredits - userBudget.usedCredits,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="text-xs mt-0.5">{remaining} credits left</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
You have used {Math.round(userBudget.usedCredits)} credits out of{" "}
|
||||||
|
{userBudget.totalCredits}.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Your budget resets on{" "}
|
||||||
|
{userBudget.budgetResetDate.toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
<p>Note: there is a slight delay in updating the credit status.</p>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useProposal } from "./useProposal";
|
|||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { useRunApp } from "./useRunApp";
|
import { useRunApp } from "./useRunApp";
|
||||||
import { useCountTokens } from "./useCountTokens";
|
import { useCountTokens } from "./useCountTokens";
|
||||||
|
import { useUserBudgetInfo } from "./useUserBudgetInfo";
|
||||||
|
|
||||||
export function getRandomNumberId() {
|
export function getRandomNumberId() {
|
||||||
return Math.floor(Math.random() * 1_000_000_000_000_000);
|
return Math.floor(Math.random() * 1_000_000_000_000_000);
|
||||||
@@ -38,6 +39,7 @@ export function useStreamChat({
|
|||||||
const { refreshVersions } = useVersions(selectedAppId);
|
const { refreshVersions } = useVersions(selectedAppId);
|
||||||
const { refreshAppIframe } = useRunApp();
|
const { refreshAppIframe } = useRunApp();
|
||||||
const { countTokens } = useCountTokens();
|
const { countTokens } = useCountTokens();
|
||||||
|
const { refetchUserBudget } = useUserBudgetInfo();
|
||||||
|
|
||||||
let chatId: number | undefined;
|
let chatId: number | undefined;
|
||||||
|
|
||||||
@@ -95,6 +97,8 @@ export function useStreamChat({
|
|||||||
}
|
}
|
||||||
refreshProposal(chatId);
|
refreshProposal(chatId);
|
||||||
|
|
||||||
|
refetchUserBudget();
|
||||||
|
|
||||||
// Keep the same as below
|
// Keep the same as below
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
refreshChats();
|
refreshChats();
|
||||||
@@ -120,7 +124,7 @@ export function useStreamChat({
|
|||||||
setError(error instanceof Error ? error.message : String(error));
|
setError(error instanceof Error ? error.message : String(error));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setMessages, setIsStreaming, setIsPreviewOpen],
|
[setMessages, setIsStreaming, setIsPreviewOpen, refetchUserBudget],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
34
src/hooks/useUserBudgetInfo.ts
Normal file
34
src/hooks/useUserBudgetInfo.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import type { UserBudgetInfo } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
|
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
export function useUserBudgetInfo() {
|
||||||
|
const queryKey = ["userBudgetInfo"];
|
||||||
|
|
||||||
|
const { data, isLoading, error, isFetching, refetch } = useQuery<
|
||||||
|
UserBudgetInfo | null,
|
||||||
|
Error,
|
||||||
|
UserBudgetInfo | null
|
||||||
|
>({
|
||||||
|
queryKey: queryKey,
|
||||||
|
queryFn: async () => {
|
||||||
|
const ipcClient = IpcClient.getInstance();
|
||||||
|
return ipcClient.getUserBudget();
|
||||||
|
},
|
||||||
|
// This data is not critical and can be stale for a bit
|
||||||
|
staleTime: FIVE_MINUTES_IN_MS,
|
||||||
|
// If an error occurs (e.g. API key not set), it returns null.
|
||||||
|
// We don't want react-query to retry automatically in such cases as it's not a transient network error.
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userBudget: data,
|
||||||
|
isLoadingUserBudget: isLoading,
|
||||||
|
userBudgetError: error,
|
||||||
|
isFetchingUserBudget: isFetching,
|
||||||
|
refetchUserBudget: refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
61
src/ipc/handlers/pro_handlers.ts
Normal file
61
src/ipc/handlers/pro_handlers.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import fetch from "node-fetch"; // Electron main process might need node-fetch
|
||||||
|
import log from "electron-log";
|
||||||
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
|
import { readSettings } from "../../main/settings"; // Assuming settings are read this way
|
||||||
|
import { UserBudgetInfoSchema } from "../ipc_types";
|
||||||
|
|
||||||
|
const logger = log.scope("pro_handlers");
|
||||||
|
const handle = createLoggedHandler(logger);
|
||||||
|
|
||||||
|
const CONVERSION_RATIO = (10 * 3) / 2;
|
||||||
|
|
||||||
|
export function registerProHandlers() {
|
||||||
|
// This method should try to avoid throwing errors because this is auxiliary
|
||||||
|
// information and isn't critical to using the app
|
||||||
|
handle("get-user-budget", async (): Promise<UserBudgetInfo | null> => {
|
||||||
|
logger.info("Attempting to fetch user budget information.");
|
||||||
|
|
||||||
|
const settings = readSettings();
|
||||||
|
|
||||||
|
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
logger.error("LLM Gateway API key (Dyad Pro) is not configured.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = "https://llm-gateway.dyad.sh/user/info";
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use native fetch if available, otherwise node-fetch will be used via import
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorBody = await response.text();
|
||||||
|
logger.error(
|
||||||
|
`Failed to fetch user budget. Status: ${response.status}. Body: ${errorBody}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const userInfoData = data["user_info"];
|
||||||
|
logger.info("Successfully fetched user budget information.");
|
||||||
|
return UserBudgetInfoSchema.parse({
|
||||||
|
usedCredits: userInfoData["spend"] * CONVERSION_RATIO,
|
||||||
|
totalCredits: userInfoData["max_budget"] * CONVERSION_RATIO,
|
||||||
|
budgetResetDate: new Date(userInfoData["budget_reset_at"]),
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Error fetching user budget: ${error.message}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import type {
|
|||||||
ImportAppResult,
|
ImportAppResult,
|
||||||
ImportAppParams,
|
ImportAppParams,
|
||||||
RenameBranchParams,
|
RenameBranchParams,
|
||||||
|
UserBudgetInfo,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { ProposalResult } from "@/lib/schemas";
|
import type { ProposalResult } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
@@ -825,4 +826,9 @@ export class IpcClient {
|
|||||||
async clearSessionData(): Promise<void> {
|
async clearSessionData(): Promise<void> {
|
||||||
return this.ipcRenderer.invoke("clear-session-data");
|
return this.ipcRenderer.invoke("clear-session-data");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to get user budget information
|
||||||
|
public async getUserBudget(): Promise<UserBudgetInfo | null> {
|
||||||
|
return this.ipcRenderer.invoke("get-user-budget");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { registerLanguageModelHandlers } from "./handlers/language_model_handler
|
|||||||
import { registerReleaseNoteHandlers } from "./handlers/release_note_handlers";
|
import { registerReleaseNoteHandlers } from "./handlers/release_note_handlers";
|
||||||
import { registerImportHandlers } from "./handlers/import_handlers";
|
import { registerImportHandlers } from "./handlers/import_handlers";
|
||||||
import { registerSessionHandlers } from "./handlers/session_handlers";
|
import { registerSessionHandlers } from "./handlers/session_handlers";
|
||||||
|
import { registerProHandlers } from "./handlers/pro_handlers";
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
@@ -41,4 +42,5 @@ export function registerIpcHandlers() {
|
|||||||
registerReleaseNoteHandlers();
|
registerReleaseNoteHandlers();
|
||||||
registerImportHandlers();
|
registerImportHandlers();
|
||||||
registerSessionHandlers();
|
registerSessionHandlers();
|
||||||
|
registerProHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export interface AppOutput {
|
export interface AppOutput {
|
||||||
type: "stdout" | "stderr" | "info" | "client-error";
|
type: "stdout" | "stderr" | "info" | "client-error";
|
||||||
message: string;
|
message: string;
|
||||||
@@ -207,3 +209,10 @@ export interface RenameBranchParams {
|
|||||||
oldBranchName: string;
|
oldBranchName: string;
|
||||||
newBranchName: string;
|
newBranchName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UserBudgetInfoSchema = z.object({
|
||||||
|
usedCredits: z.number(),
|
||||||
|
totalCredits: z.number(),
|
||||||
|
budgetResetDate: z.date(),
|
||||||
|
});
|
||||||
|
export type UserBudgetInfo = z.infer<typeof UserBudgetInfoSchema>;
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const validInvokeChannels = [
|
|||||||
"check-app-name",
|
"check-app-name",
|
||||||
"rename-branch",
|
"rename-branch",
|
||||||
"clear-session-data",
|
"clear-session-data",
|
||||||
|
"get-user-budget",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Add valid receive channels
|
// Add valid receive channels
|
||||||
|
|||||||
Reference in New Issue
Block a user