Custom welcome message (#234)

* json

* connect

* useWelcomMessage

* Update shared/src/messages/jobs-welcome.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Shaheer Sarfaraz
2026-02-25 02:59:38 +00:00
committed by GitHub
co-authored by Copilot
parent 7514aa1b28
commit 26dbed15b9
3 changed files with 456 additions and 36 deletions
@@ -0,0 +1,57 @@
import welcomeMessages from "@shared/messages/jobs-welcome.json";
import { useMemo } from "react";
import { useProfile } from "./useProfile";
function hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
export function useWelcomeMessage(): string {
const { personName } = useProfile();
return useMemo(() => {
const firstName = personName?.split(" ")[0] || "User";
const today = new Date().toDateString();
let isFirstDay = true;
try {
let firstSeenDate = localStorage.getItem("jobOps_firstWelcomeDate");
if (!firstSeenDate) {
firstSeenDate = today;
localStorage.setItem("jobOps_firstWelcomeDate", today);
}
isFirstDay = firstSeenDate === today;
} catch (_e) {
// Ignore localStorage errors (e.g. private mode restrictions)
// Fallback to true so we just show the first message
}
const lines = welcomeMessages.lines;
let selectedIndex = 0; // Always default to the first message
if (!isFirstDay) {
// If it's not their first day, randomize consistently for the day
const seed = Math.abs(hashCode(`${firstName}-${today}`));
selectedIndex = seed % lines.length;
}
const line = lines[selectedIndex];
switch (line.placement) {
case "inline":
return line.text.replace("{name}", firstName);
case "prefix":
return `${firstName}, ${line.text}`;
case "suffix":
return `${line.text}, ${firstName}.`;
default:
return line.text;
}
}, [personName]);
}
@@ -1,4 +1,5 @@
import { PipelineProgress } from "@client/components";
import { useWelcomeMessage } from "@client/hooks/useWelcomeMessage";
import type { JobStatus } from "@shared/types.js";
import type React from "react";
@@ -8,15 +9,14 @@ interface OrchestratorSummaryProps {
}
export const OrchestratorSummary: React.FC<OrchestratorSummaryProps> = ({
stats,
isPipelineRunning,
}) => {
const totalJobs = Object.values(stats).reduce((a, b) => a + b, 0);
const welcomeText = useWelcomeMessage();
return (
<section className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight">Jobs</h1>
<h1 className="text-lg font-medium tracking-tight">{welcomeText}</h1>
</div>
{isPipelineRunning && (
@@ -24,39 +24,6 @@ export const OrchestratorSummary: React.FC<OrchestratorSummaryProps> = ({
<PipelineProgress isRunning={isPipelineRunning} />
</div>
)}
{/* Compact metrics summary - demoted visual weight */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground/80">
<span>
<span className="tabular-nums">{stats.ready}</span> ready
</span>
<span className="text-border"></span>
<span>
<span className="tabular-nums">
{stats.discovered + stats.processing}
</span>{" "}
discovered
</span>
<span className="text-border"></span>
<span>
<span className="tabular-nums">{stats.applied}</span> applied
</span>
<span className="text-border"></span>
<span className="font-medium text-foreground/60">
{totalJobs} jobs total
</span>
{(stats.skipped > 0 || stats.expired > 0) && (
<>
<span className="text-border"></span>
<span className="text-muted-foreground/60">
<span className="tabular-nums">
{stats.skipped + stats.expired}
</span>{" "}
skipped
</span>
</>
)}
</div>
</section>
);
};