fix: randomized tweet variations + referral fixes (#737)

* fix(agent): declare @browseros/shared as workspace dependency

The agent app imports @browseros/shared/constants/urls in
lib/referral/submit-referral.ts but never declared the package in its
dependencies, so vite failed to resolve the import during dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(referral): cap daily referral earnings at 500 credits

Block tweet submissions client-side once the user's balance reaches
500 to prevent unlimited credit farming via repeated shares.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(referral): randomize tweet variations for Twitter share

Replace the single hardcoded share text with 10 feature-specific
variations (agent mode, chat, scheduled tasks, connect apps, cowork,
workflows, memory, skills, local models, ad blocking) and pick one at
random each time the share button is clicked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(referral): regenerate share URL on click

Previously getShareOnTwitterUrl() was evaluated once at render time as
a static href, so every click produced the same tweet variation. Move
the call into onClick so a new random variation is picked each time.

Addresses Greptile P1 review on PR #737.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Felarof
2026-04-16 17:09:28 -07:00
committed by GitHub
parent 7f2e387903
commit 6d3498c91b
5 changed files with 109 additions and 4 deletions

View File

@@ -1,3 +1,4 @@
import { REFERRAL_LIMITS } from '@browseros/shared/constants/limits'
import { ExternalLink, Loader2, Send } from 'lucide-react' import { ExternalLink, Loader2, Send } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import { useState } from 'react' import { useState } from 'react'
@@ -24,8 +25,11 @@ export const ShareForCredits: FC<ShareForCreditsProps> = ({ compact }) => {
const { data } = useCredits() const { data } = useCredits()
const invalidateCredits = useInvalidateCredits() const invalidateCredits = useInvalidateCredits()
const credits = data?.credits ?? 0
const atDailyMax = credits >= REFERRAL_LIMITS.MAX_DAILY_CREDITS
const handleSubmit = async () => { const handleSubmit = async () => {
if (!tweetUrl.trim() || !data?.browserosId) return if (!tweetUrl.trim() || !data?.browserosId || atDailyMax) return
setIsSubmitting(true) setIsSubmitting(true)
setResult(null) setResult(null)
@@ -55,10 +59,22 @@ export const ShareForCredits: FC<ShareForCreditsProps> = ({ compact }) => {
} }
} }
if (atDailyMax) {
return (
<div className={compact ? 'space-y-2' : 'space-y-3'}>
<p className={compact ? 'text-muted-foreground text-xs' : 'text-sm'}>
You've reached the daily cap of {REFERRAL_LIMITS.MAX_DAILY_CREDITS}{' '}
credits. Come back tomorrow to earn more!
</p>
</div>
)
}
return ( return (
<div className={compact ? 'space-y-2' : 'space-y-3'}> <div className={compact ? 'space-y-2' : 'space-y-3'}>
<p className={compact ? 'text-muted-foreground text-xs' : 'text-sm'}> <p className={compact ? 'text-muted-foreground text-xs' : 'text-sm'}>
Share BrowserOS on Twitter to earn 200 bonus credits! Share BrowserOS on Twitter to earn{' '}
{REFERRAL_LIMITS.CREDITS_PER_REFERRAL} bonus credits!
</p> </p>
<ul className="list-disc space-y-0.5 pl-4 text-muted-foreground text-xs"> <ul className="list-disc space-y-0.5 pl-4 text-muted-foreground text-xs">
@@ -67,6 +83,10 @@ export const ShareForCredits: FC<ShareForCreditsProps> = ({ compact }) => {
</li> </li>
<li>Tweet must be posted within the last 30 minutes</li> <li>Tweet must be posted within the last 30 minutes</li>
<li>Each tweet can only be submitted once</li> <li>Each tweet can only be submitted once</li>
<li>
Daily cap of {REFERRAL_LIMITS.MAX_DAILY_CREDITS} credits — resets at
midnight UTC
</li>
</ul> </ul>
<Button variant="outline" size="sm" className="w-full gap-2" asChild> <Button variant="outline" size="sm" className="w-full gap-2" asChild>
@@ -74,6 +94,9 @@ export const ShareForCredits: FC<ShareForCreditsProps> = ({ compact }) => {
href={getShareOnTwitterUrl()} href={getShareOnTwitterUrl()}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => {
e.currentTarget.href = getShareOnTwitterUrl()
}}
> >
<ExternalLink className="h-3.5 w-3.5" /> <ExternalLink className="h-3.5 w-3.5" />
Share on Twitter Share on Twitter

View File

@@ -27,7 +27,82 @@ export async function submitReferral(
return response.json() return response.json()
} }
const TWEET_VARIATIONS = [
`ngl @browseros_ai is kinda wild
just type what u want in plain english and it handles the annoying web shit
forms, research, data pulls... all automated
actually works`,
`been using @browseros_ai to chat with webpages lately
summarize articles, pull data, translate stuff
all happens in the same tab
no copy/paste, no switching windows
just ask and it does it`,
`wake up to @browseros_ai having already read ur emails and calendar while u were sleeping
scheduled agents are lowkey magic`,
`ngl @browseros_ai is kinda crazy
connects gmail, slack, linear, notion + 40 other apps into one ai assistant
just talk to it in plain english and it handles cross-app workflows for u
no more switching between tabs like a psycho`,
`i use @browseros_ai to automate research
it handles the browser work and drops reports straight into local folders
no switching between tools or manually saving files
just one task instead of three`,
`been messing with @browseros_ai lately
it comes with a prebuilt MCP server and I connect it claude code or codex and it just runs things for you
set it up once, use it whenever
way better than clicking through the same shit manually every time`,
`the ai actually remembers what we talked about yesterday
no more "here's the context again" every single conversation
@browseros_ai just picks up where we left off
feels like talking to someone who actually pays attention`,
`i built a skill library for my ai agent
now when i need it to do something specific, i just load the recipe i made earlier
@browseros_ai MCP is very handy`,
`been running @browseros_ai with ollama locally
everything stays on my machine, nothing gets sent out
kinda nice not having to think about what data i'm sharing`,
`switched to @browseros_ai from chrome
blocks 10x more ads and runs full ublock origin (not the lite version)
check it out`,
]
export function getShareOnTwitterUrl(): string { export function getShareOnTwitterUrl(): string {
const text = 'I use @browseros_ai to browse the web with AI. Check it out!' const text =
TWEET_VARIATIONS[Math.floor(Math.random() * TWEET_VARIATIONS.length)]
return `https://x.com/intent/tweet?text=${encodeURIComponent(text)}` return `https://x.com/intent/tweet?text=${encodeURIComponent(text)}`
} }

View File

@@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@ai-sdk/react": "^3.0.96", "@ai-sdk/react": "^3.0.96",
"@browseros/server": "workspace:*", "@browseros/server": "workspace:*",
"@browseros/shared": "workspace:*",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@lobehub/icons": "^2.44.0", "@lobehub/icons": "^2.44.0",
"@mdxeditor/editor": "^3.52.4", "@mdxeditor/editor": "^3.52.4",

View File

@@ -27,6 +27,7 @@
"dependencies": { "dependencies": {
"@ai-sdk/react": "^3.0.96", "@ai-sdk/react": "^3.0.96",
"@browseros/server": "workspace:*", "@browseros/server": "workspace:*",
"@browseros/shared": "workspace:*",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@lobehub/icons": "^2.44.0", "@lobehub/icons": "^2.44.0",
"@mdxeditor/editor": "^3.52.4", "@mdxeditor/editor": "^3.52.4",
@@ -2210,7 +2211,7 @@
"chrome-devtools-frontend": ["chrome-devtools-frontend@1.0.1577886", "", {}, "sha512-B9hY3o/0RuVCDWNYh9YnkEbRrPUMCY+NaOgBxvZRzGvqbGSMNckkVSdO67SwWR8bm4fo/qplXbUj0cSr229V6w=="], "chrome-devtools-frontend": ["chrome-devtools-frontend@1.0.1577886", "", {}, "sha512-B9hY3o/0RuVCDWNYh9YnkEbRrPUMCY+NaOgBxvZRzGvqbGSMNckkVSdO67SwWR8bm4fo/qplXbUj0cSr229V6w=="],
"chrome-devtools-mcp": ["chrome-devtools-mcp@0.20.3", "", { "bin": { "chrome-devtools-mcp": "build/src/bin/chrome-devtools-mcp.js", "chrome-devtools": "build/src/bin/chrome-devtools.js" } }, "sha512-6MlNKlKa+J1FX9w4SUnFERF4MRGWLlrnZvIJGhhsuuMPM7qUG0F4SwheRyjwl0+tsTemxMCBHiib8mXkg5j6og=="], "chrome-devtools-mcp": ["chrome-devtools-mcp@0.21.0", "", { "bin": { "chrome-devtools-mcp": "build/src/bin/chrome-devtools-mcp.js", "chrome-devtools": "build/src/bin/chrome-devtools.js" } }, "sha512-d+iqrRmcwpRFV3Q4DRCF2LCoq+WCRU3GhISKQ9v8g+1C2Uh8upj3urkjxNO4QIjhBMIYei/VQ1OQLFceby80Og=="],
"chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="], "chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="],

View File

@@ -80,3 +80,8 @@ export const CONTENT_LIMITS = {
CONSOLE_DEFAULT_LIMIT: 50, CONSOLE_DEFAULT_LIMIT: 50,
CONSOLE_MAX_LIMIT: 200, CONSOLE_MAX_LIMIT: 200,
} as const } as const
export const REFERRAL_LIMITS = {
MAX_DAILY_CREDITS: 500,
CREDITS_PER_REFERRAL: 200,
} as const