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
540 changes: 122 additions & 418 deletions frontend/src/components/custom-rules.tsx

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions frontend/src/components/hero-metric-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Progress } from "@/components/ui/progress";
interface UsageStats {
productive_minutes: number;
neutral_minutes: number;
distractive_minutes: number;
distracting_minutes: number;
productivity_score: number;
}

Expand All @@ -22,7 +22,7 @@ export type DailyStats = {
date: number;
productive_minutes: number;
neutral_minutes: number;
distractive_minutes: number;
distracting_minutes: number;
};

interface MetricCardProps {
Expand Down Expand Up @@ -57,8 +57,8 @@ function getContextLabel(comparisonMode: ComparisonMode): string {
}
}

function calculateFocusScore(productive: number, distractive: number): number {
const total = productive + distractive;
function calculateFocusScore(productive: number, distracting: number): number {
const total = productive + distracting;
if (total === 0) return 100; // No activity = perfect focus (no distractions)
return (productive / total) * 100;
}
Expand Down Expand Up @@ -226,11 +226,11 @@ export function HeroMetricCards({
const totalActiveMinutes =
stats.productive_minutes +
stats.neutral_minutes +
stats.distractive_minutes;
stats.distracting_minutes;

const focusScore = Number.isFinite(stats.productivity_score)
? stats.productivity_score
: calculateFocusScore(stats.productive_minutes, stats.distractive_minutes);
: calculateFocusScore(stats.productive_minutes, stats.distracting_minutes);

return (
<div className="space-y-3">
Expand All @@ -242,8 +242,8 @@ export function HeroMetricCards({
contextLabel={contextLabel}
/>
<MetricCard
title="Distractive"
minutes={stats.distractive_minutes}
title="Distracting"
minutes={stats.distracting_minutes}
colorScheme="rose"
contextLabel={contextLabel}
/>
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/components/insights/bento-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const MIN_SECONDS_FOR_INSIGHTS = 3600;

const SERIES = [
{ key: "productive", field: "productive_seconds", label: "Productive", bg: "bg-emerald-500/80", dot: "bg-emerald-500", text: "text-emerald-400" },
{ key: "distractive", field: "distractive_seconds", label: "Distractive", bg: "bg-rose-500/80", dot: "bg-rose-500", text: "text-rose-400" },
{ key: "distracting", field: "distracting_seconds", label: "Distracting", bg: "bg-rose-500/80", dot: "bg-rose-500", text: "text-rose-400" },
{ key: "idle", field: "idle_seconds", label: "Idle", bg: "bg-zinc-400/60", dot: "bg-zinc-400", text: "text-zinc-400" },
{ key: "other", field: "other_seconds", label: "Other", bg: "bg-amber-400/60", dot: "bg-amber-400", text: "text-amber-400" },
] as const;
Expand All @@ -57,7 +57,7 @@ const buildHourlySlots = (breakdown: Record<string, ProductivityScore> | null |
return {
HourLabel: formatHourLabel(hour),
productive_seconds: score?.productive_seconds ?? 0,
distractive_seconds: score?.distractive_seconds ?? 0,
distracting_seconds: score?.distracting_seconds ?? 0,
idle_seconds: score?.idle_seconds ?? 0,
other_seconds: score?.other_seconds ?? 0,
};
Expand Down Expand Up @@ -112,7 +112,7 @@ function HourlyBreakdownChart({
}) {
const [visible, setVisible] = useState<Record<SeriesKey, boolean>>({
productive: true,
distractive: true,
distracting: true,
idle: false,
other: false,
});
Expand Down Expand Up @@ -164,9 +164,9 @@ function HourlyBreakdownChart({
);
}

// Stacking order top-to-bottom: distractive, other, idle, productive
// Stacking order top-to-bottom: distracting, other, idle, productive
const segments = [
{ ...SERIES[1], seconds: visible.distractive ? hour.distractive_seconds : 0 },
{ ...SERIES[1], seconds: visible.distracting ? hour.distracting_seconds : 0 },
{ ...SERIES[3], seconds: visible.other ? hour.other_seconds : 0 },
{ ...SERIES[2], seconds: visible.idle ? hour.idle_seconds : 0 },
{ ...SERIES[0], seconds: visible.productive ? hour.productive_seconds : 0 },
Expand Down Expand Up @@ -308,8 +308,8 @@ export function BentoDashboard() {
const isLoading = isStoreLoading || isQueryLoading;

const productiveSeconds = overview?.productivity_score?.productive_seconds ?? 0;
const distractiveSeconds = overview?.productivity_score?.distractive_seconds ?? 0;
const totalTrackedSeconds = productiveSeconds + distractiveSeconds;
const distractingSeconds = overview?.productivity_score?.distracting_seconds ?? 0;
const totalTrackedSeconds = productiveSeconds + distractingSeconds;
const hasEnoughData = totalTrackedSeconds >= MIN_SECONDS_FOR_INSIGHTS;

const focusScore = Math.round(overview?.productivity_score?.productivity_score ?? 0);
Expand Down Expand Up @@ -453,14 +453,14 @@ export function BentoDashboard() {
</CardContent>
</Card>

{/* Distractive Hours */}
{/* Distracting Hours */}
<Card className="bg-gradient-to-br from-rose-500/10 to-rose-600/5 border-rose-500/20">
<CardContent className="pt-6">
<p className="text-xs font-bold uppercase tracking-widest text-rose-400">
Distractive
Distracting
</p>
<p className="text-3xl font-bold text-rose-400 mt-1">
{formatDuration(distractiveSeconds)}
{formatDuration(distractingSeconds)}
</p>
<p className="text-xs text-muted-foreground mt-2">
Time lost
Expand Down
169 changes: 169 additions & 0 deletions frontend/src/components/rules-reference-sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { useMemo } from "react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { RULE_SNIPPETS } from "@/lib/rules/snippets";
import { CodeBlock } from "@/components/ui/code-block";
import { toast } from "sonner";

export function RulesReferenceSheet({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const examples = useMemo(
() => RULE_SNIPPETS.filter((snippet) => snippet.id !== "import-runtime"),
[]
);

const copySnippet = async (code: string) => {
try {
await navigator.clipboard.writeText(code);
toast.success("Example copied");
} catch {
toast.error("Could not copy example");
}
};

return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-full sm:max-w-lg p-0 flex flex-col">
<SheetHeader className="px-4 pt-4 pb-3 border-b border-border/50 shrink-0">
<SheetTitle className="text-sm">Docs & Examples</SheetTitle>
<SheetDescription className="text-xs">
Runtime API reference and ready-to-use rule snippets
</SheetDescription>
</SheetHeader>

<Tabs defaultValue="docs" className="flex-1 min-h-0 flex flex-col">
<div className="px-4 py-3 border-b border-border/30 shrink-0">
<TabsList className="grid w-full grid-cols-2 h-8">
<TabsTrigger value="docs" className="text-xs">Docs</TabsTrigger>
<TabsTrigger value="examples" className="text-xs">Examples</TabsTrigger>
</TabsList>
</div>

<TabsContent value="docs" className="m-0 flex-1 min-h-0">
<ScrollArea className="h-full">
<div className="p-4 space-y-4 text-xs">
<div className="space-y-1">
<p className="font-semibold text-foreground">Import</p>
<CodeBlock
height="120px"
code={`import {
productive, distracting, neutral,
block, allow, pause,
Timezone, runtime,
type Classify, type Enforce,
} from "@focusd/runtime";`}
/>
</div>

<div className="space-y-1">
<p className="font-semibold text-foreground">Function Signature</p>
<CodeBlock
height="50px"
code={`export function classify(): Classify | undefined;
export function enforcement(): Enforce | undefined;`}
/>
</div>

<div className="space-y-1 mt-2">
<p className="font-semibold text-foreground">Identity</p>
<ul className="space-y-1 text-muted-foreground font-mono text-[11px]">
<li>runtime.usage.app / title / domain / host / path / url</li>
<li>runtime.usage.classification</li>
</ul>
</div>

<div className="space-y-1">
<p className="font-semibold text-foreground">Day & Hour (Runtime)</p>
<ul className="space-y-1 text-muted-foreground font-mono text-[11px]">
<li>runtime.today.focusScore / runtime.today.productiveMinutes / runtime.today.distractingMinutes</li>
<li>runtime.hour.focusScore / runtime.hour.productiveMinutes / runtime.hour.distractingMinutes</li>
</ul>
</div>

<div className="space-y-1">
<p className="font-semibold text-foreground">Current App/Site</p>
<ul className="space-y-1 text-muted-foreground font-mono text-[11px]">
<li>runtime.usage.current.usedToday</li>
<li>runtime.usage.current.blocks</li>
<li>runtime.usage.current.sinceBlock</li>
<li>runtime.usage.current.usedSinceBlock</li>
<li>runtime.usage.current.last(60)</li>
</ul>
</div>

<div className="space-y-1">
<p className="font-semibold text-foreground">Migration</p>
<ul className="space-y-1 text-muted-foreground font-mono text-[11px]">
<li>Old style classify(usage) / enforcement(usage) is no longer supported.</li>
</ul>
</div>

<div className="space-y-1">
<p className="font-semibold text-foreground">Time (Runtime)</p>
<ul className="space-y-1 text-muted-foreground font-mono text-[11px]">
<li>runtime.time.now(Timezone.UTC)</li>
<li>runtime.time.day(Timezone.Europe_London)</li>
</ul>
</div>

<div className="space-y-1">
<p className="font-semibold text-foreground">Helpers</p>
<ul className="space-y-1 text-muted-foreground font-mono text-[11px]">
<li>productive(reason, tags?)</li>
<li>distracting(reason, tags?)</li>
<li>neutral(reason, tags?)</li>
<li>block(reason)</li>
<li>allow(reason)</li>
<li>pause(reason)</li>
</ul>
</div>
</div>
</ScrollArea>
</TabsContent>

<TabsContent value="examples" className="m-0 flex-1 min-h-0">
<ScrollArea className="h-full">
<div className="p-4 space-y-3">
{examples.map((example) => {
const lineCount = example.code.split('\n').length;
const estimatedHeight = Math.max(50, lineCount * 22) + 20 + "px"; // 22px per line + padding

return (
<div key={example.id} className="rounded-lg border border-border/50 bg-card/50 p-3 space-y-2">
<div>
<p className="text-xs font-semibold text-foreground">{example.title}</p>
<p className="text-[11px] text-muted-foreground">{example.description}</p>
</div>
<CodeBlock code={example.code} height={estimatedHeight} />
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] mt-1"
onClick={() => copySnippet(example.code)}
>
Copy Snippet
</Button>
</div>
);
})}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</SheetContent>
</Sheet>
);
}
44 changes: 44 additions & 0 deletions frontend/src/components/ui/code-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Editor, { type Monaco } from "@monaco-editor/react";
import { useCallback } from "react";
import { RUNTIME_TYPES_FILE_PATH, RUNTIME_TYPES_SOURCE } from "@/lib/rules/runtime-types";

export function CodeBlock({ code, height = "100px" }: { code: string; height?: string }) {
const handleEditorWillMount = useCallback((monaco: Monaco) => {
monaco.languages.typescript.typescriptDefaults.addExtraLib(RUNTIME_TYPES_SOURCE, RUNTIME_TYPES_FILE_PATH);

const typesUri = monaco.Uri.parse(RUNTIME_TYPES_FILE_PATH);
if (!monaco.editor.getModel(typesUri)) {
monaco.editor.createModel(RUNTIME_TYPES_SOURCE, "typescript", typesUri);
}
}, []);

return (
<div className="rounded-md overflow-hidden border border-border/50 bg-[#1e1e1e] ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
<Editor
value={code}
height={height}
language="typescript"
theme="vs-dark"
beforeMount={handleEditorWillMount}
options={{
readOnly: true,
minimap: { enabled: false },
lineNumbers: "off",
scrollBeyondLastLine: false,
folding: false,
wordWrap: "on",
padding: { top: 8, bottom: 8 },
fontSize: 12,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Source Code Pro', monospace",
renderLineHighlight: "none",
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
scrollbar: {
vertical: "hidden",
horizontal: "hidden"
}
}}
/>
</div>
);
}
14 changes: 7 additions & 7 deletions frontend/src/components/weekly-trend-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ export type DailyStats = {
date: number;
productive_minutes: number;
neutral_minutes: number;
distractive_minutes: number;
distracting_minutes: number;
};

const trendChartConfig = {
productive: { label: "Productive", color: "#22c55e" },
neutral: { label: "Neutral", color: "#eab308" },
distractive: { label: "Distractive", color: "#ef4444" },
distracting: { label: "Distracting", color: "#ef4444" },
} satisfies ChartConfig;

interface WeeklyTrendChartProps {
Expand All @@ -46,13 +46,13 @@ function generateMockData(): DailyStats[] {
// Generate varied but realistic mock data
const productiveBase = 240 + Math.floor(Math.random() * 180); // 4-7 hours
const neutralBase = 30 + Math.floor(Math.random() * 60); // 0.5-1.5 hours
const distractiveBase = 20 + Math.floor(Math.random() * 80); // 0.3-1.6 hours
const distractingBase = 20 + Math.floor(Math.random() * 80); // 0.3-1.6 hours

data.push({
date: Math.floor(date.getTime() / 1000),
productive_minutes: productiveBase,
neutral_minutes: neutralBase,
distractive_minutes: distractiveBase,
distracting_minutes: distractingBase,
});
}

Expand Down Expand Up @@ -88,7 +88,7 @@ export function WeeklyTrendChart({
day: getDayName(day.date),
productive: minutesToHours(day.productive_minutes),
neutral: minutesToHours(day.neutral_minutes),
distractive: minutesToHours(day.distractive_minutes),
distracting: minutesToHours(day.distracting_minutes),
}));

return (
Expand Down Expand Up @@ -144,9 +144,9 @@ export function WeeklyTrendChart({
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="distractive"
dataKey="distracting"
stackId="1"
fill="var(--color-distractive)"
fill="var(--color-distracting)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
Expand Down
Loading
Loading