Skip to content

Commit

Permalink
Partial implementation of voting
Browse files Browse the repository at this point in the history
  • Loading branch information
johnwarden committed Dec 9, 2023
1 parent 97c38d1 commit 99b4053
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 105 deletions.
6 changes: 3 additions & 3 deletions app/attention.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ const GLOBAL_PRIOR_VOTES_PER_VIEW = new GammaDistribution(.1, 4)
export enum LocationType {
NewPost = 0,
TagPage = 1,
UserFeed = 2,
PostReplies = 2
}

export type Location = {
locationType: LocationType,
oneBasedRank: number,
oneBasedRank: number
}


Expand Down Expand Up @@ -53,7 +53,7 @@ export function logTagVote(tag: string) {
}


export async function logPostPageView(_tag: string, _postId: number, _userId: string|null) {
export async function logPostPageView(_tag: string, _postId: number, _userId: string | null) {
// todo
}

Expand Down
34 changes: 23 additions & 11 deletions app/components/ui/feed.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { PostDetails } from "#app/components/ui/post.tsx"
import { type Post } from '#app/db/types.ts'
import { type RankedPost } from '#app/ranking.ts'
import { type Location, LocationType } from '#app/attention.ts'

type FeedProps = {
export function Feed({ tag, posts }: {
tag: string
posts: RankedPost[]
}

export function Feed({ tag, posts }: FeedProps) {
}) {
return (
<div className='flex flex-column place-items-start'>
<ul>
{posts.map((post) => (
<li>
<div className='flex-1 justify-self-center'>
<PostDetails post={post as Post} note={post.note} tag={tag} teaser={true} />
</div>
</li>
))}
{
posts.map((post, i) => {

let randomLocation: Location | null =
post.random
? {
oneBasedRank: i + 1,
locationType: LocationType.TagPage,
} : null

return (
<li key={post.id}>
<div className='flex-1 justify-self-center'>
<PostDetails post={post as Post} note={post.note} tag={tag} teaser={true} randomLocation={randomLocation} />
</div>
</li>

)
})
}
</ul>
</div>
);
Expand Down
47 changes: 36 additions & 11 deletions app/components/ui/post.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Button } from '#app/components/ui/button.tsx';
import { type Post } from '#app/db/types.ts';
import { Link } from '@remix-run/react';
import { Direction } from "#app/vote.ts";
import { Link, useFetcher } from '@remix-run/react';
import { type Location, LocationType } from '#app/attention.ts';


type PostProps = {
export function PostDetails({ tag, post, note, teaser, randomLocation }: {
tag: string,
post: Post,
note: Post | null,
teaser: boolean,
}

export function PostDetails({ tag, post, note, teaser }: PostProps) {
randomLocation: Location | null,
}) {
return (
<div className='flex justify-center'>
<div className="bg-primary-foreground rounded-lg p-5 m-5 w-full max-w-3xl">
Expand All @@ -25,12 +26,14 @@ export function PostDetails({ tag, post, note, teaser }: PostProps) {
? <span />
: <NoteAttachment note={note} tag={tag} />
}
<div>
<VoteButtons postId={post.id} tag={tag} noteId={note !== null ? note.id : null} randomLocation={randomLocation}/>
</div>
{
teaser
? <span />
:
<div>
<VoteButtons />
<ReplyForm parentId={post.id} tag={tag} />
</div>
}
Expand Down Expand Up @@ -71,12 +74,33 @@ export function NoteAttachment({ tag, note }: NoteAttachmentProps) {
)
}

export function VoteButtons() {
export function VoteButtons({ tag, postId, noteId, randomLocation}: { tag: string, postId: number, noteId: number | null, randomLocation: Location | null }) {

const fetcher = useFetcher();

let state = Direction.Neutral

return (
<div>
<Button variant='destructive' size='lg'>Upvote</Button>
<Button variant='destructive' size='lg'>Downvote</Button>
</div>
<fetcher.Form method="post">
<input type="hidden" name="_action" value="vote" />
<input type="hidden" name="postId" value={postId} />
<input type="hidden" name="tag" value={tag} />
<input type="hidden" name="state" value={Direction[state]} />

{randomLocation === null ? <span/> : <span>
<input type="hidden" name="randomLocationType" value={LocationType[randomLocation.locationType]} />
<input type="hidden" name="oneBasedRank" value={randomLocation === null ? "" : randomLocation.oneBasedRank} />
</span>
}

{noteId === null ? <span /> : <input type="hidden" name="noteId" value={noteId} />}

<div>
<Button className="upvote" name="direction" value="Up"></Button>
<Button className="downvote" name="direction" value="Down"></Button>
</div>

</fetcher.Form>
)
}

Expand All @@ -86,6 +110,7 @@ export function ReplyForm({ parentId, tag }: { parentId: number, tag: string })
console.log("Parent id in replyFOrm is ", parentId)
return (
<form id="reply-form" method="post">
<input type="hidden" name="_action" value="reply" />
<div className="w-full flex">
<input type="hidden" name="parentId" value={parentId} />
<input type="hidden" name="tag" value={tag} />
Expand Down
6 changes: 3 additions & 3 deletions app/exploration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LocationType, type Location } from "#app/attention.ts";

/*
logExplorationVote is a key part of to our attention model.
logVoteOnRandomlyRankedPost is a key part of to our attention model.
In order to estimate the information rate for a post, we track the cumulative
attention for each post using a method similar to the one described in the
Expand All @@ -38,7 +38,7 @@ of votes at each location. Dividing by the total impressions gives us the
expected votes per impression at each location -- or the deltaAttention
(per impression) at each location
The logExplorationVote function below updates this aggregate given a
The logVoteOnRandomlyRankedPost function below updates this aggregate given a
location.
Expand All @@ -51,7 +51,7 @@ location.
const movingAverageAlpha = .9999
const windowSize = 1 / (1 - movingAverageAlpha)

export async function logExplorationVote(location: Location) {
export async function logVoteOnRandomlyRankedPost(location: Location) {

const result = await db
.updateTable('ExplorationStats')
Expand Down
54 changes: 18 additions & 36 deletions app/ranking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ type ScoreData = {
topNoteId: number | null
}

export type RankedPost = Post & ScoreData & { explorationPool: boolean, note: Post | null }
export type RankedPost = Post & ScoreData & { random: boolean, note: Post | null }
type PostWithStats = Post & { attention: number, voteCount: number, voteTotal: number }

// const fatigueFactor = .9
const explorationPoolSize = .1
const randomPoolSize = .1
const attentionCutoff = 2 // Note all posts start with 1 unit of attention (from poster)


Expand Down Expand Up @@ -123,29 +123,29 @@ async function getRankedPostsInternal(tagId: number, maxResults: number): Promis
scoredPosts = scoredPosts.filter(p => p.informationValue > 0)


// Then split into two pools: rankedPosts, and explorationPosts, based on
// Then split into two pools: rankedPosts, and randomPosts, based on
// whether cumulative attention is above or below the cutoff.

let rankedPosts = scoredPosts
.filter(p => p.attention >= attentionCutoff)
.sort((a, b) => { return b.score - a.score })
let nRankedPosts = rankedPosts.length

let explorationPosts = scoredPosts.filter(p => p.attention < attentionCutoff)
let nExplorationPosts = explorationPosts.length
let randomPosts = scoredPosts.filter(p => p.attention < attentionCutoff)
let nRandomPosts = randomPosts.length


let nResults = nPosts
if (nResults > maxResults) {
nResults = maxResults
}

console.log("Number of posts", nResults, nPosts, nExplorationPosts, nRankedPosts)
console.log("Number of posts", nResults, nPosts, nRandomPosts, nRankedPosts)

// Finally, create nResults results by iterating through the ranked posts
// while randomly inserting posts from the exploration pool (with a
// probability of explorationPoolSize) at each rank. When we run out of
// ranked posts, return random posts from the exploration pool until we
// while randomly inserting posts from the random pool (with a
// probability of randomPoolSize) at each rank. When we run out of
// ranked posts, return random posts from the random pool until we
// have nResults total posts.

let results: RankedPost[] = Array(nResults)
Expand All @@ -155,23 +155,23 @@ async function getRankedPostsInternal(tagId: number, maxResults: number): Promis
let ep
let p = null

if (i < nRankedPosts && Math.random() > explorationPoolSize || nExplorationPosts == 0) {
if (i < nRankedPosts && Math.random() > randomPoolSize || nRandomPosts == 0) {
p = rankedPosts[i - nInsertions]
// console.log("Taking post ranked", i, i - nInsertions)
ep = false
} else {
assert(nExplorationPosts > 0, "nExplorationPosts > 0") // this must be true if my logic is correct. But is my logic correct.
let randomPostNum = Math.floor(Math.random() * nExplorationPosts)
// console.log("Taking random post", i, nExplorationPosts, randomPostNum)
p = explorationPosts[randomPostNum]
explorationPosts.splice(randomPostNum, 1)
nExplorationPosts--
assert(nExplorationPosts == explorationPosts.length, "nExplorationPosts == explorationPosts.length")
assert(nRandomPosts > 0, "nRandomPosts > 0") // this must be true if my logic is correct. But is my logic correct.
let randomPostNum = Math.floor(Math.random() * nRandomPosts)
// console.log("Taking random post", i, nRandomPosts, randomPostNum)
p = randomPosts[randomPostNum]
randomPosts.splice(randomPostNum, 1)
nRandomPosts--
assert(nRandomPosts == randomPosts.length, "nRandomPosts == randomPosts.length")
nInsertions += 1
ep = true
}
assert(p !== undefined)
let s = { oneBasedRank: i + 1, explorationPool: ep };
let s = { oneBasedRank: i + 1, random: ep };

let note = p.topNoteId !== null
? await db
Expand Down Expand Up @@ -262,24 +262,6 @@ export async function getRankedNotes(tag: string, postId: number): Promise<Post[
.execute()
}

// await logPostPageView(tag, post.id, userId, notes)


// Exploration Pool Logic:
// R% of impressions at each rank are exploration pool
// so for each rank, exploration impression with probability of R
// randomly choose post from exploration pool
// exploration pool is posts with less than certain amount of attention
// or better, with a certain confidence interval
// increment impression count at that rank
// increment cumulative attention of post
// create link that has eRank of eImpression
// log eVote when
// weight factors is just average of eVote / eImpression group by rank


// }




Expand Down
2 changes: 2 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { useOptionalUser, useUser } from './utils/user.ts'
import { db } from "#app/db.ts";
import { Feed } from './components/ui/feed.tsx'
import { type Post } from '#app/db/types.ts'
import { ExternalScripts } from "remix-utils/external-scripts";

export const links: LinksFunction = () => {
return [
Expand Down Expand Up @@ -201,6 +202,7 @@ function Document({
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Links />
<ExternalScripts/>
</head>
<body className="bg-background text-foreground">
{children}
Expand Down
Loading

0 comments on commit 99b4053

Please sign in to comment.