Spaces:
Runtime error
Runtime error
Commit 路
ba40d17
1
Parent(s): b612dd5
LETS GOOOOOOOOOOOOOOOOOOOOOOOOO
Browse files- backend/app/routers/artifacts.py +2 -2
- backend/app/routers/sources.py +4 -5
- backend/app/services/plans.py +1 -1
- frontend/seo/prerender.ts +1 -1
- frontend/seo/sitemap.ts +1 -1
- frontend/src/api/types.ts +2 -2
- frontend/src/components/PricingTiers.tsx +1 -1
- frontend/src/components/Reader.tsx +4 -4
- frontend/src/pages/Sources.tsx +9 -6
- frontend/src/pages/Welcome.tsx +6 -6
backend/app/routers/artifacts.py
CHANGED
|
@@ -60,8 +60,8 @@ def _target_title(ctx: Ctx, notebook_id: uuid.UUID | None, document_id: uuid.UUI
|
|
| 60 |
if not doc.path:
|
| 61 |
plan = effective_plan(ctx.db, ctx.workspace)
|
| 62 |
raise plan_limit_error(plan, "indexed_posts",
|
| 63 |
-
f"This post is
|
| 64 |
-
"
|
| 65 |
return doc.title
|
| 66 |
if notebook_id:
|
| 67 |
nb = ctx.db.scalar(select(Notebook).where(Notebook.id == notebook_id,
|
|
|
|
| 60 |
if not doc.path:
|
| 61 |
plan = effective_plan(ctx.db, ctx.workspace)
|
| 62 |
raise plan_limit_error(plan, "indexed_posts",
|
| 63 |
+
f"This post is not indexed. Your plan indexes your latest {plan.indexed_posts} "
|
| 64 |
+
"posts; upgrade to use your whole archive.")
|
| 65 |
return doc.title
|
| 66 |
if notebook_id:
|
| 67 |
nb = ctx.db.scalar(select(Notebook).where(Notebook.id == notebook_id,
|
backend/app/routers/sources.py
CHANGED
|
@@ -35,7 +35,7 @@ def serialize_source(source: Source, doc_count: int = 0, locked_count: int = 0)
|
|
| 35 |
"sync_error": source.sync_error,
|
| 36 |
"last_synced_at": source.last_synced_at.isoformat() if source.last_synced_at else None,
|
| 37 |
"document_count": doc_count,
|
| 38 |
-
#
|
| 39 |
"locked_count": locked_count,
|
| 40 |
"is_imports": source.feed_url == IMPORTS_FEED,
|
| 41 |
}
|
|
@@ -159,7 +159,7 @@ documents_router = APIRouter(prefix="/api/documents", tags=["documents"])
|
|
| 159 |
|
| 160 |
|
| 161 |
def is_locked(d: Document) -> bool:
|
| 162 |
-
"""
|
| 163 |
return not d.path and bool((d.metadata_json or {}).get("locked"))
|
| 164 |
|
| 165 |
|
|
@@ -212,10 +212,9 @@ def get_document(document_id: uuid.UUID, ctx: Ctx = Depends(get_ctx)):
|
|
| 212 |
raise HTTPException(404, "Post not found")
|
| 213 |
source = ctx.db.get(Source, doc.source_id) if doc.source_id else None
|
| 214 |
if is_locked(doc):
|
| 215 |
-
#
|
| 216 |
-
preview = [ln for ln in (doc.clean_text or "").splitlines() if ln.strip()][:3]
|
| 217 |
plan = effective_plan(ctx.db, ctx.workspace)
|
| 218 |
-
return {**serialize_document(doc, source), "lines": [f"# {doc.title}"
|
| 219 |
"preview": True, "limit": plan.indexed_posts}
|
| 220 |
lines: list[str] = []
|
| 221 |
if doc.path:
|
|
|
|
| 35 |
"sync_error": source.sync_error,
|
| 36 |
"last_synced_at": source.last_synced_at.isoformat() if source.last_synced_at else None,
|
| 37 |
"document_count": doc_count,
|
| 38 |
+
# Found but beyond the plan's post limit: listed, not indexed until the workspace upgrades.
|
| 39 |
"locked_count": locked_count,
|
| 40 |
"is_imports": source.feed_url == IMPORTS_FEED,
|
| 41 |
}
|
|
|
|
| 159 |
|
| 160 |
|
| 161 |
def is_locked(d: Document) -> bool:
|
| 162 |
+
"""Past the plan's post limit (see ingest.store_locked): listed with title and link only, not indexed."""
|
| 163 |
return not d.path and bool((d.metadata_json or {}).get("locked"))
|
| 164 |
|
| 165 |
|
|
|
|
| 212 |
raise HTTPException(404, "Post not found")
|
| 213 |
source = ctx.db.get(Source, doc.source_id) if doc.source_id else None
|
| 214 |
if is_locked(doc):
|
| 215 |
+
# Listed, not indexed: there is no text to show until the workspace upgrades.
|
|
|
|
| 216 |
plan = effective_plan(ctx.db, ctx.workspace)
|
| 217 |
+
return {**serialize_document(doc, source), "lines": [f"# {doc.title}"],
|
| 218 |
"preview": True, "limit": plan.indexed_posts}
|
| 219 |
lines: list[str] = []
|
| 220 |
if doc.path:
|
backend/app/services/plans.py
CHANGED
|
@@ -41,7 +41,7 @@ PLANS: dict[str, Plan] = {
|
|
| 41 |
voice_cloning=False,
|
| 42 |
brand_kit=False,
|
| 43 |
features=(
|
| 44 |
-
"1 source,
|
| 45 |
"Grounded research chat with citations",
|
| 46 |
"3 min of audio overviews a month",
|
| 47 |
"1 min of video a month",
|
|
|
|
| 41 |
voice_cloning=False,
|
| 42 |
brand_kit=False,
|
| 43 |
features=(
|
| 44 |
+
"1 source, your latest 5 posts indexed",
|
| 45 |
"Grounded research chat with citations",
|
| 46 |
"3 min of audio overviews a month",
|
| 47 |
"1 min of video a month",
|
frontend/seo/prerender.ts
CHANGED
|
@@ -182,7 +182,7 @@ function routes(): Rendered[] {
|
|
| 182 |
description:
|
| 183 |
"Notestack pricing: Free ($0), Writer ($24.99 a month, $18.99 billed annually) and Studio ($48.99 a month, $36.99 billed annually). Annual billing saves 24%.",
|
| 184 |
body: page(
|
| 185 |
-
`<h1>Pick your trajectory</h1><ul><li><h2>Free</h2><p>$0. 1 source,
|
| 186 |
),
|
| 187 |
},
|
| 188 |
{
|
|
|
|
| 182 |
description:
|
| 183 |
"Notestack pricing: Free ($0), Writer ($24.99 a month, $18.99 billed annually) and Studio ($48.99 a month, $36.99 billed annually). Annual billing saves 24%.",
|
| 184 |
body: page(
|
| 185 |
+
`<h1>Pick your trajectory</h1><ul><li><h2>Free</h2><p>$0. 1 source, your latest 5 posts indexed, 3 audio minutes, 1 video minute, 2 Launch Kits a month.</p></li><li><h2>Writer</h2><p>$24.99 a month ($18.99 a month billed annually). 3 sources, 500 posts, 60 audio minutes, 30 video minutes, 50 Launch Kits.</p></li><li><h2>Studio</h2><p>$48.99 a month ($36.99 a month billed annually). 10 sources, 5,000 posts, 240 audio minutes, 120 video minutes, unlimited Launch Kits.</p></li></ul><p><a href="/auth?mode=signup">Start free</a></p>`,
|
| 186 |
),
|
| 187 |
},
|
| 188 |
{
|
frontend/seo/sitemap.ts
CHANGED
|
@@ -96,7 +96,7 @@ differs from NotebookLM.
|
|
| 96 |
SEO pack, carousel).
|
| 97 |
- **Publishing:** a Launchpad calendar that auto posts to X, LinkedIn and Bluesky, plus a topic map of rising and
|
| 98 |
dormant themes and evergreen resurfacing of old posts.
|
| 99 |
-
- **Pricing (USD):** Free: 1 source,
|
| 100 |
Writer: $24.99 a month ($18.99 a month billed annually): 3 sources, 500 posts, 60 audio minutes, 30 video minutes,
|
| 101 |
50 launch kits. Studio: $48.99 a month ($36.99 a month billed annually): 10 sources, 5,000 posts, 240 audio
|
| 102 |
minutes, 120 video minutes, unlimited launch kits. Annual billing saves 24%.
|
|
|
|
| 96 |
SEO pack, carousel).
|
| 97 |
- **Publishing:** a Launchpad calendar that auto posts to X, LinkedIn and Bluesky, plus a topic map of rising and
|
| 98 |
dormant themes and evergreen resurfacing of old posts.
|
| 99 |
+
- **Pricing (USD):** Free: 1 source, latest 5 posts indexed, 3 audio minutes, 1 video minute, 2 launch kits a month.
|
| 100 |
Writer: $24.99 a month ($18.99 a month billed annually): 3 sources, 500 posts, 60 audio minutes, 30 video minutes,
|
| 101 |
50 launch kits. Studio: $48.99 a month ($36.99 a month billed annually): 10 sources, 5,000 posts, 240 audio
|
| 102 |
minutes, 120 video minutes, unlimited launch kits. Annual billing saves 24%.
|
frontend/src/api/types.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type Source = {
|
|
| 25 |
sync_error: string | null;
|
| 26 |
last_synced_at: string | null;
|
| 27 |
document_count: number;
|
| 28 |
-
/**
|
| 29 |
locked_count: number;
|
| 30 |
is_imports: boolean;
|
| 31 |
};
|
|
@@ -40,7 +40,7 @@ export type Doc = {
|
|
| 40 |
published_at: string | null;
|
| 41 |
words: number;
|
| 42 |
evergreen_score: number | null;
|
| 43 |
-
/**
|
| 44 |
locked: boolean;
|
| 45 |
};
|
| 46 |
|
|
|
|
| 25 |
sync_error: string | null;
|
| 26 |
last_synced_at: string | null;
|
| 27 |
document_count: number;
|
| 28 |
+
/** Found but past the plan's post limit: listed, not indexed until the workspace upgrades. */
|
| 29 |
locked_count: number;
|
| 30 |
is_imports: boolean;
|
| 31 |
};
|
|
|
|
| 40 |
published_at: string | null;
|
| 41 |
words: number;
|
| 42 |
evergreen_score: number | null;
|
| 43 |
+
/** Found but past the plan's post limit: title and link only, not indexed until the workspace upgrades. */
|
| 44 |
locked: boolean;
|
| 45 |
};
|
| 46 |
|
frontend/src/components/PricingTiers.tsx
CHANGED
|
@@ -26,7 +26,7 @@ const FALLBACK: Plan[] = [
|
|
| 26 |
tagline: "For trying Notestack on your archive",
|
| 27 |
price_monthly_usd: 0,
|
| 28 |
features: [
|
| 29 |
-
"1 source,
|
| 30 |
"Grounded research chat with citations",
|
| 31 |
"3 min of audio overviews a month",
|
| 32 |
"1 min of video a month",
|
|
|
|
| 26 |
tagline: "For trying Notestack on your archive",
|
| 27 |
price_monthly_usd: 0,
|
| 28 |
features: [
|
| 29 |
+
"1 source, your latest 5 posts indexed",
|
| 30 |
"Grounded research chat with citations",
|
| 31 |
"3 min of audio overviews a month",
|
| 32 |
"1 min of video a month",
|
frontend/src/components/Reader.tsx
CHANGED
|
@@ -74,14 +74,14 @@ export function Reader({
|
|
| 74 |
{doc.preview && (
|
| 75 |
<div className="welcome-cap locked-cap">
|
| 76 |
<div>
|
| 77 |
-
<strong>This post is
|
| 78 |
<p className="muted small">
|
| 79 |
-
Your plan
|
| 80 |
-
|
| 81 |
</p>
|
| 82 |
</div>
|
| 83 |
<button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
|
| 84 |
-
Upgrade to
|
| 85 |
</button>
|
| 86 |
</div>
|
| 87 |
)}
|
|
|
|
| 74 |
{doc.preview && (
|
| 75 |
<div className="welcome-cap locked-cap">
|
| 76 |
<div>
|
| 77 |
+
<strong>This post is not indexed</strong>
|
| 78 |
<p className="muted small">
|
| 79 |
+
Your plan indexes your latest {doc.limit ?? 5} posts. Upgrade to index this one and the rest of your
|
| 80 |
+
archive, so you can ask about it and turn it into audio, video and Launch Kits.
|
| 81 |
</p>
|
| 82 |
</div>
|
| 83 |
<button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
|
| 84 |
+
Upgrade to index it
|
| 85 |
</button>
|
| 86 |
</div>
|
| 87 |
)}
|
frontend/src/pages/Sources.tsx
CHANGED
|
@@ -162,7 +162,7 @@ export default function Sources() {
|
|
| 162 |
<strong>{s.title ?? s.feed_url}</strong>
|
| 163 |
<span className="mono muted">
|
| 164 |
{s.is_imports ? "imports" : s.platform} 路 {s.document_count} posts
|
| 165 |
-
{s.locked_count > 0 ? `
|
| 166 |
{s.last_synced_at ? ` 路 synced ${formatDate(s.last_synced_at, true)}` : ""}
|
| 167 |
</span>
|
| 168 |
</div>
|
|
@@ -177,11 +177,14 @@ export default function Sources() {
|
|
| 177 |
{s.locked_count > 0 && !active && (
|
| 178 |
<div className="welcome-cap locked-cap">
|
| 179 |
<div>
|
| 180 |
-
<strong>{s.locked_count} posts
|
| 181 |
-
<p className="muted small">
|
|
|
|
|
|
|
|
|
|
| 182 |
</div>
|
| 183 |
<button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
|
| 184 |
-
|
| 185 |
</button>
|
| 186 |
</div>
|
| 187 |
)}
|
|
@@ -237,8 +240,8 @@ export default function Sources() {
|
|
| 237 |
{d.title}
|
| 238 |
</button>
|
| 239 |
{d.locked && (
|
| 240 |
-
<span className="mono muted small" title="
|
| 241 |
-
|
| 242 |
</span>
|
| 243 |
)}
|
| 244 |
<span className="mono muted">
|
|
|
|
| 162 |
<strong>{s.title ?? s.feed_url}</strong>
|
| 163 |
<span className="mono muted">
|
| 164 |
{s.is_imports ? "imports" : s.platform} 路 {s.document_count} posts
|
| 165 |
+
{s.locked_count > 0 ? ` found, ${s.document_count - s.locked_count} indexed` : ""}
|
| 166 |
{s.last_synced_at ? ` 路 synced ${formatDate(s.last_synced_at, true)}` : ""}
|
| 167 |
</span>
|
| 168 |
</div>
|
|
|
|
| 177 |
{s.locked_count > 0 && !active && (
|
| 178 |
<div className="welcome-cap locked-cap">
|
| 179 |
<div>
|
| 180 |
+
<strong>{s.locked_count} posts not indexed</strong>
|
| 181 |
+
<p className="muted small">
|
| 182 |
+
Your plan only indexes your latest posts. Upgrade to index the rest so answers, audio and Launch Kits
|
| 183 |
+
can use your whole archive.
|
| 184 |
+
</p>
|
| 185 |
</div>
|
| 186 |
<button type="button" className="btn btn-small btn-primary" onClick={() => openUpgrade("indexed_posts")}>
|
| 187 |
+
Index all {s.document_count}
|
| 188 |
</button>
|
| 189 |
</div>
|
| 190 |
)}
|
|
|
|
| 240 |
{d.title}
|
| 241 |
</button>
|
| 242 |
{d.locked && (
|
| 243 |
+
<span className="mono muted small" title="Upgrade to index this post">
|
| 244 |
+
Not indexed
|
| 245 |
</span>
|
| 246 |
)}
|
| 247 |
<span className="mono muted">
|
frontend/src/pages/Welcome.tsx
CHANGED
|
@@ -144,7 +144,7 @@ export default function Welcome() {
|
|
| 144 |
|
| 145 |
const indexingDone = live?.status === "done";
|
| 146 |
const indexingFailed = live?.status === "failed";
|
| 147 |
-
//
|
| 148 |
const locked = Number(live?.result?.locked ?? 0);
|
| 149 |
const capped = indexingDone && locked > 0;
|
| 150 |
const available = Number(live?.result?.available ?? posts);
|
|
@@ -153,11 +153,11 @@ export default function Welcome() {
|
|
| 153 |
<div className="welcome-cap">
|
| 154 |
<div>
|
| 155 |
<strong>
|
| 156 |
-
|
| 157 |
</strong>
|
| 158 |
<p className="muted small">
|
| 159 |
-
|
| 160 |
-
|
| 161 |
</p>
|
| 162 |
</div>
|
| 163 |
<button
|
|
@@ -168,7 +168,7 @@ export default function Welcome() {
|
|
| 168 |
navigate("/app?upgrade=1", { replace: true });
|
| 169 |
}}
|
| 170 |
>
|
| 171 |
-
|
| 172 |
</button>
|
| 173 |
</div>
|
| 174 |
) : null;
|
|
@@ -336,7 +336,7 @@ export default function Welcome() {
|
|
| 336 |
|
| 337 |
{step === "goal" && (
|
| 338 |
<section className="card welcome-card">
|
| 339 |
-
<p className="eyebrow">{indexingDone ? (capped ? `${available} of ${found} posts
|
| 340 |
<h1>What should we launch first?</h1>
|
| 341 |
{lockedNotice}
|
| 342 |
<div className="goal-grid">
|
|
|
|
| 144 |
|
| 145 |
const indexingDone = live?.status === "done";
|
| 146 |
const indexingFailed = live?.status === "failed";
|
| 147 |
+
// The ingest job lists the whole archive but indexes only the plan's limit; the rest waits behind an upgrade.
|
| 148 |
const locked = Number(live?.result?.locked ?? 0);
|
| 149 |
const capped = indexingDone && locked > 0;
|
| 150 |
const available = Number(live?.result?.available ?? posts);
|
|
|
|
| 153 |
<div className="welcome-cap">
|
| 154 |
<div>
|
| 155 |
<strong>
|
| 156 |
+
We found {found} posts. The free plan indexed only your latest {available}.
|
| 157 |
</strong>
|
| 158 |
<p className="muted small">
|
| 159 |
+
The other {locked} are sitting in your archive, unread. Until you upgrade, answers, audio overviews and Launch
|
| 160 |
+
Kits can only use {available} posts. Upgrade and all {found} are indexed automatically.
|
| 161 |
</p>
|
| 162 |
</div>
|
| 163 |
<button
|
|
|
|
| 168 |
navigate("/app?upgrade=1", { replace: true });
|
| 169 |
}}
|
| 170 |
>
|
| 171 |
+
Index all {found} posts
|
| 172 |
</button>
|
| 173 |
</div>
|
| 174 |
) : null;
|
|
|
|
| 336 |
|
| 337 |
{step === "goal" && (
|
| 338 |
<section className="card welcome-card">
|
| 339 |
+
<p className="eyebrow">{indexingDone ? (capped ? `${available} of ${found} posts indexed` : `${posts} posts indexed`) : "Still indexing in the background"}</p>
|
| 340 |
<h1>What should we launch first?</h1>
|
| 341 |
{lockedNotice}
|
| 342 |
<div className="goal-grid">
|