Unverified 提交 bb0f30b0 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Pro Trial models (#2387)

<!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2387"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Restricts Dyad Pro trial users to the Auto model and adds an upgrade call-to-action. Non-trial users see the full set of cloud and local models as before. - **New Features** - Added useTrialModelRestriction hook to detect trial status and auto-switch to the Auto model. - Updated ModelPicker to show an upgrade banner and only the Auto model for trial users; hides cloud and local models. - Extended IPC user budget response and schema to include isTrial. - Updated TitleBar to show "Pro Trial" when applicable. <sup>Written for commit 714d2c704f155c004240e563e0e850ace0c9f5f8. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces trial-based gating in the model picker and an effect that can automatically change a user’s selected model, which could impact UX if trial detection is wrong or delayed. > > **Overview** > **Trial enforcement for model selection.** Adds `useTrialModelRestriction` to derive `isTrial` from `get-user-budget` and auto-switch trial users to the `auto` model. > > **Model picker gating + upgrade CTA.** Updates `ModelPicker` to show an upgrade banner (opening the subscription URL via `ipc.system.openExternalUrl`) and to hide all cloud/local model choices for trial users, leaving only the `auto` option. > > **IPC/schema update.** Extends user budget IPC types and `pro_handlers` API parsing to include an `isTrial` flag (defaulting to `false` when absent). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7928275e3b1a7d32ec792609968d9d5786ee8582. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
上级 58b9b1d4
...@@ -216,7 +216,11 @@ export function DyadProButton({ ...@@ -216,7 +216,11 @@ export function DyadProButton({
)} )}
size="sm" size="sm"
> >
{isDyadProEnabled ? "Pro" : "Pro (off)"} {isDyadProEnabled
? userBudget?.isTrial
? "Pro Trial"
: "Pro"
: "Pro (off)"}
{userBudget && isDyadProEnabled && ( {userBudget && isDyadProEnabled && (
<AICreditStatus userBudget={userBudget} /> <AICreditStatus userBudget={userBudget} />
)} )}
......
...@@ -21,7 +21,7 @@ import { useLocalModels } from "@/hooks/useLocalModels"; ...@@ -21,7 +21,7 @@ import { useLocalModels } from "@/hooks/useLocalModels";
import { useLocalLMSModels } from "@/hooks/useLMStudioModels"; import { useLocalLMSModels } from "@/hooks/useLMStudioModels";
import { useLanguageModelsByProviders } from "@/hooks/useLanguageModelsByProviders"; import { useLanguageModelsByProviders } from "@/hooks/useLanguageModelsByProviders";
import { LocalModel } from "@/ipc/types"; import { ipc, LocalModel } from "@/ipc/types";
import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { PriceBadge } from "@/components/PriceBadge"; import { PriceBadge } from "@/components/PriceBadge";
...@@ -29,10 +29,12 @@ import { TURBO_MODELS } from "@/ipc/shared/language_model_constants"; ...@@ -29,10 +29,12 @@ import { TURBO_MODELS } from "@/ipc/shared/language_model_constants";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys"; import { queryKeys } from "@/lib/queryKeys";
import { useTrialModelRestriction } from "@/hooks/useTrialModelRestriction";
export function ModelPicker() { export function ModelPicker() {
const { settings, updateSettings } = useSettings(); const { settings, updateSettings } = useSettings();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { isTrial } = useTrialModelRestriction();
const onModelSelect = (model: LargeLanguageModel) => { const onModelSelect = (model: LargeLanguageModel) => {
updateSettings({ selectedModel: model }); updateSettings({ selectedModel: model });
// Invalidate token count when model changes since different models have different context windows // Invalidate token count when model changes since different models have different context windows
...@@ -199,8 +201,49 @@ export function ModelPicker() { ...@@ -199,8 +201,49 @@ export function ModelPicker() {
<DropdownMenuLabel>Cloud Models</DropdownMenuLabel> <DropdownMenuLabel>Cloud Models</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{/* Cloud models - loading state */} {/* Trial user upgrade banner */}
{loading ? ( {isTrial && (
<>
<div className="px-2 py-3 bg-gradient-to-r from-indigo-50 to-sky-50 dark:from-indigo-950/50 dark:to-sky-950/50">
<p className="text-sm text-indigo-700 dark:text-indigo-300 mb-2">
Upgrade from Dyad Pro trial to unlock more models.
</p>
<Button
variant="outline"
size="sm"
className="cursor-pointer w-full bg-indigo-600 hover:bg-indigo-700 text-white hover:text-white border-indigo-600"
onClick={() => {
ipc.system.openExternalUrl(
"https://academy.dyad.sh/subscription",
);
setOpen(false);
}}
>
Upgrade to Dyad Pro
</Button>
</div>
<DropdownMenuSeparator />
{/* Trial users only see the auto model */}
<DropdownMenuItem
className="bg-secondary"
onClick={() => {
onModelSelect({ name: "auto", provider: "auto" });
setOpen(false);
}}
>
<div className="flex justify-between items-start w-full">
<span>Auto</span>
<span className="text-[11px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium">
Trial
</span>
</div>
</DropdownMenuItem>
</>
)}
{/* Cloud models - only show for non-trial users */}
{!isTrial &&
(loading ? (
<div className="text-xs text-center py-2 text-muted-foreground"> <div className="text-xs text-center py-2 text-muted-foreground">
Loading models... Loading models...
</div> </div>
...@@ -323,7 +366,9 @@ export function ModelPicker() { ...@@ -323,7 +366,9 @@ export function ModelPicker() {
} }
onClick={() => { onClick={() => {
const customModelId = const customModelId =
model.type === "custom" ? model.id : undefined; model.type === "custom"
? model.id
: undefined;
onModelSelect({ onModelSelect({
name: model.apiName, name: model.apiName,
provider: providerId, provider: providerId,
...@@ -439,8 +484,11 @@ export function ModelPicker() { ...@@ -439,8 +484,11 @@ export function ModelPicker() {
</DropdownMenuSub> </DropdownMenuSub>
)} )}
</> </>
)} ))}
{/* Local Models - only show for non-trial users */}
{!isTrial && (
<>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{/* Local Models Parent SubMenu */} {/* Local Models Parent SubMenu */}
<DropdownMenuSub> <DropdownMenuSub>
...@@ -466,7 +514,9 @@ export function ModelPicker() { ...@@ -466,7 +514,9 @@ export function ModelPicker() {
Loading... Loading...
</span> </span>
) : ollamaError ? ( ) : ollamaError ? (
<span className="text-xs text-red-500">Error loading</span> <span className="text-xs text-red-500">
Error loading
</span>
) : !hasOllamaModels ? ( ) : !hasOllamaModels ? (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
None available None available
...@@ -547,7 +597,9 @@ export function ModelPicker() { ...@@ -547,7 +597,9 @@ export function ModelPicker() {
Loading... Loading...
</span> </span>
) : lmStudioError ? ( ) : lmStudioError ? (
<span className="text-xs text-red-500">Error loading</span> <span className="text-xs text-red-500">
Error loading
</span>
) : !hasLMStudioModels ? ( ) : !hasLMStudioModels ? (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
None available None available
...@@ -572,7 +624,8 @@ export function ModelPicker() { ...@@ -572,7 +624,8 @@ export function ModelPicker() {
<div className="flex flex-col"> <div className="flex flex-col">
<span>Error loading models</span> <span>Error loading models</span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{lmStudioError.message} {/* Display specific error */} {lmStudioError.message}{" "}
{/* Display specific error */}
</span> </span>
</div> </div>
</div> </div>
...@@ -618,6 +671,8 @@ export function ModelPicker() { ...@@ -618,6 +671,8 @@ export function ModelPicker() {
</DropdownMenuSub> </DropdownMenuSub>
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
</>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
......
import { useEffect } from "react";
import { useUserBudgetInfo } from "./useUserBudgetInfo";
import { useSettings } from "./useSettings";
import { isDyadProEnabled } from "../lib/schemas";
const AUTO_MODEL = { name: "auto", provider: "auto" };
export function useTrialModelRestriction() {
const { userBudget, isLoadingUserBudget } = useUserBudgetInfo();
const { settings, updateSettings } = useSettings();
const isTrial =
(userBudget?.isTrial && settings && isDyadProEnabled(settings)) ?? false;
const isOnAutoModel =
settings?.selectedModel?.provider === "auto" &&
settings?.selectedModel?.name === "auto";
// Auto-switch to auto model if user is on trial and not already on auto
useEffect(() => {
if (isTrial && settings && !isOnAutoModel && !isLoadingUserBudget) {
updateSettings({ selectedModel: AUTO_MODEL });
}
}, [isTrial, isOnAutoModel, isLoadingUserBudget, settings, updateSettings]);
return {
isTrial,
isLoadingTrialStatus: isLoadingUserBudget,
};
}
...@@ -11,6 +11,7 @@ export const UserInfoResponseSchema = z.object({ ...@@ -11,6 +11,7 @@ export const UserInfoResponseSchema = z.object({
totalCredits: z.number(), totalCredits: z.number(),
budgetResetDate: z.string(), // ISO date string from API budgetResetDate: z.string(), // ISO date string from API
userId: z.string(), userId: z.string(),
isTrial: z.boolean().optional().default(false),
}); });
export type UserInfoResponse = z.infer<typeof UserInfoResponseSchema>; export type UserInfoResponse = z.infer<typeof UserInfoResponseSchema>;
...@@ -30,6 +31,7 @@ export function registerProHandlers() { ...@@ -30,6 +31,7 @@ export function registerProHandlers() {
totalCredits: 1000, totalCredits: 1000,
budgetResetDate: resetDate, budgetResetDate: resetDate,
redactedUserId: "<redacted-user-id-testing>", redactedUserId: "<redacted-user-id-testing>",
isTrial: false,
}; };
} }
logger.info("Attempting to fetch user budget information."); logger.info("Attempting to fetch user budget information.");
...@@ -83,6 +85,7 @@ export function registerProHandlers() { ...@@ -83,6 +85,7 @@ export function registerProHandlers() {
totalCredits: data.totalCredits, totalCredits: data.totalCredits,
budgetResetDate: new Date(data.budgetResetDate), budgetResetDate: new Date(data.budgetResetDate),
redactedUserId: redactedUserId, redactedUserId: redactedUserId,
isTrial: data.isTrial,
}); });
return userBudgetInfo; return userBudgetInfo;
......
...@@ -73,6 +73,7 @@ export const UserBudgetInfoSchema = z ...@@ -73,6 +73,7 @@ export const UserBudgetInfoSchema = z
totalCredits: z.number(), totalCredits: z.number(),
budgetResetDate: z.date(), budgetResetDate: z.date(),
redactedUserId: z.string(), redactedUserId: z.string(),
isTrial: z.boolean(),
}) })
.nullable(); .nullable();
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论