Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 0 additions & 121 deletions lib/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,124 +155,3 @@ export async function getTrendingStorylines(
return enriched.slice(offset, offset + limit);
}

/**
* Fetch rising storylines — stories with accelerating signals.
*
* Computes the same 4 signals in a recent window (last 3 days) vs
* prior window (days 3-6). TVL is point-in-time (same for both windows,
* as historical TVL requires snapshots). Price change is inherently
* recent (24h lookback), so prior window uses the same value as a
* baseline denominator — acceleration comes from rating + plot signals.
*/
export async function getRisingStorylines(
supabase: SupabaseClient<Database>,
limit = 20,
writerType?: number,
offset = 0,
genre?: string,
lang?: string,
): Promise<RankedStoryline[]> {
const { storylines } = await fetchCandidatesAndRatings(supabase, writerType, genre, lang);
if (storylines.length === 0) return [];

const now = new Date();
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString();
const sixDaysAgo = new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString();

// Exclude storylines younger than the prior window (6 days) — they have
// no meaningful prior activity to compare against.
const eligible = storylines.filter((sl) => {
if (!sl.block_timestamp) return false;
return new Date(sl.block_timestamp).getTime() <= new Date(sixDaysAgo).getTime();
});
if (eligible.length === 0) return [];

const storylineIds = eligible.map((sl) => sl.storyline_id);

// Batch: windowed ratings
const { data: recentRatings } = await supabase.from("ratings")
.select("storyline_id, rating")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase())
.gte("updated_at", threeDaysAgo);

const { data: priorRatings } = await supabase.from("ratings")
.select("storyline_id, rating")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase())
.gte("updated_at", sixDaysAgo)
.lt("updated_at", threeDaysAgo);

// Batch: windowed plot counts
const { data: recentPlots } = await supabase.from("plots")
.select("storyline_id")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase())
.gte("block_timestamp", threeDaysAgo);

const { data: priorPlots } = await supabase.from("plots")
.select("storyline_id")
.in("storyline_id", storylineIds)
.eq("contract_address", STORY_FACTORY.toLowerCase())
.gte("block_timestamp", sixDaysAgo)
.lt("block_timestamp", threeDaysAgo);

function avgFromRows(rows: { storyline_id: number; rating: number }[] | null, slId: number): number {
if (!rows) return 0;
const filtered = rows.filter((r: { storyline_id: number }) => r.storyline_id === slId);
if (filtered.length === 0) return 0;
return filtered.reduce((s: number, r: { rating: number }) => s + r.rating, 0) / filtered.length;
}

function countFromRows(rows: { storyline_id: number }[] | null, slId: number): number {
if (!rows) return 0;
return rows.filter((r: { storyline_id: number }) => r.storyline_id === slId).length;
}

// Single parallel batch for all on-chain reads
const onChainResults = await Promise.all(
eligible.map((sl) => enrichWithOnChain(sl)),
);

const enriched = eligible.map((sl, i): RankedStoryline => {
const { priceChange, tvlRaw, tvlDecimals } = onChainResults[i];

// Recent window composite (all 4 signals)
const recentAvgRating = avgFromRows(recentRatings, sl.storyline_id);
const recentPlotCount = countFromRows(recentPlots, sl.storyline_id);
const recentScore = computeTrendScore(
recentAvgRating,
priceChange,
tvlRaw,
tvlDecimals,
recentPlotCount,
threeDaysAgo,
);

// Prior window composite (same 4 signals, same TVL + price as baseline)
const priorAvgRating = avgFromRows(priorRatings, sl.storyline_id);
const priorPlotCount = countFromRows(priorPlots, sl.storyline_id);
const priorScore = computeTrendScore(
priorAvgRating,
priceChange, // same baseline — acceleration from rating/plot signals
tvlRaw, // point-in-time, same for both windows
tvlDecimals,
priorPlotCount,
sixDaysAgo,
);

// Require minimum prior activity (at least 1 rating or 1 plot in prior window)
const hasPriorActivity = priorAvgRating > 0 || priorPlotCount > 0;
if (!hasPriorActivity) {
return { ...sl, trendScore: 0 };
}

// Rise = recent / prior (acceleration ratio)
const trendScore = recentScore / (priorScore + 0.01);

return { ...sl, trendScore };
});

enriched.sort((a, b) => b.trendScore - a.trendScore);
return enriched.filter((s) => s.trendScore > 1).slice(offset, offset + limit);
}
25 changes: 2 additions & 23 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createServerClient, type Storyline } from "../../lib/supabase";
import { STORY_FACTORY } from "../../lib/contracts/constants";
import { getTrendingStorylines, getRisingStorylines } from "../../lib/ranking";
import { getTrendingStorylines } from "../../lib/ranking";
import { StoryCard } from "../components/StoryCard";
import { FilterBar, type WriterFilterValue } from "../components/FilterBar";
import { GENRES, LANGUAGES } from "../../lib/genres";
import Link from "next/link";

export const revalidate = 120;

const TABS = ["new", "trending", "rising", "completed"] as const;
const TABS = ["new", "trending"] as const;
type Tab = (typeof TABS)[number];

const WRITER_VALUES: WriterFilterValue[] = ["all", "human", "agent"];
Expand Down Expand Up @@ -148,33 +148,12 @@ async function queryTab(
return data ?? [];
}

case "completed": {
let q = supabase
.from("storylines")
.select("*")
.eq("hidden", false)
.eq("sunset", true)
.eq("contract_address", STORY_FACTORY.toLowerCase());
q = applyFilters(q);
const { data } = await q
.order("plot_count", { ascending: false })
.range(from, to)
.returns<Storyline[]>();
return data ?? [];
}

case "trending": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
const g = genre !== "all" ? genre : undefined;
const l = lang !== "all" ? lang : undefined;
return getTrendingStorylines(supabase, PAGE_SIZE, wt, from, g, l);
}

case "rising": {
const wt = writer === "human" ? 0 : writer === "agent" ? 1 : undefined;
const g = genre !== "all" ? genre : undefined;
const l = lang !== "all" ? lang : undefined;
return getRisingStorylines(supabase, PAGE_SIZE, wt, from, g, l);
}
}
}
2 changes: 0 additions & 2 deletions src/components/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const WRITER_OPTIONS = ["All", "Human", "AI"] as const;
const SORT_OPTIONS = [
{ value: "new", label: "Recent" },
{ value: "trending", label: "Trending" },
{ value: "rising", label: "Rising" },
{ value: "completed", label: "Completed" },
] as const;

export type WriterFilterValue = "all" | "human" | "agent";
Expand Down
Loading