AkurAI Build
Menu

Public repository / e0753e49bfea7ef672aabf8df9a3a27b8bdd9dc7

AkurAI-Mail / src/lib/webmail/SignalBadge.svelte

Latest change aa05f301181c865428b6faf9c750f0631324c73a - feat: compute reply-needed, follow-up and priority signals for conversations (AKURAI-MAIL-7) by Ólafur Búi Ólafsson

<script lang="ts">
  import { Bell, Clock, Flag, MessageCircle } from "lucide-svelte";

  let { signalType, signal, onCorrect, onDismiss, convId, compact = false }: {
    signalType: string;
    signal: any;
    onCorrect?: (convId: string, type: string, action: string, value?: any) => void;
    onDismiss?: (convId: string, type: string, action: string) => void;
    convId: string;
    compact?: boolean;
  } = $props();

  const dismissed = $derived(signal?.dismissed ?? false);

  function iconFor(type: string) {
    if (type === "replyNeeded") return MessageCircle;
    if (type === "followUpDue") return Clock;
    if (type === "priority") return Flag;
    return Bell;
  }

  function labelFor(type: string) {
    if (type === "replyNeeded") return "Reply needed";
    if (type === "followUpDue") return "Follow-up";
    if (type === "priority") return signal?.value === "high" ? "High priority" : "Priority";
    return type;
  }

  function colorClass(type: string, value?: string) {
    if (type === "replyNeeded" && value) return "signal-amber";
    if (type === "followUpDue" && value) return "signal-blue";
    if (type === "priority" && value === "high") return "signal-red";
    return "signal-muted";
  }
</script>

{#if signal && !dismissed}
  <span class="signal-badge {colorClass(signalType, signal?.value)}" class:compact title={signal?.evidence?.join("; ") ?? ""}>
    {#if compact}
      {@const Icon = iconFor(signalType)}
      <Icon size={13} strokeWidth={2} />
    {:else}
      {@const Icon = iconFor(signalType)}
      <Icon size={14} strokeWidth={2} />
      <span>{labelFor(signalType)}</span>
      {#if signalType === "replyNeeded" && signal?.unansweredAge}
        <em>{signal.unansweredAge.days > 0 ? `${signal.unansweredAge.days}d` : `${signal.unansweredAge.hours}h`}</em>
      {/if}
      {#if onDismiss}
        <button class="signal-dismiss" aria-label="Dismiss" onclick={() => onDismiss(convId, signalType, "dismiss")}>&times;</button>
      {/if}
    {/if}
  </span>
{/if}

<style>
  .signal-badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 12px;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 5px;
    line-height: 1;
    white-space: nowrap;
  }
  .signal-badge.compact {
    padding: 2px 4px;
    gap: 0;
    border-radius: 4px;
    min-width: 0;
  }
  .signal-amber {
    background: #fff7e0;
    color: #9a6b00;
    border: 1px solid #f0d060;
  }
  .signal-blue {
    background: #e8f0fe;
    color: #1a56b4;
    border: 1px solid #b8d4f8;
  }
  .signal-red {
    background: #fdecea;
    color: #b3261e;
    border: 1px solid #f5c6c2;
  }
  .signal-muted {
    background: #f3f4f6;
    color: #61716c;
    border: 1px solid #dce4e1;
  }
  .signal-badge em {
    font-style: normal;
    opacity: 0.7;
    margin-left: 1px;
  }
  .signal-dismiss {
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
    padding: 0 2px;
    margin-left: 2px;
    font-size: 15px;
    line-height: 1;
    opacity: 0.5;
  }
  .signal-dismiss:hover {
    opacity: 1;
  }
</style>