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({
)}
size="sm"
>
{isDyadProEnabled ? "Pro" : "Pro (off)"}
{isDyadProEnabled
? userBudget?.isTrial
? "Pro Trial"
: "Pro"
: "Pro (off)"}
{userBudget && isDyadProEnabled && (
<AICreditStatus userBudget={userBudget} />
)}
......
......@@ -21,7 +21,7 @@ import { useLocalModels } from "@/hooks/useLocalModels";
import { useLocalLMSModels } from "@/hooks/useLMStudioModels";
import { useLanguageModelsByProviders } from "@/hooks/useLanguageModelsByProviders";
import { LocalModel } from "@/ipc/types";
import { ipc, LocalModel } from "@/ipc/types";
import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { useSettings } from "@/hooks/useSettings";
import { PriceBadge } from "@/components/PriceBadge";
......@@ -29,10 +29,12 @@ import { TURBO_MODELS } from "@/ipc/shared/language_model_constants";
import { cn } from "@/lib/utils";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
import { useTrialModelRestriction } from "@/hooks/useTrialModelRestriction";
export function ModelPicker() {
const { settings, updateSettings } = useSettings();
const queryClient = useQueryClient();
const { isTrial } = useTrialModelRestriction();
const onModelSelect = (model: LargeLanguageModel) => {
updateSettings({ selectedModel: model });
// Invalidate token count when model changes since different models have different context windows
......@@ -199,8 +201,49 @@ export function ModelPicker() {
<DropdownMenuLabel>Cloud Models</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Cloud models - loading state */}
{loading ? (
{/* Trial user upgrade banner */}
{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">
Loading models...
</div>
......@@ -323,7 +366,9 @@ export function ModelPicker() {
}
onClick={() => {
const customModelId =
model.type === "custom" ? model.id : undefined;
model.type === "custom"
? model.id
: undefined;
onModelSelect({
name: model.apiName,
provider: providerId,
......@@ -439,8 +484,11 @@ export function ModelPicker() {
</DropdownMenuSub>
)}
</>
)}
))}
{/* Local Models - only show for non-trial users */}
{!isTrial && (
<>
<DropdownMenuSeparator />
{/* Local Models Parent SubMenu */}
<DropdownMenuSub>
......@@ -466,7 +514,9 @@ export function ModelPicker() {
Loading...
</span>
) : ollamaError ? (
<span className="text-xs text-red-500">Error loading</span>
<span className="text-xs text-red-500">
Error loading
</span>
) : !hasOllamaModels ? (
<span className="text-xs text-muted-foreground">
None available
......@@ -547,7 +597,9 @@ export function ModelPicker() {
Loading...
</span>
) : lmStudioError ? (
<span className="text-xs text-red-500">Error loading</span>
<span className="text-xs text-red-500">
Error loading
</span>
) : !hasLMStudioModels ? (
<span className="text-xs text-muted-foreground">
None available
......@@ -572,7 +624,8 @@ export function ModelPicker() {
<div className="flex flex-col">
<span>Error loading models</span>
<span className="text-xs text-muted-foreground">
{lmStudioError.message} {/* Display specific error */}
{lmStudioError.message}{" "}
{/* Display specific error */}
</span>
</div>
</div>
......@@ -618,6 +671,8 @@ export function ModelPicker() {
</DropdownMenuSub>
</DropdownMenuSubContent>
</DropdownMenuSub>
</>
)}
</DropdownMenuContent>
</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({
totalCredits: z.number(),
budgetResetDate: z.string(), // ISO date string from API
userId: z.string(),
isTrial: z.boolean().optional().default(false),
});
export type UserInfoResponse = z.infer<typeof UserInfoResponseSchema>;
......@@ -30,6 +31,7 @@ export function registerProHandlers() {
totalCredits: 1000,
budgetResetDate: resetDate,
redactedUserId: "<redacted-user-id-testing>",
isTrial: false,
};
}
logger.info("Attempting to fetch user budget information.");
......@@ -83,6 +85,7 @@ export function registerProHandlers() {
totalCredits: data.totalCredits,
budgetResetDate: new Date(data.budgetResetDate),
redactedUserId: redactedUserId,
isTrial: data.isTrial,
});
return userBudgetInfo;
......
......@@ -73,6 +73,7 @@ export const UserBudgetInfoSchema = z
totalCredits: z.number(),
budgetResetDate: z.date(),
redactedUserId: z.string(),
isTrial: z.boolean(),
})
.nullable();
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论