Add XP progress
diff --git a/website/public/locales/en/leaderboard.json b/website/public/locales/en/leaderboard.json
index 68805e0426..8262746241 100644
--- a/website/public/locales/en/leaderboard.json
+++ b/website/public/locales/en/leaderboard.json
@@ -9,6 +9,7 @@
   "labels_simple": "Labels (simple)",
   "last_updated_at": "Last updated at: {{val, datetime}}",
   "leaderboard": "Leaderboard",
+  "level_progress_message": "With a total of {{score}} points, you have reached level {{level,number,integer}}!",
   "month": "Month",
   "monthly": "Monthly",
   "next": "Next",
@@ -19,6 +20,7 @@
   "prompt": "Prompts",
   "rank": "Rank",
   "rankings": "Rankings",
+  "reached_max_level": "You have reached max level, thank you for your hard work!",
   "replies_assistant": "Replies as Assistant",
   "replies_prompter": "Replies as Prompter",
   "reply": "Replies",
@@ -31,6 +33,7 @@
   "view_all": "View all",
   "week": "Week",
   "weekly": "Weekly",
+  "xp_progress_message": "You need {{need, number, integer}} more points to reach the next level!",
   "your_account": "Your account",
   "your_stats": "Your statistics"
 }
diff --git a/website/src/components/Account/XPBar.tsx b/website/src/components/Account/XPBar.tsx
new file mode 100644
index 0000000000..8be6dba463
--- /dev/null
+++ b/website/src/components/Account/XPBar.tsx
@@ -0,0 +1,51 @@
+import { Grid, GridItem, keyframes, Progress, Text } from "@chakra-ui/react";
+import { Icon } from "@chakra-ui/react";
+import { Star } from "lucide-react";
+import { useTranslation } from "next-i18next";
+import { useMemo } from "react";
+import { useUserScore } from "src/hooks/ui/useUserScore";
+
+const spin = keyframes`
+0% {transform: rotate(0deg);}
+100% {transform: rotate(360deg);}
+`;
+const starAnimation = `${spin} infinite 2.5s cubic-bezier(0.52, -0.43, 0, 1.04)`;
+
+export const XPBar = () => {
+  const { t } = useTranslation("leaderboard");
+  const { level, score, scoreUntilNextLevel, reachedMaxLevel } = useUserScore();
+
+  const nextLevelText = useMemo(() => {
+    if (reachedMaxLevel) {
+      return t("reached_max_level");
+    }
+    return t("xp_progress_message", { need: scoreUntilNextLevel });
+  }, [reachedMaxLevel, scoreUntilNextLevel, t]);
+
+  return (
+    <Grid
+      rowGap={1}
+      columnGap={6}
+      templateAreas={`
+       "star row1"
+       "star row2"
+       "star progress"
+    `}
+      gridTemplateColumns="auto 1fr"
+      alignItems="center"
+    >
+      <GridItem area="star" justifySelf="center">
+        <Icon as={Star} boxSize={20} fill="gold" color="gold" animation={starAnimation} />
+      </GridItem>
+      <GridItem area="row1">
+        <Text>{t("level_progress_message", { score, level })}</Text>
+      </GridItem>
+      <GridItem area="row2">
+        <Text>{nextLevelText}</Text>
+      </GridItem>
+      <GridItem area="progress">
+        <Progress hasStripe colorScheme="yellow" value={level} />
+      </GridItem>
+    </Grid>
+  );
+};
diff --git a/website/src/components/Header/UserScore.tsx b/website/src/components/Header/UserScore.tsx
index b7994bf97a..0b7dc70393 100644
--- a/website/src/components/Header/UserScore.tsx
+++ b/website/src/components/Header/UserScore.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from "next-i18next";
 import { useUserScore } from "src/hooks/ui/useUserScore";
 
 export const UserScore = () => {
-  const score = useUserScore();
+  const { score } = useUserScore();
   const { t } = useTranslation("leaderboard");
 
   if (!Number.isFinite(score)) {
diff --git a/website/src/hooks/ui/useUserScore.ts b/website/src/hooks/ui/useUserScore.ts
index 53ecf28594..999544eab2 100644
--- a/website/src/hooks/ui/useUserScore.ts
+++ b/website/src/hooks/ui/useUserScore.ts
@@ -3,6 +3,18 @@ import { get } from "src/lib/api";
 import { LeaderboardEntity, LeaderboardTimeFrame } from "src/types/Leaderboard";
 import uswSWRImmutable from "swr/immutable";
 
+// https://github.com/LAION-AI/Open-Assistant/issues/1957
+function* generateThresholds(baseline = 3, alpha = 1.1521, maxLevel = 100) {
+  let sum = 0;
+  yield sum;
+  for (let i = 1; i < maxLevel; i++) {
+    sum += i * alpha + baseline;
+    yield Math.round(sum);
+  }
+}
+
+const thresholds = Array.from(generateThresholds());
+
 export const useUserScore = () => {
   const { status } = useSession();
   const isLoggedIn = status === "authenticated";
@@ -14,6 +26,13 @@ export const useUserScore = () => {
       keepPreviousData: true,
     }
   );
-  const score: number | undefined = entries?.total?.leader_score;
-  return score;
+  const score = entries?.total?.leader_score ?? 0;
+  const level = entries?.total?.level ?? 0;
+
+  const currentLevelScore = thresholds[level];
+  const nextLevelScore = thresholds[level + 1] ?? Infinity;
+  const scoreUntilNextLevel = nextLevelScore - score;
+  const reachedMaxLevel = level === 100;
+
+  return { score, level, currentLevelScore, nextLevelScore, scoreUntilNextLevel, reachedMaxLevel };
 };
diff --git a/website/src/pages/account/index.tsx b/website/src/pages/account/index.tsx
index 59f40e17a2..ff4e94ffee 100644
--- a/website/src/pages/account/index.tsx
+++ b/website/src/pages/account/index.tsx
@@ -6,6 +6,7 @@ import React from "react";
 export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
 import { Pencil } from "lucide-react";
 import { useTranslation } from "next-i18next";
+import { XPBar } from "src/components/Account/XPBar";
 import { SurveyCard } from "src/components/Survey/SurveyCard";
 import { get } from "src/lib/api";
 import { getTypeSafei18nKey } from "src/lib/i18n";
@@ -51,6 +52,8 @@ export default function Account() {
               <Text as="b">Email</Text>
               <Text>{session.user.email ?? t("no_email")}</Text>
             </Grid>
+            <Divider my={4} />
+            <XPBar />
           </SurveyCard>
           <SurveyCard>
             <Title>{t("your_stats")}</Title>
diff --git a/website/src/pages/dashboard.tsx b/website/src/pages/dashboard.tsx
index c3ea14f8ea..10a49a8b83 100644
--- a/website/src/pages/dashboard.tsx
+++ b/website/src/pages/dashboard.tsx
@@ -1,4 +1,4 @@
-import { Flex } from "@chakra-ui/react";
+import { Card, CardBody, Flex } from "@chakra-ui/react";
 import Head from "next/head";
 import { useTranslation } from "next-i18next";
 import { useMemo } from "react";
@@ -7,6 +7,7 @@ import { getDashboardLayout } from "src/components/Layout";
 import { get } from "src/lib/api";
 import { AvailableTasks, TaskCategory } from "src/types/Task";
 export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
+import { XPBar } from "src/components/Account/XPBar";
 import { TaskCategoryItem } from "src/components/Dashboard/TaskOption";
 import { useCurrentLocale } from "src/hooks/locale/useCurrentLocale";
 import { API_ROUTES } from "src/lib/routes";
@@ -33,6 +34,11 @@ const Dashboard = () => {
       <Flex direction="column" gap="10">
         <WelcomeCard />
         <TaskOption content={availableTaskTypes} />
+        <Card>
+          <CardBody>
+            <XPBar />
+          </CardBody>
+        </Card>
         <LeaderboardWidget />
       </Flex>
     </>
diff --git a/website/src/types/Leaderboard.ts b/website/src/types/Leaderboard.ts
index d1a1218f6d..453cbb3e41 100644
--- a/website/src/types/Leaderboard.ts
+++ b/website/src/types/Leaderboard.ts
@@ -23,6 +23,7 @@ export interface LeaderboardEntity {
   auth_method: string;
   display_name: string;
   leader_score: number;
+  level: number; // between 0 and 100
   base_date: string;
   image?: string;
   modified_date: string;

Closes #1957 

![image](https://user-images.githubusercontent.com/24505302/225850374-24fe1a4b-3b3d-4252-9efa-f4b7f6829997.png)

The star is animated, we maybe want to consider [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) at some point.