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,425 +201,478 @@ export function ModelPicker() {
<DropdownMenuLabel>Cloud Models</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Cloud models - loading state */}
{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 */
{/* Trial user upgrade banner */}
{isTrial && (
<>
{/* 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 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>
) : !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>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
{Object.keys(modelsByProviders).length > 1 && (
<DropdownMenuSeparator />
)}
</>
)}
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
{Object.keys(modelsByProviders).length > 1 && (
<DropdownMenuSeparator />
)}
</>
)}
{/* Primary providers as submenus */}
{primaryProviders.map(([providerId, models]) => {
models = models.filter((model) => {
// Don't show free models if Dyad Pro is enabled because
// we will use the paid models (in Dyad Pro backend) which
// don't have the free limitations.
if (
isDyadProEnabled(settings) &&
model.apiName.endsWith(":free")
) {
return false;
}
return true;
});
const provider = providers?.find((p) => p.id === providerId);
const providerDisplayName =
provider?.id === "auto"
? "Dyad Turbo"
: (provider?.name ?? 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>{providerDisplayName}</span>
{provider?.type === "cloud" &&
!provider?.secondary &&
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">
Pro
{/* Primary providers as submenus */}
{primaryProviders.map(([providerId, models]) => {
models = models.filter((model) => {
// Don't show free models if Dyad Pro is enabled because
// we will use the paid models (in Dyad Pro backend) which
// don't have the free limitations.
if (
isDyadProEnabled(settings) &&
model.apiName.endsWith(":free")
) {
return false;
}
return true;
});
const provider = providers?.find((p) => p.id === providerId);
const providerDisplayName =
provider?.id === "auto"
? "Dyad Turbo"
: (provider?.name ?? 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>{providerDisplayName}</span>
{provider?.type === "cloud" &&
!provider?.secondary &&
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">
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>
)}
{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 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">
{models.length} models
{secondaryProviders.length} providers
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto">
<DropdownMenuLabel>
{providerDisplayName + " Models"}
</DropdownMenuLabel>
<DropdownMenuSubContent className="w-56">
<DropdownMenuLabel>Other AI providers</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>
)}
{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>
</DropdownMenuItem>
</TooltipTrigger>
<TooltipContent side="right">
{model.description}
</TooltipContent>
</Tooltip>
))}
</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>
);
})}
{/* 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 Parent SubMenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger className="w-full font-normal">
<div className="flex flex-col items-start">
<span>Local models</span>
<span className="text-xs text-muted-foreground">
LM Studio, Ollama
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
{/* Ollama Models SubMenu */}
{/* Local Models - only show for non-trial users */}
{!isTrial && (
<>
<DropdownMenuSeparator />
{/* Local Models Parent SubMenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={ollamaLoading && !hasOllamaModels} // Disable if loading and no models yet
className="w-full font-normal"
>
<DropdownMenuSubTrigger className="w-full font-normal">
<div className="flex flex-col items-start">
<span>Ollama</span>
{ollamaLoading ? (
<span className="text-xs text-muted-foreground">
Loading...
</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>
)}
<span>Local models</span>
<span className="text-xs text-muted-foreground">
LM Studio, Ollama
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto">
<DropdownMenuLabel>Ollama Models</DropdownMenuLabel>
<DropdownMenuSeparator />
{ollamaLoading && ollamaModels.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>
) : ollamaError ? (
<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">
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}
<DropdownMenuSubContent className="w-56">
{/* Ollama Models SubMenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={ollamaLoading && !hasOllamaModels} // Disable if loading and no models yet
className="w-full font-normal"
>
<div className="flex flex-col items-start">
<span>Ollama</span>
{ollamaLoading ? (
<span className="text-xs text-muted-foreground">
Loading...
</span>
</div>
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : 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>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56 max-h-100 overflow-y-auto">
<DropdownMenuLabel>Ollama Models</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* LM Studio Models SubMenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={lmStudioLoading && !hasLMStudioModels} // Disable if loading and no models yet
className="w-full font-normal"
>
<div className="flex flex-col items-start">
<span>LM Studio</span>
{lmStudioLoading ? (
<span className="text-xs text-muted-foreground">
Loading...
</span>
) : lmStudioError ? (
<span className="text-xs text-red-500">Error loading</span>
) : !hasLMStudioModels ? (
<span className="text-xs text-muted-foreground">
None available
</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 />
{ollamaLoading && ollamaModels.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>
) : ollamaError ? (
<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">
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>
</div>
</DropdownMenuItem>
))
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
{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>
) : 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}
{/* LM Studio Models SubMenu */}
<DropdownMenuSub>
<DropdownMenuSubTrigger
disabled={lmStudioLoading && !hasLMStudioModels} // Disable if loading and no models yet
className="w-full font-normal"
>
<div className="flex flex-col items-start">
<span>LM Studio</span>
{lmStudioLoading ? (
<span className="text-xs text-muted-foreground">
Loading...
</span>
) : lmStudioError ? (
<span className="text-xs text-red-500">
Error loading
</span>
) : !hasLMStudioModels ? (
<span className="text-xs text-muted-foreground">
None available
</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>
</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>
</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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论