mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
filter low quality emails
This commit is contained in:
parent
e76febc3a3
commit
bcdb193947
3 changed files with 351 additions and 238 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as React from "react"
|
||||
import { useState, useEffect, useCallback, useMemo } from "react"
|
||||
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Tags, ChevronRight, Plus, X } from "lucide-react"
|
||||
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Tags, Mail, BookOpen, ChevronRight, Plus, X } from "lucide-react"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTheme } from "@/contexts/theme-context"
|
||||
import { toast } from "sonner"
|
||||
|
|
@ -700,11 +701,15 @@ interface TagDef {
|
|||
applicability: "email" | "notes" | "both"
|
||||
description: string
|
||||
example?: string
|
||||
noteEffect?: "create" | "skip" | "none"
|
||||
}
|
||||
|
||||
const TAG_TYPE_ORDER = [
|
||||
"relationship", "relationship-sub", "topic", "email-type",
|
||||
"filter", "action", "status", "source",
|
||||
const NOTE_TAG_TYPE_ORDER = [
|
||||
"relationship", "relationship-sub", "topic", "action", "status", "source",
|
||||
]
|
||||
|
||||
const EMAIL_TAG_TYPE_ORDER = [
|
||||
"relationship", "topic", "email-type", "filter", "action", "status",
|
||||
]
|
||||
|
||||
const TAG_TYPE_LABELS: Record<string, string> = {
|
||||
|
|
@ -719,73 +724,183 @@ const TAG_TYPE_LABELS: Record<string, string> = {
|
|||
}
|
||||
|
||||
const DEFAULT_TAGS: TagDef[] = [
|
||||
{ tag: "investor", type: "relationship", applicability: "both", description: "Investors, VCs, or angels", example: "Following up on our meeting — we'd like to move forward with the Series A term sheet." },
|
||||
{ tag: "customer", type: "relationship", applicability: "both", description: "Paying customers", example: "We're seeing great results with Rowboat. Can we discuss expanding to more teams?" },
|
||||
{ tag: "prospect", type: "relationship", applicability: "both", description: "Potential customers", example: "Thanks for the demo yesterday. We're interested in starting a pilot." },
|
||||
{ tag: "partner", type: "relationship", applicability: "both", description: "Business partners", example: "Let's discuss how we can promote the integration to both our user bases." },
|
||||
{ tag: "vendor", type: "relationship", applicability: "both", description: "Service providers you work with", example: "Here are the updated employment agreements you requested." },
|
||||
{ tag: "product", type: "relationship", applicability: "both", description: "Products or services you use (automated)", example: "Your AWS bill for January 2025 is now available." },
|
||||
{ tag: "candidate", type: "relationship", applicability: "both", description: "Job applicants", example: "Thanks for reaching out. I'd love to learn more about the engineering role." },
|
||||
{ tag: "team", type: "relationship", applicability: "both", description: "Internal team members", example: "Here's the updated roadmap for Q2. Let's discuss in our sync." },
|
||||
{ tag: "advisor", type: "relationship", applicability: "both", description: "Advisors, mentors, or board members", example: "I've reviewed the deck. Here are my thoughts on the GTM strategy." },
|
||||
{ tag: "personal", type: "relationship", applicability: "both", description: "Family or friends", example: "Are you coming to Thanksgiving this year? Let me know your travel dates." },
|
||||
{ tag: "press", type: "relationship", applicability: "both", description: "Journalists or media", example: "I'm writing a piece on AI agents. Would you be available for an interview?" },
|
||||
{ tag: "community", type: "relationship", applicability: "both", description: "Users, peers, or open source contributors", example: "Love what you're building with Rowboat. Here's a bug I found..." },
|
||||
{ tag: "government", type: "relationship", applicability: "both", description: "Government agencies", example: "Your Delaware franchise tax is due by March 1, 2025." },
|
||||
{ tag: "primary", type: "relationship-sub", applicability: "notes", description: "Main contact or decision maker", example: "Sarah Chen — VP Engineering, your main point of contact at Acme." },
|
||||
{ tag: "secondary", type: "relationship-sub", applicability: "notes", description: "Supporting contact, involved but not the lead", example: "David Kim — Engineer CC'd on customer emails." },
|
||||
{ tag: "executive-assistant", type: "relationship-sub", applicability: "notes", description: "EA or admin handling scheduling and logistics", example: "Lisa — Sarah's EA who schedules all her meetings." },
|
||||
{ tag: "cc", type: "relationship-sub", applicability: "notes", description: "Person who's CC'd but not actively engaged", example: "Manager looped in for visibility on deal." },
|
||||
{ tag: "referred-by", type: "relationship-sub", applicability: "notes", description: "Person who made an introduction or referral", example: "David Park — Investor who intro'd you to Sarah." },
|
||||
{ tag: "former", type: "relationship-sub", applicability: "notes", description: "Previously held this relationship, no longer active", example: "John — Former customer who churned last year." },
|
||||
{ tag: "champion", type: "relationship-sub", applicability: "notes", description: "Internal advocate pushing for you", example: "Engineer who loves your product and is selling internally." },
|
||||
{ tag: "blocker", type: "relationship-sub", applicability: "notes", description: "Person opposing or blocking progress", example: "CFO resistant to spending on new tools." },
|
||||
{ tag: "sales", type: "topic", applicability: "both", description: "Sales conversations, deals, and revenue", example: "Here's the pricing proposal we discussed. Let me know if you have questions." },
|
||||
{ tag: "support", type: "topic", applicability: "both", description: "Help requests, issues, and customer support", example: "We're seeing an error when trying to export. Can you help?" },
|
||||
{ tag: "legal", type: "topic", applicability: "both", description: "Contracts, terms, compliance, and legal matters", example: "Legal has reviewed the MSA. Attached are our requested changes." },
|
||||
{ tag: "finance", type: "topic", applicability: "both", description: "Money, invoices, payments, banking, and taxes", example: "Your invoice #1234 for $5,000 is attached. Payment due in 30 days." },
|
||||
{ tag: "hiring", type: "topic", applicability: "both", description: "Recruiting, interviews, and employment", example: "We'd like to move forward with a final round interview. Are you available Thursday?" },
|
||||
{ tag: "fundraising", type: "topic", applicability: "both", description: "Raising money and investor relations", example: "Thanks for sending the deck. We'd like to schedule a partner meeting." },
|
||||
{ tag: "travel", type: "topic", applicability: "both", description: "Flights, hotels, trips, and travel logistics", example: "Your flight to Tokyo on March 15 is confirmed. Confirmation #ABC123." },
|
||||
{ tag: "event", type: "topic", applicability: "both", description: "Conferences, meetups, and gatherings", example: "You're invited to speak at TechCrunch Disrupt. Can you confirm your availability?" },
|
||||
{ tag: "shopping", type: "topic", applicability: "both", description: "Purchases, orders, and returns", example: "Your order #12345 has shipped. Track it here." },
|
||||
{ tag: "health", type: "topic", applicability: "both", description: "Medical, wellness, and health-related matters", example: "Your appointment with Dr. Smith is confirmed for Monday at 2pm." },
|
||||
{ tag: "learning", type: "topic", applicability: "both", description: "Courses, education, and skill-building", example: "Welcome to the Advanced Python course. Here's your access link." },
|
||||
{ tag: "research", type: "topic", applicability: "both", description: "Research requests and information gathering", example: "Here's the market analysis you requested on the AI agent space." },
|
||||
{ tag: "intro", type: "email-type", applicability: "both", description: "Warm introduction from someone you know", example: "I'd like to introduce you to Sarah Chen, VP Engineering at Acme." },
|
||||
{ tag: "followup", type: "email-type", applicability: "both", description: "Following up on a previous conversation", example: "Following up on our call last week. Have you had a chance to review the proposal?" },
|
||||
{ tag: "scheduling", type: "email-type", applicability: "email", description: "Meeting and calendar scheduling", example: "Are you available for a call next Tuesday at 2pm?" },
|
||||
{ tag: "cold-outreach", type: "email-type", applicability: "email", description: "Unsolicited contact from someone you don't know", example: "Hi, I noticed your company is growing fast. I'd love to show you how we can help with..." },
|
||||
{ tag: "newsletter", type: "email-type", applicability: "email", description: "Newsletters, marketing emails, and subscriptions", example: "This week in AI: The latest developments in agent frameworks..." },
|
||||
{ tag: "notification", type: "email-type", applicability: "email", description: "Automated alerts, receipts, and system notifications", example: "Your password was changed successfully. If this wasn't you, contact support." },
|
||||
{ tag: "spam", type: "filter", applicability: "email", description: "Junk and unwanted email", example: "Congratulations! You've won $1,000,000..." },
|
||||
{ tag: "promotion", type: "filter", applicability: "email", description: "Marketing offers and sales pitches", example: "50% off all items this weekend only!" },
|
||||
{ tag: "social", type: "filter", applicability: "email", description: "Social media notifications", example: "John Smith commented on your post." },
|
||||
{ tag: "forums", type: "filter", applicability: "email", description: "Mailing lists and group discussions", example: "Re: [dev-list] Question about API design" },
|
||||
{ tag: "action-required", type: "action", applicability: "both", description: "Needs a response or action from you", example: "Can you send me the pricing by Friday?" },
|
||||
{ tag: "fyi", type: "action", applicability: "email", description: "Informational only, no action needed", example: "Just wanted to let you know the deal closed. Thanks for your help!" },
|
||||
{ tag: "urgent", type: "action", applicability: "both", description: "Time-sensitive, needs immediate attention", example: "We need your signature on the contract by EOD today or we lose the deal." },
|
||||
{ tag: "waiting", type: "action", applicability: "both", description: "Waiting on a response from them" },
|
||||
{ tag: "unread", type: "status", applicability: "email", description: "Not yet processed" },
|
||||
{ tag: "to-reply", type: "status", applicability: "email", description: "Need to respond" },
|
||||
{ tag: "done", type: "status", applicability: "email", description: "Handled, can be archived" },
|
||||
{ tag: "active", type: "status", applicability: "notes", description: "Currently relevant, recent activity" },
|
||||
{ tag: "archived", type: "status", applicability: "notes", description: "No longer active, kept for reference" },
|
||||
{ tag: "stale", type: "status", applicability: "notes", description: "No activity in 60+ days, needs attention or archive" },
|
||||
{ tag: "email", type: "source", applicability: "notes", description: "Created or updated from email" },
|
||||
{ tag: "meeting", type: "source", applicability: "notes", description: "Created or updated from meeting transcript" },
|
||||
{ tag: "browser", type: "source", applicability: "notes", description: "Content captured from web browsing" },
|
||||
{ tag: "web-search", type: "source", applicability: "notes", description: "Information from web search" },
|
||||
{ tag: "manual", type: "source", applicability: "notes", description: "Manually entered by user" },
|
||||
{ tag: "import", type: "source", applicability: "notes", description: "Imported from another system" },
|
||||
{ tag: "investor", type: "relationship", applicability: "both", noteEffect: "create", description: "Investors, VCs, or angels", example: "Following up on our meeting — we'd like to move forward with the Series A term sheet." },
|
||||
{ tag: "customer", type: "relationship", applicability: "both", noteEffect: "create", description: "Paying customers", example: "We're seeing great results with Rowboat. Can we discuss expanding to more teams?" },
|
||||
{ tag: "prospect", type: "relationship", applicability: "both", noteEffect: "create", description: "Potential customers", example: "Thanks for the demo yesterday. We're interested in starting a pilot." },
|
||||
{ tag: "partner", type: "relationship", applicability: "both", noteEffect: "create", description: "Business partners", example: "Let's discuss how we can promote the integration to both our user bases." },
|
||||
{ tag: "vendor", type: "relationship", applicability: "both", noteEffect: "create", description: "Service providers you work with", example: "Here are the updated employment agreements you requested." },
|
||||
{ tag: "product", type: "relationship", applicability: "both", noteEffect: "skip", description: "Products or services you use (automated)", example: "Your AWS bill for January 2025 is now available." },
|
||||
{ tag: "candidate", type: "relationship", applicability: "both", noteEffect: "create", description: "Job applicants", example: "Thanks for reaching out. I'd love to learn more about the engineering role." },
|
||||
{ tag: "team", type: "relationship", applicability: "both", noteEffect: "create", description: "Internal team members", example: "Here's the updated roadmap for Q2. Let's discuss in our sync." },
|
||||
{ tag: "advisor", type: "relationship", applicability: "both", noteEffect: "create", description: "Advisors, mentors, or board members", example: "I've reviewed the deck. Here are my thoughts on the GTM strategy." },
|
||||
{ tag: "personal", type: "relationship", applicability: "both", noteEffect: "create", description: "Family or friends", example: "Are you coming to Thanksgiving this year? Let me know your travel dates." },
|
||||
{ tag: "press", type: "relationship", applicability: "both", noteEffect: "create", description: "Journalists or media", example: "I'm writing a piece on AI agents. Would you be available for an interview?" },
|
||||
{ tag: "community", type: "relationship", applicability: "both", noteEffect: "create", description: "Users, peers, or open source contributors", example: "Love what you're building with Rowboat. Here's a bug I found..." },
|
||||
{ tag: "government", type: "relationship", applicability: "both", noteEffect: "create", description: "Government agencies", example: "Your Delaware franchise tax is due by March 1, 2025." },
|
||||
{ tag: "primary", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Main contact or decision maker", example: "Sarah Chen — VP Engineering, your main point of contact at Acme." },
|
||||
{ tag: "secondary", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Supporting contact, involved but not the lead", example: "David Kim — Engineer CC'd on customer emails." },
|
||||
{ tag: "executive-assistant", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "EA or admin handling scheduling and logistics", example: "Lisa — Sarah's EA who schedules all her meetings." },
|
||||
{ tag: "cc", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Person who's CC'd but not actively engaged", example: "Manager looped in for visibility on deal." },
|
||||
{ tag: "referred-by", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Person who made an introduction or referral", example: "David Park — Investor who intro'd you to Sarah." },
|
||||
{ tag: "former", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Previously held this relationship, no longer active", example: "John — Former customer who churned last year." },
|
||||
{ tag: "champion", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Internal advocate pushing for you", example: "Engineer who loves your product and is selling internally." },
|
||||
{ tag: "blocker", type: "relationship-sub", applicability: "notes", noteEffect: "none", description: "Person opposing or blocking progress", example: "CFO resistant to spending on new tools." },
|
||||
{ tag: "sales", type: "topic", applicability: "both", noteEffect: "create", description: "Sales conversations, deals, and revenue", example: "Here's the pricing proposal we discussed. Let me know if you have questions." },
|
||||
{ tag: "support", type: "topic", applicability: "both", noteEffect: "create", description: "Help requests, issues, and customer support", example: "We're seeing an error when trying to export. Can you help?" },
|
||||
{ tag: "legal", type: "topic", applicability: "both", noteEffect: "create", description: "Contracts, terms, compliance, and legal matters", example: "Legal has reviewed the MSA. Attached are our requested changes." },
|
||||
{ tag: "finance", type: "topic", applicability: "both", noteEffect: "create", description: "Money, invoices, payments, banking, and taxes", example: "Your invoice #1234 for $5,000 is attached. Payment due in 30 days." },
|
||||
{ tag: "hiring", type: "topic", applicability: "both", noteEffect: "create", description: "Recruiting, interviews, and employment", example: "We'd like to move forward with a final round interview. Are you available Thursday?" },
|
||||
{ tag: "fundraising", type: "topic", applicability: "both", noteEffect: "create", description: "Raising money and investor relations", example: "Thanks for sending the deck. We'd like to schedule a partner meeting." },
|
||||
{ tag: "travel", type: "topic", applicability: "both", noteEffect: "skip", description: "Flights, hotels, trips, and travel logistics", example: "Your flight to Tokyo on March 15 is confirmed. Confirmation #ABC123." },
|
||||
{ tag: "event", type: "topic", applicability: "both", noteEffect: "create", description: "Conferences, meetups, and gatherings", example: "You're invited to speak at TechCrunch Disrupt. Can you confirm your availability?" },
|
||||
{ tag: "shopping", type: "topic", applicability: "both", noteEffect: "skip", description: "Purchases, orders, and returns", example: "Your order #12345 has shipped. Track it here." },
|
||||
{ tag: "health", type: "topic", applicability: "both", noteEffect: "skip", description: "Medical, wellness, and health-related matters", example: "Your appointment with Dr. Smith is confirmed for Monday at 2pm." },
|
||||
{ tag: "learning", type: "topic", applicability: "both", noteEffect: "skip", description: "Courses, education, and skill-building", example: "Welcome to the Advanced Python course. Here's your access link." },
|
||||
{ tag: "research", type: "topic", applicability: "both", noteEffect: "create", description: "Research requests and information gathering", example: "Here's the market analysis you requested on the AI agent space." },
|
||||
{ tag: "intro", type: "email-type", applicability: "both", noteEffect: "create", description: "Warm introduction from someone you know", example: "I'd like to introduce you to Sarah Chen, VP Engineering at Acme." },
|
||||
{ tag: "followup", type: "email-type", applicability: "both", noteEffect: "create", description: "Following up on a previous conversation", example: "Following up on our call last week. Have you had a chance to review the proposal?" },
|
||||
{ tag: "scheduling", type: "email-type", applicability: "email", noteEffect: "skip", description: "Meeting and calendar scheduling", example: "Are you available for a call next Tuesday at 2pm?" },
|
||||
{ tag: "cold-outreach", type: "email-type", applicability: "email", noteEffect: "skip", description: "Unsolicited contact from someone you don't know", example: "Hi, I noticed your company is growing fast. I'd love to show you how we can help with..." },
|
||||
{ tag: "newsletter", type: "email-type", applicability: "email", noteEffect: "skip", description: "Newsletters, marketing emails, and subscriptions", example: "This week in AI: The latest developments in agent frameworks..." },
|
||||
{ tag: "notification", type: "email-type", applicability: "email", noteEffect: "skip", description: "Automated alerts, receipts, and system notifications", example: "Your password was changed successfully. If this wasn't you, contact support." },
|
||||
{ tag: "spam", type: "filter", applicability: "email", noteEffect: "skip", description: "Junk and unwanted email", example: "Congratulations! You've won $1,000,000..." },
|
||||
{ tag: "promotion", type: "filter", applicability: "email", noteEffect: "skip", description: "Marketing offers and sales pitches", example: "50% off all items this weekend only!" },
|
||||
{ tag: "social", type: "filter", applicability: "email", noteEffect: "skip", description: "Social media notifications", example: "John Smith commented on your post." },
|
||||
{ tag: "forums", type: "filter", applicability: "email", noteEffect: "skip", description: "Mailing lists and group discussions", example: "Re: [dev-list] Question about API design" },
|
||||
{ tag: "action-required", type: "action", applicability: "both", noteEffect: "create", description: "Needs a response or action from you", example: "Can you send me the pricing by Friday?" },
|
||||
{ tag: "fyi", type: "action", applicability: "email", noteEffect: "skip", description: "Informational only, no action needed", example: "Just wanted to let you know the deal closed. Thanks for your help!" },
|
||||
{ tag: "urgent", type: "action", applicability: "both", noteEffect: "create", description: "Time-sensitive, needs immediate attention", example: "We need your signature on the contract by EOD today or we lose the deal." },
|
||||
{ tag: "waiting", type: "action", applicability: "both", noteEffect: "create", description: "Waiting on a response from them" },
|
||||
{ tag: "unread", type: "status", applicability: "email", noteEffect: "none", description: "Not yet processed" },
|
||||
{ tag: "to-reply", type: "status", applicability: "email", noteEffect: "none", description: "Need to respond" },
|
||||
{ tag: "done", type: "status", applicability: "email", noteEffect: "none", description: "Handled, can be archived" },
|
||||
{ tag: "active", type: "status", applicability: "notes", noteEffect: "none", description: "Currently relevant, recent activity" },
|
||||
{ tag: "archived", type: "status", applicability: "notes", noteEffect: "none", description: "No longer active, kept for reference" },
|
||||
{ tag: "stale", type: "status", applicability: "notes", noteEffect: "none", description: "No activity in 60+ days, needs attention or archive" },
|
||||
{ tag: "email", type: "source", applicability: "notes", noteEffect: "none", description: "Created or updated from email" },
|
||||
{ tag: "meeting", type: "source", applicability: "notes", noteEffect: "none", description: "Created or updated from meeting transcript" },
|
||||
{ tag: "browser", type: "source", applicability: "notes", noteEffect: "none", description: "Content captured from web browsing" },
|
||||
{ tag: "web-search", type: "source", applicability: "notes", noteEffect: "none", description: "Information from web search" },
|
||||
{ tag: "manual", type: "source", applicability: "notes", noteEffect: "none", description: "Manually entered by user" },
|
||||
{ tag: "import", type: "source", applicability: "notes", noteEffect: "none", description: "Imported from another system" },
|
||||
]
|
||||
|
||||
function TagGroupTable({
|
||||
group,
|
||||
tags,
|
||||
collapsed,
|
||||
onToggle,
|
||||
onAdd,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
getGlobalIndex,
|
||||
isEmail,
|
||||
}: {
|
||||
group: { type: string; label: string; tags: TagDef[] }
|
||||
tags: TagDef[]
|
||||
collapsed: boolean
|
||||
onToggle: () => void
|
||||
onAdd: () => void
|
||||
onUpdate: (index: number, field: keyof TagDef, value: string | boolean) => void
|
||||
onRemove: (index: number) => void
|
||||
getGlobalIndex: (type: string, localIndex: number) => number
|
||||
isEmail: boolean
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronRight className={cn("size-3.5 transition-transform", !collapsed && "rotate-90")} />
|
||||
{group.label}
|
||||
<span className="text-[10px] ml-0.5">({group.tags.length})</span>
|
||||
</button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={onAdd}
|
||||
>
|
||||
<Plus className="size-3 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{!collapsed && group.tags.length > 0 && (
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<div className={cn(
|
||||
"gap-1 bg-muted/50 px-2 py-1 text-[10px] font-medium text-muted-foreground uppercase tracking-wider grid",
|
||||
isEmail ? "grid-cols-[100px_1fr_1fr_60px_24px]" : "grid-cols-[100px_1fr_1fr_24px]"
|
||||
)}>
|
||||
<div>Label</div>
|
||||
<div>Description</div>
|
||||
<div>Example</div>
|
||||
{isEmail && <div className="text-center" title="Emails with this label will be excluded from creating notes">Skip notes</div>}
|
||||
<div />
|
||||
</div>
|
||||
{group.tags.map((tag, localIdx) => {
|
||||
const globalIdx = getGlobalIndex(group.type, localIdx)
|
||||
return (
|
||||
<div key={globalIdx} className={cn(
|
||||
"gap-1 border-t px-2 py-0.5 items-center grid",
|
||||
isEmail ? "grid-cols-[100px_1fr_1fr_60px_24px]" : "grid-cols-[100px_1fr_1fr_24px]"
|
||||
)}>
|
||||
<Input
|
||||
value={tag.tag}
|
||||
onChange={e => onUpdate(globalIdx, "tag", e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder="tag-name"
|
||||
title={tag.tag}
|
||||
/>
|
||||
<Input
|
||||
value={tag.description}
|
||||
onChange={e => onUpdate(globalIdx, "description", e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder="Description"
|
||||
title={tag.description}
|
||||
/>
|
||||
<Input
|
||||
value={tag.example || ""}
|
||||
onChange={e => onUpdate(globalIdx, "example", e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder="Example"
|
||||
title={tag.example || ""}
|
||||
/>
|
||||
{isEmail && (
|
||||
<div className="flex justify-center">
|
||||
<Switch
|
||||
checked={tag.noteEffect === "skip"}
|
||||
onCheckedChange={checked => onUpdate(globalIdx, "noteEffect", checked ? "skip" : "create")}
|
||||
className="scale-75"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onRemove(globalIdx)}
|
||||
className="flex items-center justify-center text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!collapsed && group.tags.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground italic px-2">No tags in this group</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
||||
const [tags, setTags] = useState<TagDef[]>([])
|
||||
const [originalTags, setOriginalTags] = useState<TagDef[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set())
|
||||
const [activeSection, setActiveSection] = useState<"notes" | "email">("notes")
|
||||
|
||||
const hasChanges = JSON.stringify(tags) !== JSON.stringify(originalTags)
|
||||
|
||||
|
|
@ -808,14 +923,30 @@ function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
load()
|
||||
}, [dialogOpen])
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const noteGroups = useMemo(() => {
|
||||
const map = new Map<string, TagDef[]>()
|
||||
for (const tag of tags) {
|
||||
if (tag.applicability === "email") continue
|
||||
const list = map.get(tag.type) ?? []
|
||||
list.push(tag)
|
||||
map.set(tag.type, list)
|
||||
}
|
||||
return TAG_TYPE_ORDER.map(type => ({
|
||||
return NOTE_TAG_TYPE_ORDER.filter(type => map.has(type)).map(type => ({
|
||||
type,
|
||||
label: TAG_TYPE_LABELS[type],
|
||||
tags: map.get(type) ?? [],
|
||||
}))
|
||||
}, [tags])
|
||||
|
||||
const emailGroups = useMemo(() => {
|
||||
const map = new Map<string, TagDef[]>()
|
||||
for (const tag of tags) {
|
||||
if (tag.applicability === "notes") continue
|
||||
const list = map.get(tag.type) ?? []
|
||||
list.push(tag)
|
||||
map.set(tag.type, list)
|
||||
}
|
||||
return EMAIL_TAG_TYPE_ORDER.filter(type => map.has(type)).map(type => ({
|
||||
type,
|
||||
label: TAG_TYPE_LABELS[type],
|
||||
tags: map.get(type) ?? [],
|
||||
|
|
@ -833,7 +964,7 @@ function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
return -1
|
||||
}, [tags])
|
||||
|
||||
const updateTag = useCallback((index: number, field: keyof TagDef, value: string) => {
|
||||
const updateTag = useCallback((index: number, field: keyof TagDef, value: string | boolean) => {
|
||||
setTags(prev => prev.map((t, i) => i === index ? { ...t, [field]: value } : t))
|
||||
}, [])
|
||||
|
||||
|
|
@ -842,14 +973,30 @@ function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
}, [])
|
||||
|
||||
const addTag = useCallback((type: string) => {
|
||||
const newTag: TagDef = { tag: "", type, applicability: "both", description: "" }
|
||||
const isEmailSection = activeSection === "email"
|
||||
const applicability = isEmailSection ? "email" as const : "notes" as const
|
||||
// For email-only types, always use "email"; for notes-only types, always use "notes"; otherwise use "both"
|
||||
const emailOnlyTypes = ["email-type", "filter"]
|
||||
const notesOnlyTypes = ["relationship-sub", "source"]
|
||||
let finalApplicability: "email" | "notes" | "both" = "both"
|
||||
if (emailOnlyTypes.includes(type)) finalApplicability = "email"
|
||||
else if (notesOnlyTypes.includes(type)) finalApplicability = "notes"
|
||||
else finalApplicability = isEmailSection ? "email" : applicability
|
||||
|
||||
const newTag: TagDef = {
|
||||
tag: "",
|
||||
type,
|
||||
applicability: finalApplicability === "email" && !isEmailSection ? "both" : finalApplicability === "notes" && isEmailSection ? "both" : finalApplicability,
|
||||
description: "",
|
||||
noteEffect: isEmailSection ? "create" : "none",
|
||||
}
|
||||
const lastIndex = tags.reduce((acc, t, i) => t.type === type ? i : acc, -1)
|
||||
if (lastIndex === -1) {
|
||||
setTags(prev => [...prev, newTag])
|
||||
} else {
|
||||
setTags(prev => [...prev.slice(0, lastIndex + 1), newTag, ...prev.slice(lastIndex + 1)])
|
||||
}
|
||||
}, [tags])
|
||||
}, [tags, activeSection])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
setSaving(true)
|
||||
|
|
@ -890,92 +1037,50 @@ function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) {
|
|||
)
|
||||
}
|
||||
|
||||
const currentGroups = activeSection === "notes" ? noteGroups : emailGroups
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center gap-1 mb-3 border-b">
|
||||
<button
|
||||
onClick={() => setActiveSection("notes")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 -mb-px transition-colors",
|
||||
activeSection === "notes"
|
||||
? "border-foreground text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<BookOpen className="size-3.5" />
|
||||
Note Tags
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSection("email")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 -mb-px transition-colors",
|
||||
activeSection === "email"
|
||||
? "border-foreground text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Mail className="size-3.5" />
|
||||
Email Labels
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-4 min-h-0">
|
||||
{groups.map(group => (
|
||||
<div key={group.type}>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<button
|
||||
onClick={() => toggleGroup(group.type)}
|
||||
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronRight className={cn("size-3.5 transition-transform", !collapsedGroups.has(group.type) && "rotate-90")} />
|
||||
{group.label}
|
||||
<span className="text-[10px] ml-0.5">({group.tags.length})</span>
|
||||
</button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={() => addTag(group.type)}
|
||||
>
|
||||
<Plus className="size-3 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{!collapsedGroups.has(group.type) && group.tags.length > 0 && (
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<div className="grid grid-cols-[100px_1fr_1fr_80px_24px] gap-1 bg-muted/50 px-2 py-1 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
<div>Tag</div>
|
||||
<div>Description</div>
|
||||
<div>Example</div>
|
||||
<div>Applies to</div>
|
||||
<div />
|
||||
</div>
|
||||
{group.tags.map((tag, localIdx) => {
|
||||
const globalIdx = getGlobalIndex(group.type, localIdx)
|
||||
return (
|
||||
<div key={globalIdx} className="grid grid-cols-[100px_1fr_1fr_80px_24px] gap-1 border-t px-2 py-0.5 items-center">
|
||||
<Input
|
||||
value={tag.tag}
|
||||
onChange={e => updateTag(globalIdx, "tag", e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder="tag-name"
|
||||
title={tag.tag}
|
||||
/>
|
||||
<Input
|
||||
value={tag.description}
|
||||
onChange={e => updateTag(globalIdx, "description", e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder="Description"
|
||||
title={tag.description}
|
||||
/>
|
||||
<Input
|
||||
value={tag.example || ""}
|
||||
onChange={e => updateTag(globalIdx, "example", e.target.value)}
|
||||
className="h-7 text-xs"
|
||||
placeholder="Example"
|
||||
title={tag.example || ""}
|
||||
/>
|
||||
<Select
|
||||
value={tag.applicability}
|
||||
onValueChange={v => updateTag(globalIdx, "applicability", v)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="both">Both</SelectItem>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="notes">Notes</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<button
|
||||
onClick={() => removeTag(globalIdx)}
|
||||
className="flex items-center justify-center text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!collapsedGroups.has(group.type) && group.tags.length === 0 && (
|
||||
<div className="text-xs text-muted-foreground italic px-2">No tags in this group</div>
|
||||
)}
|
||||
</div>
|
||||
{currentGroups.map(group => (
|
||||
<TagGroupTable
|
||||
key={group.type}
|
||||
group={group}
|
||||
tags={tags}
|
||||
collapsed={collapsedGroups.has(group.type)}
|
||||
onToggle={() => toggleGroup(group.type)}
|
||||
onAdd={() => addTag(group.type)}
|
||||
onUpdate={updateTag}
|
||||
onRemove={removeTag}
|
||||
getGlobalIndex={getGlobalIndex}
|
||||
isEmail={activeSection === "email"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="pt-3 border-t mt-3 flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { renderNoteTypesBlock } from './note_system.js';
|
||||
import { renderNoteEffectRules } from './tag_system.js';
|
||||
|
||||
export function getRaw(): string {
|
||||
return `---
|
||||
|
|
@ -141,18 +142,7 @@ Either:
|
|||
|
||||
**For emails, read the YAML frontmatter labels and apply these rules:**
|
||||
|
||||
**CREATE/UPDATE notes if the email has ANY of these labels:**
|
||||
- **Relationship:** Investor, Customer, Prospect, Partner, Vendor, Candidate, Team, Advisor, Personal, Press, Community, Government
|
||||
- **Topic:** Sales, Support, Legal, Finance, Hiring, Fundraising, Event, Research
|
||||
- **Type:** Intro, Followup
|
||||
- **Action:** Action Required, Urgent, Waiting
|
||||
|
||||
**SKIP if the email ONLY has these labels (and none from above):**
|
||||
- **Relationship:** Product
|
||||
- **Topic:** Travel, Shopping, Health, Learning
|
||||
- **Type:** Scheduling, Cold Outreach, Newsletter, Notification
|
||||
- **Filter:** Spam, Promotion, Social, Forums
|
||||
- **Action:** FYI
|
||||
${renderNoteEffectRules()}
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -247,22 +237,7 @@ labeled_at: "2026-02-28T12:00:00Z"
|
|||
|
||||
## Decision Rules
|
||||
|
||||
Check the labels against the create/skip lists:
|
||||
|
||||
**CREATE/UPDATE notes if ANY label matches:**
|
||||
- relationship: Investor, Customer, Prospect, Partner, Vendor, Candidate, Team, Advisor, Personal, Press, Community, Government
|
||||
- topics: Sales, Support, Legal, Finance, Hiring, Fundraising, Event, Research
|
||||
- type: Intro, Followup
|
||||
- action: Action Required, Urgent, Waiting
|
||||
|
||||
**SKIP if labels ONLY match:**
|
||||
- relationship: Product
|
||||
- topics: Travel, Shopping, Health, Learning
|
||||
- type: Scheduling, Cold Outreach, Newsletter, Notification
|
||||
- filter: Spam, Promotion, Social, Forums
|
||||
- action: FYI
|
||||
|
||||
**Logic:** If even one label falls in the "create" list, process the email. Only skip if ALL labels fall in the "skip" list.
|
||||
${renderNoteEffectRules()}
|
||||
|
||||
## Filter Decision Output
|
||||
|
||||
|
|
|
|||
|
|
@ -14,93 +14,97 @@ export type TagType =
|
|||
| 'status'
|
||||
| 'source';
|
||||
|
||||
export type NoteEffect = 'create' | 'skip' | 'none';
|
||||
|
||||
export interface TagDefinition {
|
||||
tag: string;
|
||||
type: TagType;
|
||||
applicability: TagApplicability;
|
||||
description: string;
|
||||
example?: string;
|
||||
/** Whether an email with this tag should create notes ('create'), be skipped ('skip'), or has no effect on note creation ('none'). */
|
||||
noteEffect?: NoteEffect;
|
||||
}
|
||||
|
||||
// ── Default definitions (used to seed ~/.rowboat/config/tags.json) ──────────
|
||||
|
||||
const DEFAULT_TAG_DEFINITIONS: TagDefinition[] = [
|
||||
// ── Relationship (both) ──────────────────────────────────────────────
|
||||
{ tag: 'investor', type: 'relationship', applicability: 'both', description: 'Investors, VCs, or angels', example: 'Following up on our meeting — we\'d like to move forward with the Series A term sheet.' },
|
||||
{ tag: 'customer', type: 'relationship', applicability: 'both', description: 'Paying customers', example: 'We\'re seeing great results with Rowboat. Can we discuss expanding to more teams?' },
|
||||
{ tag: 'prospect', type: 'relationship', applicability: 'both', description: 'Potential customers', example: 'Thanks for the demo yesterday. We\'re interested in starting a pilot.' },
|
||||
{ tag: 'partner', type: 'relationship', applicability: 'both', description: 'Business partners', example: 'Let\'s discuss how we can promote the integration to both our user bases.' },
|
||||
{ tag: 'vendor', type: 'relationship', applicability: 'both', description: 'Service providers you work with', example: 'Here are the updated employment agreements you requested.' },
|
||||
{ tag: 'product', type: 'relationship', applicability: 'both', description: 'Products or services you use (automated)', example: 'Your AWS bill for January 2025 is now available.' },
|
||||
{ tag: 'candidate', type: 'relationship', applicability: 'both', description: 'Job applicants', example: 'Thanks for reaching out. I\'d love to learn more about the engineering role.' },
|
||||
{ tag: 'team', type: 'relationship', applicability: 'both', description: 'Internal team members', example: 'Here\'s the updated roadmap for Q2. Let\'s discuss in our sync.' },
|
||||
{ tag: 'advisor', type: 'relationship', applicability: 'both', description: 'Advisors, mentors, or board members', example: 'I\'ve reviewed the deck. Here are my thoughts on the GTM strategy.' },
|
||||
{ tag: 'personal', type: 'relationship', applicability: 'both', description: 'Family or friends', example: 'Are you coming to Thanksgiving this year? Let me know your travel dates.' },
|
||||
{ tag: 'press', type: 'relationship', applicability: 'both', description: 'Journalists or media', example: 'I\'m writing a piece on AI agents. Would you be available for an interview?' },
|
||||
{ tag: 'community', type: 'relationship', applicability: 'both', description: 'Users, peers, or open source contributors', example: 'Love what you\'re building with Rowboat. Here\'s a bug I found...' },
|
||||
{ tag: 'government', type: 'relationship', applicability: 'both', description: 'Government agencies', example: 'Your Delaware franchise tax is due by March 1, 2025.' },
|
||||
{ tag: 'investor', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Investors, VCs, or angels', example: 'Following up on our meeting — we\'d like to move forward with the Series A term sheet.' },
|
||||
{ tag: 'customer', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Paying customers', example: 'We\'re seeing great results with Rowboat. Can we discuss expanding to more teams?' },
|
||||
{ tag: 'prospect', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Potential customers', example: 'Thanks for the demo yesterday. We\'re interested in starting a pilot.' },
|
||||
{ tag: 'partner', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Business partners', example: 'Let\'s discuss how we can promote the integration to both our user bases.' },
|
||||
{ tag: 'vendor', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Service providers you work with', example: 'Here are the updated employment agreements you requested.' },
|
||||
{ tag: 'product', type: 'relationship', applicability: 'both', noteEffect: 'skip', description: 'Products or services you use (automated)', example: 'Your AWS bill for January 2025 is now available.' },
|
||||
{ tag: 'candidate', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Job applicants', example: 'Thanks for reaching out. I\'d love to learn more about the engineering role.' },
|
||||
{ tag: 'team', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Internal team members', example: 'Here\'s the updated roadmap for Q2. Let\'s discuss in our sync.' },
|
||||
{ tag: 'advisor', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Advisors, mentors, or board members', example: 'I\'ve reviewed the deck. Here are my thoughts on the GTM strategy.' },
|
||||
{ tag: 'personal', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Family or friends', example: 'Are you coming to Thanksgiving this year? Let me know your travel dates.' },
|
||||
{ tag: 'press', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Journalists or media', example: 'I\'m writing a piece on AI agents. Would you be available for an interview?' },
|
||||
{ tag: 'community', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Users, peers, or open source contributors', example: 'Love what you\'re building with Rowboat. Here\'s a bug I found...' },
|
||||
{ tag: 'government', type: 'relationship', applicability: 'both', noteEffect: 'create', description: 'Government agencies', example: 'Your Delaware franchise tax is due by March 1, 2025.' },
|
||||
|
||||
// ── Relationship Sub-Tags (notes only) ───────────────────────────────
|
||||
{ tag: 'primary', type: 'relationship-sub', applicability: 'notes', description: 'Main contact or decision maker', example: 'Sarah Chen — VP Engineering, your main point of contact at Acme.' },
|
||||
{ tag: 'secondary', type: 'relationship-sub', applicability: 'notes', description: 'Supporting contact, involved but not the lead', example: 'David Kim — Engineer CC\'d on customer emails.' },
|
||||
{ tag: 'executive-assistant', type: 'relationship-sub', applicability: 'notes', description: 'EA or admin handling scheduling and logistics', example: 'Lisa — Sarah\'s EA who schedules all her meetings.' },
|
||||
{ tag: 'cc', type: 'relationship-sub', applicability: 'notes', description: 'Person who\'s CC\'d but not actively engaged', example: 'Manager looped in for visibility on deal.' },
|
||||
{ tag: 'referred-by', type: 'relationship-sub', applicability: 'notes', description: 'Person who made an introduction or referral', example: 'David Park — Investor who intro\'d you to Sarah.' },
|
||||
{ tag: 'former', type: 'relationship-sub', applicability: 'notes', description: 'Previously held this relationship, no longer active', example: 'John — Former customer who churned last year.' },
|
||||
{ tag: 'champion', type: 'relationship-sub', applicability: 'notes', description: 'Internal advocate pushing for you', example: 'Engineer who loves your product and is selling internally.' },
|
||||
{ tag: 'blocker', type: 'relationship-sub', applicability: 'notes', description: 'Person opposing or blocking progress', example: 'CFO resistant to spending on new tools.' },
|
||||
{ tag: 'primary', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Main contact or decision maker', example: 'Sarah Chen — VP Engineering, your main point of contact at Acme.' },
|
||||
{ tag: 'secondary', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Supporting contact, involved but not the lead', example: 'David Kim — Engineer CC\'d on customer emails.' },
|
||||
{ tag: 'executive-assistant', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'EA or admin handling scheduling and logistics', example: 'Lisa — Sarah\'s EA who schedules all her meetings.' },
|
||||
{ tag: 'cc', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Person who\'s CC\'d but not actively engaged', example: 'Manager looped in for visibility on deal.' },
|
||||
{ tag: 'referred-by', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Person who made an introduction or referral', example: 'David Park — Investor who intro\'d you to Sarah.' },
|
||||
{ tag: 'former', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Previously held this relationship, no longer active', example: 'John — Former customer who churned last year.' },
|
||||
{ tag: 'champion', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Internal advocate pushing for you', example: 'Engineer who loves your product and is selling internally.' },
|
||||
{ tag: 'blocker', type: 'relationship-sub', applicability: 'notes', noteEffect: 'none', description: 'Person opposing or blocking progress', example: 'CFO resistant to spending on new tools.' },
|
||||
|
||||
// ── Topic (both) ─────────────────────────────────────────────────────
|
||||
{ tag: 'sales', type: 'topic', applicability: 'both', description: 'Sales conversations, deals, and revenue', example: 'Here\'s the pricing proposal we discussed. Let me know if you have questions.' },
|
||||
{ tag: 'support', type: 'topic', applicability: 'both', description: 'Help requests, issues, and customer support', example: 'We\'re seeing an error when trying to export. Can you help?' },
|
||||
{ tag: 'legal', type: 'topic', applicability: 'both', description: 'Contracts, terms, compliance, and legal matters', example: 'Legal has reviewed the MSA. Attached are our requested changes.' },
|
||||
{ tag: 'finance', type: 'topic', applicability: 'both', description: 'Money, invoices, payments, banking, and taxes', example: 'Your invoice #1234 for $5,000 is attached. Payment due in 30 days.' },
|
||||
{ tag: 'hiring', type: 'topic', applicability: 'both', description: 'Recruiting, interviews, and employment', example: 'We\'d like to move forward with a final round interview. Are you available Thursday?' },
|
||||
{ tag: 'fundraising', type: 'topic', applicability: 'both', description: 'Raising money and investor relations', example: 'Thanks for sending the deck. We\'d like to schedule a partner meeting.' },
|
||||
{ tag: 'travel', type: 'topic', applicability: 'both', description: 'Flights, hotels, trips, and travel logistics', example: 'Your flight to Tokyo on March 15 is confirmed. Confirmation #ABC123.' },
|
||||
{ tag: 'event', type: 'topic', applicability: 'both', description: 'Conferences, meetups, and gatherings', example: 'You\'re invited to speak at TechCrunch Disrupt. Can you confirm your availability?' },
|
||||
{ tag: 'shopping', type: 'topic', applicability: 'both', description: 'Purchases, orders, and returns', example: 'Your order #12345 has shipped. Track it here.' },
|
||||
{ tag: 'health', type: 'topic', applicability: 'both', description: 'Medical, wellness, and health-related matters', example: 'Your appointment with Dr. Smith is confirmed for Monday at 2pm.' },
|
||||
{ tag: 'learning', type: 'topic', applicability: 'both', description: 'Courses, education, and skill-building', example: 'Welcome to the Advanced Python course. Here\'s your access link.' },
|
||||
{ tag: 'research', type: 'topic', applicability: 'both', description: 'Research requests and information gathering', example: 'Here\'s the market analysis you requested on the AI agent space.' },
|
||||
{ tag: 'sales', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Sales conversations, deals, and revenue', example: 'Here\'s the pricing proposal we discussed. Let me know if you have questions.' },
|
||||
{ tag: 'support', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Help requests, issues, and customer support', example: 'We\'re seeing an error when trying to export. Can you help?' },
|
||||
{ tag: 'legal', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Contracts, terms, compliance, and legal matters', example: 'Legal has reviewed the MSA. Attached are our requested changes.' },
|
||||
{ tag: 'finance', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Money, invoices, payments, banking, and taxes', example: 'Your invoice #1234 for $5,000 is attached. Payment due in 30 days.' },
|
||||
{ tag: 'hiring', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Recruiting, interviews, and employment', example: 'We\'d like to move forward with a final round interview. Are you available Thursday?' },
|
||||
{ tag: 'fundraising', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Raising money and investor relations', example: 'Thanks for sending the deck. We\'d like to schedule a partner meeting.' },
|
||||
{ tag: 'travel', type: 'topic', applicability: 'both', noteEffect: 'skip', description: 'Flights, hotels, trips, and travel logistics', example: 'Your flight to Tokyo on March 15 is confirmed. Confirmation #ABC123.' },
|
||||
{ tag: 'event', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Conferences, meetups, and gatherings', example: 'You\'re invited to speak at TechCrunch Disrupt. Can you confirm your availability?' },
|
||||
{ tag: 'shopping', type: 'topic', applicability: 'both', noteEffect: 'skip', description: 'Purchases, orders, and returns', example: 'Your order #12345 has shipped. Track it here.' },
|
||||
{ tag: 'health', type: 'topic', applicability: 'both', noteEffect: 'skip', description: 'Medical, wellness, and health-related matters', example: 'Your appointment with Dr. Smith is confirmed for Monday at 2pm.' },
|
||||
{ tag: 'learning', type: 'topic', applicability: 'both', noteEffect: 'skip', description: 'Courses, education, and skill-building', example: 'Welcome to the Advanced Python course. Here\'s your access link.' },
|
||||
{ tag: 'research', type: 'topic', applicability: 'both', noteEffect: 'create', description: 'Research requests and information gathering', example: 'Here\'s the market analysis you requested on the AI agent space.' },
|
||||
|
||||
// ── Email Type ───────────────────────────────────────────────────────
|
||||
{ tag: 'intro', type: 'email-type', applicability: 'both', description: 'Warm introduction from someone you know', example: 'I\'d like to introduce you to Sarah Chen, VP Engineering at Acme.' },
|
||||
{ tag: 'followup', type: 'email-type', applicability: 'both', description: 'Following up on a previous conversation', example: 'Following up on our call last week. Have you had a chance to review the proposal?' },
|
||||
{ tag: 'scheduling', type: 'email-type', applicability: 'email', description: 'Meeting and calendar scheduling', example: 'Are you available for a call next Tuesday at 2pm?' },
|
||||
{ tag: 'cold-outreach', type: 'email-type', applicability: 'email', description: 'Unsolicited contact from someone you don\'t know', example: 'Hi, I noticed your company is growing fast. I\'d love to show you how we can help with...' },
|
||||
{ tag: 'newsletter', type: 'email-type', applicability: 'email', description: 'Newsletters, marketing emails, and subscriptions', example: 'This week in AI: The latest developments in agent frameworks...' },
|
||||
{ tag: 'notification', type: 'email-type', applicability: 'email', description: 'Automated alerts, receipts, and system notifications', example: 'Your password was changed successfully. If this wasn\'t you, contact support.' },
|
||||
{ tag: 'intro', type: 'email-type', applicability: 'both', noteEffect: 'create', description: 'Warm introduction from someone you know', example: 'I\'d like to introduce you to Sarah Chen, VP Engineering at Acme.' },
|
||||
{ tag: 'followup', type: 'email-type', applicability: 'both', noteEffect: 'create', description: 'Following up on a previous conversation', example: 'Following up on our call last week. Have you had a chance to review the proposal?' },
|
||||
{ tag: 'scheduling', type: 'email-type', applicability: 'email', noteEffect: 'skip', description: 'Meeting and calendar scheduling', example: 'Are you available for a call next Tuesday at 2pm?' },
|
||||
{ tag: 'cold-outreach', type: 'email-type', applicability: 'email', noteEffect: 'skip', description: 'Unsolicited contact from someone you don\'t know', example: 'Hi, I noticed your company is growing fast. I\'d love to show you how we can help with...' },
|
||||
{ tag: 'newsletter', type: 'email-type', applicability: 'email', noteEffect: 'skip', description: 'Newsletters, marketing emails, and subscriptions', example: 'This week in AI: The latest developments in agent frameworks...' },
|
||||
{ tag: 'notification', type: 'email-type', applicability: 'email', noteEffect: 'skip', description: 'Automated alerts, receipts, and system notifications', example: 'Your password was changed successfully. If this wasn\'t you, contact support.' },
|
||||
|
||||
// ── Filter (email only) ──────────────────────────────────────────────
|
||||
{ tag: 'spam', type: 'filter', applicability: 'email', description: 'Junk and unwanted email', example: 'Congratulations! You\'ve won $1,000,000...' },
|
||||
{ tag: 'promotion', type: 'filter', applicability: 'email', description: 'Marketing offers and sales pitches', example: '50% off all items this weekend only!' },
|
||||
{ tag: 'social', type: 'filter', applicability: 'email', description: 'Social media notifications', example: 'John Smith commented on your post.' },
|
||||
{ tag: 'forums', type: 'filter', applicability: 'email', description: 'Mailing lists and group discussions', example: 'Re: [dev-list] Question about API design' },
|
||||
{ tag: 'spam', type: 'filter', applicability: 'email', noteEffect: 'skip', description: 'Junk and unwanted email', example: 'Congratulations! You\'ve won $1,000,000...' },
|
||||
{ tag: 'promotion', type: 'filter', applicability: 'email', noteEffect: 'skip', description: 'Marketing offers and sales pitches', example: '50% off all items this weekend only!' },
|
||||
{ tag: 'social', type: 'filter', applicability: 'email', noteEffect: 'skip', description: 'Social media notifications', example: 'John Smith commented on your post.' },
|
||||
{ tag: 'forums', type: 'filter', applicability: 'email', noteEffect: 'skip', description: 'Mailing lists and group discussions', example: 'Re: [dev-list] Question about API design' },
|
||||
|
||||
// ── Action ───────────────────────────────────────────────────────────
|
||||
{ tag: 'action-required', type: 'action', applicability: 'both', description: 'Needs a response or action from you', example: 'Can you send me the pricing by Friday?' },
|
||||
{ tag: 'fyi', type: 'action', applicability: 'email', description: 'Informational only, no action needed', example: 'Just wanted to let you know the deal closed. Thanks for your help!' },
|
||||
{ tag: 'urgent', type: 'action', applicability: 'both', description: 'Time-sensitive, needs immediate attention', example: 'We need your signature on the contract by EOD today or we lose the deal.' },
|
||||
{ tag: 'waiting', type: 'action', applicability: 'both', description: 'Waiting on a response from them' },
|
||||
{ tag: 'action-required', type: 'action', applicability: 'both', noteEffect: 'create', description: 'Needs a response or action from you', example: 'Can you send me the pricing by Friday?' },
|
||||
{ tag: 'fyi', type: 'action', applicability: 'email', noteEffect: 'skip', description: 'Informational only, no action needed', example: 'Just wanted to let you know the deal closed. Thanks for your help!' },
|
||||
{ tag: 'urgent', type: 'action', applicability: 'both', noteEffect: 'create', description: 'Time-sensitive, needs immediate attention', example: 'We need your signature on the contract by EOD today or we lose the deal.' },
|
||||
{ tag: 'waiting', type: 'action', applicability: 'both', noteEffect: 'create', description: 'Waiting on a response from them' },
|
||||
|
||||
// ── Status (email) ───────────────────────────────────────────────────
|
||||
{ tag: 'unread', type: 'status', applicability: 'email', description: 'Not yet processed' },
|
||||
{ tag: 'to-reply', type: 'status', applicability: 'email', description: 'Need to respond' },
|
||||
{ tag: 'done', type: 'status', applicability: 'email', description: 'Handled, can be archived' },
|
||||
{ tag: 'unread', type: 'status', applicability: 'email', noteEffect: 'none', description: 'Not yet processed' },
|
||||
{ tag: 'to-reply', type: 'status', applicability: 'email', noteEffect: 'none', description: 'Need to respond' },
|
||||
{ tag: 'done', type: 'status', applicability: 'email', noteEffect: 'none', description: 'Handled, can be archived' },
|
||||
|
||||
// ── Source (notes only) ──────────────────────────────────────────────
|
||||
{ tag: 'email', type: 'source', applicability: 'notes', description: 'Created or updated from email' },
|
||||
{ tag: 'meeting', type: 'source', applicability: 'notes', description: 'Created or updated from meeting transcript' },
|
||||
{ tag: 'browser', type: 'source', applicability: 'notes', description: 'Content captured from web browsing' },
|
||||
{ tag: 'web-search', type: 'source', applicability: 'notes', description: 'Information from web search' },
|
||||
{ tag: 'manual', type: 'source', applicability: 'notes', description: 'Manually entered by user' },
|
||||
{ tag: 'import', type: 'source', applicability: 'notes', description: 'Imported from another system' },
|
||||
{ tag: 'email', type: 'source', applicability: 'notes', noteEffect: 'none', description: 'Created or updated from email' },
|
||||
{ tag: 'meeting', type: 'source', applicability: 'notes', noteEffect: 'none', description: 'Created or updated from meeting transcript' },
|
||||
{ tag: 'browser', type: 'source', applicability: 'notes', noteEffect: 'none', description: 'Content captured from web browsing' },
|
||||
{ tag: 'web-search', type: 'source', applicability: 'notes', noteEffect: 'none', description: 'Information from web search' },
|
||||
{ tag: 'manual', type: 'source', applicability: 'notes', noteEffect: 'none', description: 'Manually entered by user' },
|
||||
{ tag: 'import', type: 'source', applicability: 'notes', noteEffect: 'none', description: 'Imported from another system' },
|
||||
|
||||
// ── Status (notes) ──────────────────────────────────────────────────
|
||||
{ tag: 'active', type: 'status', applicability: 'notes', description: 'Currently relevant, recent activity' },
|
||||
{ tag: 'archived', type: 'status', applicability: 'notes', description: 'No longer active, kept for reference' },
|
||||
{ tag: 'stale', type: 'status', applicability: 'notes', description: 'No activity in 60+ days, needs attention or archive' },
|
||||
{ tag: 'active', type: 'status', applicability: 'notes', noteEffect: 'none', description: 'Currently relevant, recent activity' },
|
||||
{ tag: 'archived', type: 'status', applicability: 'notes', noteEffect: 'none', description: 'No longer active, kept for reference' },
|
||||
{ tag: 'stale', type: 'status', applicability: 'notes', noteEffect: 'none', description: 'No activity in 60+ days, needs attention or archive' },
|
||||
];
|
||||
|
||||
// ── Disk-backed config with mtime caching ──────────────────────────────────
|
||||
|
|
@ -186,6 +190,35 @@ function renderTagGroups(tags: TagDefinition[]): string {
|
|||
return `# Tag System Reference\n\n${sections.join('\n\n')}`;
|
||||
}
|
||||
|
||||
export function renderNoteEffectRules(): string {
|
||||
const tags = getTagDefinitions();
|
||||
const skipByType = new Map<string, string[]>();
|
||||
const createByType = new Map<string, string[]>();
|
||||
|
||||
for (const t of tags) {
|
||||
const effect = t.noteEffect ?? 'none';
|
||||
if (effect === 'none') continue;
|
||||
const label = TYPE_LABELS[t.type] ?? t.type;
|
||||
const map = effect === 'skip' ? skipByType : createByType;
|
||||
const list = map.get(label) ?? [];
|
||||
list.push(t.tag.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join(' '));
|
||||
map.set(label, list);
|
||||
}
|
||||
|
||||
const formatList = (map: Map<string, string[]>) =>
|
||||
Array.from(map.entries()).map(([type, tags]) => `- **${type}:** ${tags.join(', ')}`).join('\n');
|
||||
|
||||
return [
|
||||
`**SKIP if the email has ANY of these labels (skip labels override everything):**`,
|
||||
formatList(skipByType),
|
||||
``,
|
||||
`**CREATE/UPDATE notes if the email has ANY of these labels (and no skip labels present):**`,
|
||||
formatList(createByType),
|
||||
``,
|
||||
`**Logic:** If even one label falls in the "skip" list, skip the email — skip labels are hard filters that override create labels.`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function renderTagSystemForNotes(): string {
|
||||
const tags = getTagDefinitions().filter(t => t.applicability !== 'email');
|
||||
return renderTagGroups(tags);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue