((set) => ({
+ offeredFor: null,
+ suggestion: null,
+
+ offerAfterAnalysis: async (analysisTimestamp) => {
+ if (!(await claimFirstRunAiTry())) return;
+ // Pro, or a free allowance already used up, spends the claim without a prompt.
+ if (aiStatusNow().mode === "free") {
+ set({ offeredFor: analysisTimestamp, suggestion: null });
+ }
+ },
+
+ setSuggestion: (suggestion) => set({ suggestion }),
+
+ dismiss: () => set({ offeredFor: null, suggestion: null }),
+}));
diff --git a/app/src/sidepanel/pages/ScorePage.test.tsx b/app/src/sidepanel/pages/ScorePage.test.tsx
index 53f0b99..454b40a 100644
--- a/app/src/sidepanel/pages/ScorePage.test.tsx
+++ b/app/src/sidepanel/pages/ScorePage.test.tsx
@@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/react";
import { useStore } from "@/lib/store";
import { useEntitlementStore } from "@/lib/entitlement-store";
+import { useFirstRunAiTry } from "@/lib/first-run-ai-try";
import { ScorePage } from "./ScorePage";
import type { SEOAnalysis, PageSEOData } from "@/types/seo";
@@ -58,6 +59,7 @@ const mockAnalysis: SEOAnalysis = {
beforeEach(() => {
useEntitlementStore.setState({ entitlementLoaded: false, isPro: false, tier: "free" });
+ useFirstRunAiTry.setState({ offeredFor: null, suggestion: null });
useStore.setState({
view: "score",
analysis: null,
@@ -138,6 +140,24 @@ describe("ScorePage", () => {
expect(screen.queryByRole("button", { name: /upgrade/i })).not.toBeInTheDocument();
});
+ it("shows the first-run AI try above the breakdown when it was offered for this analysis", () => {
+ useFirstRunAiTry.setState({ offeredFor: mockAnalysis.timestamp, suggestion: null });
+ useStore.setState({ analysis: mockAnalysis });
+ render();
+
+ const tryHeading = screen.getByRole("heading", { name: /try one ai suggestion/i });
+ expect(
+ tryHeading.compareDocumentPosition(screen.getByText("Meta Tags")) &
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+
+ it("shows no AI try for an analysis it was not offered for", () => {
+ useStore.setState({ analysis: mockAnalysis });
+ render();
+ expect(screen.queryByRole("heading", { name: /try one ai suggestion/i })).not.toBeInTheDocument();
+ });
+
it("shows warning banner when fetchWarnings present", () => {
const analysisWithWarnings: SEOAnalysis = {
...mockAnalysis,
diff --git a/app/src/sidepanel/pages/ScorePage.tsx b/app/src/sidepanel/pages/ScorePage.tsx
index fbd6e9b..0c0dd60 100644
--- a/app/src/sidepanel/pages/ScorePage.tsx
+++ b/app/src/sidepanel/pages/ScorePage.tsx
@@ -3,6 +3,7 @@ import { ArrowLeft, AlertTriangle, Sparkles } from "lucide-react";
import confetti from "canvas-confetti";
import { ScoreGauge } from "@/components/ui/ScoreGauge";
import { SummaryCard } from "@/components/SummaryCard";
+import { FirstRunAiTry } from "@/components/FirstRunAiTry";
import { Footer } from "@/components/Footer";
import { PanelHeader } from "@/components/PanelHeader";
import { ThemeToggle } from "@/components/ui/ThemeToggle";
@@ -106,6 +107,9 @@ export function ScorePage() {
+ {/* One-time AI try after a fresh install's first analysis */}
+
+
{/* Category breakdown */}
{analysis.categories.map((cat) => (
diff --git a/docs/free-vs-pro-gating.md b/docs/free-vs-pro-gating.md
index 133712c..3e9ed64 100644
--- a/docs/free-vs-pro-gating.md
+++ b/docs/free-vs-pro-gating.md
@@ -11,6 +11,7 @@ from that store — never an ad-hoc `apiKey` check or raw license data.
| On-page SEO scoring | ✅ Full | ✅ Full | *ungated — never gate* |
| Score breakdown (per-check detail) | ✅ Full | ✅ Full | *ungated — never gate* |
| AI recommendations (title, meta, H1, URL, intro, H2, alt text) | ✅ Capped monthly, via hosted proxy | ✅ Higher cap via proxy, or unlimited with own key | `useCanUseAI()` / `useAiStatus()` |
+| First-run AI try (one title suggestion offered after a fresh install's first analysis, #80) | ✅ Once per install; counts against the monthly allowance | ❌ Never shown | `aiStatusNow().mode === "free"` at the first analysis; hidden whenever `isPro` |
| Advanced analysis (page type, secondary keywords) | ❌ | ✅ | `canUseAdvancedOptions` |
| Multi-language AI output | ❌ English only | ✅ Any supported language | `canUseMultiLanguage` |
| Schema markup generation | ❌ | ✅ | `canUseSchema` |