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,425 +201,478 @@ export function ModelPicker() { ...@@ -199,425 +201,478 @@ 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="text-xs text-center py-2 text-muted-foreground">
Loading models...
</div>
) : !modelsByProviders ||
Object.keys(modelsByProviders).length === 0 ? (
<div className="text-xs text-center py-2 text-muted-foreground">
No cloud models available
</div>
) : (
/* Cloud models loaded */
<> <>
{/* Auto models at top level if any */} <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">
{autoModels.length > 0 && ( <p className="text-sm text-indigo-700 dark:text-indigo-300 mb-2">
<> Upgrade from Dyad Pro trial to unlock more models.
{autoModels.map((model) => ( </p>
<Tooltip key={`auto-${model.apiName}`}> <Button
<TooltipTrigger asChild> variant="outline"
<DropdownMenuItem size="sm"
className={ className="cursor-pointer w-full bg-indigo-600 hover:bg-indigo-700 text-white hover:text-white border-indigo-600"
selectedModel.provider === "auto" && onClick={() => {
selectedModel.name === model.apiName ipc.system.openExternalUrl(
? "bg-secondary" "https://academy.dyad.sh/subscription",
: "" );
} setOpen(false);
onClick={() => { }}
onModelSelect({ >
name: model.apiName, Upgrade to Dyad Pro
provider: "auto", </Button>
}); </div>
setOpen(false); <DropdownMenuSeparator />
}} {/* Trial users only see the auto model */}
> <DropdownMenuItem
<div className="flex justify-between items-start w-full"> className="bg-secondary"
<span className="flex flex-col items-start"> onClick={() => {
<span>{model.displayName}</span> onModelSelect({ name: "auto", provider: "auto" });
</span> setOpen(false);
<div className="flex items-center gap-1.5"> }}
{model.tag && ( >
<span <div className="flex justify-between items-start w-full">
className={cn( <span>Auto</span>
"text-[11px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium", <span className="text-[11px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium">
model.tagColor, Trial
)} </span>
> </div>
{model.tag} </DropdownMenuItem>
</span> </>
)} )}
{/* Cloud models - only show for non-trial users */}
{!isTrial &&
(loading ? (
<div className="text-xs text-center py-2 text-muted-foreground">
Loading models...
</div>
) : !modelsByProviders ||
Object.keys(modelsByProviders).length === 0 ? (
<div className="text-xs text-center py-2 text-muted-foreground">
No cloud models available
</div>
) : (
/* Cloud models loaded */
<>
{/* Auto models at top level if any */}
{autoModels.length > 0 && (
<>
{autoModels.map((model) => (
<Tooltip key={`auto-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
className={
selectedModel.provider === "auto" &&
selectedModel.name === model.apiName
? "bg-secondary"
: ""
}
onClick={() => {
onModelSelect({
name: model.apiName,
provider: "auto",
});
setOpen(false);
}}
>
<div className="flex justify-between items-start w-full">
<span className="flex flex-col items-start">
<span>{model.displayName}</span>
</span>
<div className="flex items-center gap-1.5">
{model.tag && (
<span
className={cn(
"text-[11px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium",
model.tagColor,
)}
>
{model.tag}
</span>
)}
</div>
</div> </div>
</div> </DropdownMenuItem>
</DropdownMenuItem> </TooltipTrigger>
</TooltipTrigger> <TooltipContent side="right">
<TooltipContent side="right"> {model.description}
{model.description} </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> ))}
))} {Object.keys(modelsByProviders).length > 1 && (
{Object.keys(modelsByProviders).length > 1 && ( <DropdownMenuSeparator />
<DropdownMenuSeparator /> )}
)} </>
</> )}
)}
{/* Primary providers as submenus */} {/* Primary providers as submenus */}
{primaryProviders.map(([providerId, models]) => { {primaryProviders.map(([providerId, models]) => {
models = models.filter((model) => { models = models.filter((model) => {
// Don't show free models if Dyad Pro is enabled because // Don't show free models if Dyad Pro is enabled because
// we will use the paid models (in Dyad Pro backend) which // we will use the paid models (in Dyad Pro backend) which
// don't have the free limitations. // don't have the free limitations.
if ( if (
isDyadProEnabled(settings) && isDyadProEnabled(settings) &&
model.apiName.endsWith(":free") model.apiName.endsWith(":free")
) { ) {
return false; return false;
} }
return true; return true;
}); });
const provider = providers?.find((p) => p.id === providerId); const provider = providers?.find((p) => p.id === providerId);
const providerDisplayName = const providerDisplayName =
provider?.id === "auto" provider?.id === "auto"
? "Dyad Turbo" ? "Dyad Turbo"
: (provider?.name ?? providerId); : (provider?.name ?? providerId);
return ( return (
<DropdownMenuSub key={providerId}> <DropdownMenuSub key={providerId}>
<DropdownMenuSubTrigger className="w-full font-normal"> <DropdownMenuSubTrigger className="w-full font-normal">
<div className="flex flex-col items-start w-full"> <div className="flex flex-col items-start w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{providerDisplayName}</span> <span>{providerDisplayName}</span>
{provider?.type === "cloud" && {provider?.type === "cloud" &&
!provider?.secondary && !provider?.secondary &&
isDyadProEnabled(settings) && ( isDyadProEnabled(settings) && (
<span className="text-[10px] bg-gradient-to-r from-indigo-600 via-indigo-500 to-indigo-600 bg-[length:200%_100%] animate-[shimmer_5s_ease-in-out_infinite] text-white px-1.5 py-0.5 rounded-full font-medium"> <span className="text-[10px] bg-gradient-to-r from-indigo-600 via-indigo-500 to-indigo-600 bg-[length:200%_100%] animate-[shimmer_5s_ease-in-out_infinite] text-white px-1.5 py-0.5 rounded-full font-medium">
Pro Pro
</span>
)}
{provider?.type === "custom" && (
<span className="text-[10px] bg-amber-500/20 text-amber-700 px-1.5 py-0.5 rounded-full font-medium">
Custom
</span> </span>
)} )}
{provider?.type === "custom" && ( </div>
<span className="text-[10px] bg-amber-500/20 text-amber-700 px-1.5 py-0.5 rounded-full font-medium"> <span className="text-xs text-muted-foreground">
Custom {models.length} models
</span> </span>
)}
</div> </div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto">
<DropdownMenuLabel>
{providerDisplayName + " Models"}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
className={
selectedModel.provider === providerId &&
selectedModel.name === model.apiName
? "bg-secondary"
: ""
}
onClick={() => {
const customModelId =
model.type === "custom"
? model.id
: undefined;
onModelSelect({
name: model.apiName,
provider: providerId,
customModelId,
});
setOpen(false);
}}
>
<div className="flex justify-between items-start w-full">
<span>{model.displayName}</span>
<PriceBadge dollarSigns={model.dollarSigns} />
{model.tag && (
<span className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium">
{model.tag}
</span>
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
);
})}
{/* Secondary providers grouped under Other AI providers */}
{secondaryProviders.length > 0 && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="w-full font-normal">
<div className="flex flex-col items-start">
<span>Other AI providers</span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{models.length} models {secondaryProviders.length} providers
</span> </span>
</div> </div>
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto"> <DropdownMenuSubContent className="w-56">
<DropdownMenuLabel> <DropdownMenuLabel>Other AI providers</DropdownMenuLabel>
{providerDisplayName + " Models"}
</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{models.map((model) => ( {secondaryProviders.map(([providerId, models]) => {
<Tooltip key={`${providerId}-${model.apiName}`}> const provider = providers?.find(
<TooltipTrigger asChild> (p) => p.id === providerId,
<DropdownMenuItem );
className={ return (
selectedModel.provider === providerId && <DropdownMenuSub key={providerId}>
selectedModel.name === model.apiName <DropdownMenuSubTrigger className="w-full font-normal">
? "bg-secondary" <div className="flex flex-col items-start w-full">
: "" <div className="flex items-center gap-2">
} <span>{provider?.name ?? providerId}</span>
onClick={() => { {provider?.type === "custom" && (
const customModelId = <span className="text-[10px] bg-amber-500/20 text-amber-700 px-1.5 py-0.5 rounded-full font-medium">
model.type === "custom" ? model.id : undefined; Custom
onModelSelect({ </span>
name: model.apiName, )}
provider: providerId, </div>
customModelId, <span className="text-xs text-muted-foreground">
}); {models.length} models
setOpen(false); </span>
}}
>
<div className="flex justify-between items-start w-full">
<span>{model.displayName}</span>
<PriceBadge dollarSigns={model.dollarSigns} />
{model.tag && (
<span className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium">
{model.tag}
</span>
)}
</div> </div>
</DropdownMenuItem> </DropdownMenuSubTrigger>
</TooltipTrigger> <DropdownMenuSubContent className="w-56">
<TooltipContent side="right"> <DropdownMenuLabel>
{model.description} {(provider?.name ?? providerId) + " Models"}
</TooltipContent> </DropdownMenuLabel>
</Tooltip> <DropdownMenuSeparator />
))} {models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
className={
selectedModel.provider === providerId &&
selectedModel.name === model.apiName
? "bg-secondary"
: ""
}
onClick={() => {
const customModelId =
model.type === "custom"
? model.id
: undefined;
onModelSelect({
name: model.apiName,
provider: providerId,
customModelId,
});
setOpen(false);
}}
>
<div className="flex justify-between items-start w-full">
<span>{model.displayName}</span>
{model.tag && (
<span className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium">
{model.tag}
</span>
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
);
})}
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
); )}
})} </>
))}
{/* Secondary providers grouped under Other AI providers */}
{secondaryProviders.length > 0 && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="w-full font-normal">
<div className="flex flex-col items-start">
<span>Other AI providers</span>
<span className="text-xs text-muted-foreground">
{secondaryProviders.length} providers
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
<DropdownMenuLabel>Other AI providers</DropdownMenuLabel>
<DropdownMenuSeparator />
{secondaryProviders.map(([providerId, models]) => {
const provider = providers?.find(
(p) => p.id === providerId,
);
return (
<DropdownMenuSub key={providerId}>
<DropdownMenuSubTrigger className="w-full font-normal">
<div className="flex flex-col items-start w-full">
<div className="flex items-center gap-2">
<span>{provider?.name ?? providerId}</span>
{provider?.type === "custom" && (
<span className="text-[10px] bg-amber-500/20 text-amber-700 px-1.5 py-0.5 rounded-full font-medium">
Custom
</span>
)}
</div>
<span className="text-xs text-muted-foreground">
{models.length} models
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
<DropdownMenuLabel>
{(provider?.name ?? providerId) + " Models"}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{models.map((model) => (
<Tooltip key={`${providerId}-${model.apiName}`}>
<TooltipTrigger asChild>
<DropdownMenuItem
className={
selectedModel.provider === providerId &&
selectedModel.name === model.apiName
? "bg-secondary"
: ""
}
onClick={() => {
const customModelId =
model.type === "custom"
? model.id
: undefined;
onModelSelect({
name: model.apiName,
provider: providerId,
customModelId,
});
setOpen(false);
}}
>
<div className="flex justify-between items-start w-full">
<span>{model.displayName}</span>
{model.tag && (
<span className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded-full font-medium">
{model.tag}
</span>
)}
</div>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
);
})}
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
</>
)}
<DropdownMenuSeparator /> {/* Local Models - only show for non-trial users */}
{/* Local Models Parent SubMenu */} {!isTrial && (
<DropdownMenuSub> <>
<DropdownMenuSubTrigger className="w-full font-normal"> <DropdownMenuSeparator />
<div className="flex flex-col items-start"> {/* Local Models Parent SubMenu */}
<span>Local models</span>
<span className="text-xs text-muted-foreground">
LM Studio, Ollama
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
{/* Ollama Models SubMenu */}
<DropdownMenuSub> <DropdownMenuSub>
<DropdownMenuSubTrigger <DropdownMenuSubTrigger className="w-full font-normal">
disabled={ollamaLoading && !hasOllamaModels} // Disable if loading and no models yet
className="w-full font-normal"
>
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<span>Ollama</span> <span>Local models</span>
{ollamaLoading ? ( <span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground"> LM Studio, Ollama
Loading... </span>
</span>
) : ollamaError ? (
<span className="text-xs text-red-500">Error loading</span>
) : !hasOllamaModels ? (
<span className="text-xs text-muted-foreground">
None available
</span>
) : (
<span className="text-xs text-muted-foreground">
{ollamaModels.length} models
</span>
)}
</div> </div>
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto"> <DropdownMenuSubContent className="w-56">
<DropdownMenuLabel>Ollama Models</DropdownMenuLabel> {/* Ollama Models SubMenu */}
<DropdownMenuSeparator /> <DropdownMenuSub>
<DropdownMenuSubTrigger
{ollamaLoading && ollamaModels.length === 0 ? ( // Show loading only if no models are loaded yet disabled={ollamaLoading && !hasOllamaModels} // Disable if loading and no models yet
<div className="text-xs text-center py-2 text-muted-foreground"> className="w-full font-normal"
Loading models... >
</div> <div className="flex flex-col items-start">
) : ollamaError ? ( <span>Ollama</span>
<div className="px-2 py-1.5 text-sm text-red-600"> {ollamaLoading ? (
<div className="flex flex-col"> <span className="text-xs text-muted-foreground">
<span>Error loading models</span> Loading...
<span className="text-xs text-muted-foreground">
Is Ollama running?
</span>
</div>
</div>
) : !hasOllamaModels ? (
<div className="px-2 py-1.5 text-sm">
<div className="flex flex-col">
<span>No local models found</span>
<span className="text-xs text-muted-foreground">
Ensure Ollama is running and models are pulled.
</span>
</div>
</div>
) : (
ollamaModels.map((model: LocalModel) => (
<DropdownMenuItem
key={`ollama-${model.modelName}`}
className={
selectedModel.provider === "ollama" &&
selectedModel.name === model.modelName
? "bg-secondary"
: ""
}
onClick={() => {
onModelSelect({
name: model.modelName,
provider: "ollama",
});
setOpen(false);
}}
>
<div className="flex flex-col">
<span>{model.displayName}</span>
<span className="text-xs text-muted-foreground truncate">
{model.modelName}
</span> </span>
</div> ) : ollamaError ? (
</DropdownMenuItem> <span className="text-xs text-red-500">
)) Error loading
)} </span>
</DropdownMenuSubContent> ) : !hasOllamaModels ? (
</DropdownMenuSub> <span className="text-xs text-muted-foreground">
None available
</span>
) : (
<span className="text-xs text-muted-foreground">
{ollamaModels.length} models
</span>
)}
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto">
<DropdownMenuLabel>Ollama Models</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* LM Studio Models SubMenu */} {ollamaLoading && ollamaModels.length === 0 ? ( // Show loading only if no models are loaded yet
<DropdownMenuSub> <div className="text-xs text-center py-2 text-muted-foreground">
<DropdownMenuSubTrigger Loading models...
disabled={lmStudioLoading && !hasLMStudioModels} // Disable if loading and no models yet </div>
className="w-full font-normal" ) : ollamaError ? (
> <div className="px-2 py-1.5 text-sm text-red-600">
<div className="flex flex-col items-start"> <div className="flex flex-col">
<span>LM Studio</span> <span>Error loading models</span>
{lmStudioLoading ? ( <span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground"> Is Ollama running?
Loading... </span>
</span> </div>
) : lmStudioError ? ( </div>
<span className="text-xs text-red-500">Error loading</span> ) : !hasOllamaModels ? (
) : !hasLMStudioModels ? ( <div className="px-2 py-1.5 text-sm">
<span className="text-xs text-muted-foreground"> <div className="flex flex-col">
None available <span>No local models found</span>
</span> <span className="text-xs text-muted-foreground">
) : ( Ensure Ollama is running and models are pulled.
<span className="text-xs text-muted-foreground"> </span>
{lmStudioModels.length} models </div>
</span> </div>
)} ) : (
</div> ollamaModels.map((model: LocalModel) => (
</DropdownMenuSubTrigger> <DropdownMenuItem
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto"> key={`ollama-${model.modelName}`}
<DropdownMenuLabel>LM Studio Models</DropdownMenuLabel> className={
<DropdownMenuSeparator /> selectedModel.provider === "ollama" &&
selectedModel.name === model.modelName
? "bg-secondary"
: ""
}
onClick={() => {
onModelSelect({
name: model.modelName,
provider: "ollama",
});
setOpen(false);
}}
>
<div className="flex flex-col">
<span>{model.displayName}</span>
<span className="text-xs text-muted-foreground truncate">
{model.modelName}
</span>
</div>
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
{lmStudioLoading && lmStudioModels.length === 0 ? ( // Show loading only if no models are loaded yet {/* LM Studio Models SubMenu */}
<div className="text-xs text-center py-2 text-muted-foreground"> <DropdownMenuSub>
Loading models... <DropdownMenuSubTrigger
</div> disabled={lmStudioLoading && !hasLMStudioModels} // Disable if loading and no models yet
) : lmStudioError ? ( className="w-full font-normal"
<div className="px-2 py-1.5 text-sm text-red-600"> >
<div className="flex flex-col"> <div className="flex flex-col items-start">
<span>Error loading models</span> <span>LM Studio</span>
<span className="text-xs text-muted-foreground"> {lmStudioLoading ? (
{lmStudioError.message} {/* Display specific error */} <span className="text-xs text-muted-foreground">
</span> Loading...
</div> </span>
</div> ) : lmStudioError ? (
) : !hasLMStudioModels ? ( <span className="text-xs text-red-500">
<div className="px-2 py-1.5 text-sm"> Error loading
<div className="flex flex-col"> </span>
<span>No loaded models found</span> ) : !hasLMStudioModels ? (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Ensure LM Studio is running and models are loaded. None available
</span>
</div>
</div>
) : (
lmStudioModels.map((model: LocalModel) => (
<DropdownMenuItem
key={`lmstudio-${model.modelName}`}
className={
selectedModel.provider === "lmstudio" &&
selectedModel.name === model.modelName
? "bg-secondary"
: ""
}
onClick={() => {
onModelSelect({
name: model.modelName,
provider: "lmstudio",
});
setOpen(false);
}}
>
<div className="flex flex-col">
{/* Display the user-friendly name */}
<span>{model.displayName}</span>
{/* Show the path as secondary info */}
<span className="text-xs text-muted-foreground truncate">
{model.modelName}
</span> </span>
) : (
<span className="text-xs text-muted-foreground">
{lmStudioModels.length} models
</span>
)}
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto">
<DropdownMenuLabel>LM Studio Models</DropdownMenuLabel>
<DropdownMenuSeparator />
{lmStudioLoading && lmStudioModels.length === 0 ? ( // Show loading only if no models are loaded yet
<div className="text-xs text-center py-2 text-muted-foreground">
Loading models...
</div> </div>
</DropdownMenuItem> ) : lmStudioError ? (
)) <div className="px-2 py-1.5 text-sm text-red-600">
)} <div className="flex flex-col">
<span>Error loading models</span>
<span className="text-xs text-muted-foreground">
{lmStudioError.message}{" "}
{/* Display specific error */}
</span>
</div>
</div>
) : !hasLMStudioModels ? (
<div className="px-2 py-1.5 text-sm">
<div className="flex flex-col">
<span>No loaded models found</span>
<span className="text-xs text-muted-foreground">
Ensure LM Studio is running and models are loaded.
</span>
</div>
</div>
) : (
lmStudioModels.map((model: LocalModel) => (
<DropdownMenuItem
key={`lmstudio-${model.modelName}`}
className={
selectedModel.provider === "lmstudio" &&
selectedModel.name === model.modelName
? "bg-secondary"
: ""
}
onClick={() => {
onModelSelect({
name: model.modelName,
provider: "lmstudio",
});
setOpen(false);
}}
>
<div className="flex flex-col">
{/* Display the user-friendly name */}
<span>{model.displayName}</span>
{/* Show the path as secondary info */}
<span className="text-xs text-muted-foreground truncate">
{model.modelName}
</span>
</div>
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuSubContent> </DropdownMenuSubContent>
</DropdownMenuSub> </DropdownMenuSub>
</DropdownMenuSubContent> </>
</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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论