diff --git a/lang/es.json b/lang/es.json index 405d4e69..7e3f2c96 100644 --- a/lang/es.json +++ b/lang/es.json @@ -2,6 +2,7 @@ "Categorized by AI": "Categorizado por IA", "Categorized by AI with :confidence% confident": "Categorizado por IA con un :confidence % de confianza", "Only show AI guesses": "Mostrar solo sugerencias de IA", + "Created by AI": "Creado por IA", "Analyze": "Analizar", "Monthly average": "Media mensual", "Trend": "Tendencia", diff --git a/resources/js/components/automation-rules/automation-rule-title.test.tsx b/resources/js/components/automation-rules/automation-rule-title.test.tsx new file mode 100644 index 00000000..ecd2fdf5 --- /dev/null +++ b/resources/js/components/automation-rules/automation-rule-title.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { type AutomationRule, type RuleOrigin } from '@/types/automation-rule'; +import { AutomationRuleTitle } from './automation-rule-title'; + +function makeRule(origin: RuleOrigin): AutomationRule { + return { + id: 'rule-1', + user_id: 'user-1', + title: 'Supermarket', + priority: 1, + origin, + rules_json: {}, + action_category_id: null, + action_note: null, + action_note_iv: null, + labels: [], + created_at: '2026-06-15T00:00:00Z', + updated_at: '2026-06-15T00:00:00Z', + deleted_at: null, + }; +} + +describe('AutomationRuleTitle', () => { + it('prefixes the title with the AI sparkle when the rule was created by AI', () => { + render(); + + expect(screen.getByText('Supermarket')).toBeInTheDocument(); + expect(screen.getByLabelText('Created by AI')).toBeInTheDocument(); + }); + + it('shows no marker for user-created rules', () => { + render(); + + expect(screen.getByText('Supermarket')).toBeInTheDocument(); + expect( + screen.queryByLabelText('Created by AI'), + ).not.toBeInTheDocument(); + }); +}); diff --git a/resources/js/components/automation-rules/automation-rule-title.tsx b/resources/js/components/automation-rules/automation-rule-title.tsx new file mode 100644 index 00000000..9cf72261 --- /dev/null +++ b/resources/js/components/automation-rules/automation-rule-title.tsx @@ -0,0 +1,39 @@ +import { AiSparkleIcon } from '@/components/ui/ai-sparkle-icon'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { type AutomationRule } from '@/types/automation-rule'; +import { __ } from '@/utils/i18n'; + +/** + * A rule's title, prefixed with the shared AI sparkle when the rule itself was + * generated by AI (origin === 'ai'). + */ +export function AutomationRuleTitle({ rule }: { rule: AutomationRule }) { + const isAiGenerated = rule.origin === 'ai'; + const label = __('Created by AI'); + + return ( +
+ {isAiGenerated && ( + + + + + + + + {label} + + + )} + {rule.title} +
+ ); +} diff --git a/resources/js/components/transactions/category-cell.tsx b/resources/js/components/transactions/category-cell.tsx index bb887a28..a285c6cf 100644 --- a/resources/js/components/transactions/category-cell.tsx +++ b/resources/js/components/transactions/category-cell.tsx @@ -1,5 +1,5 @@ -import { AiSparkleIcon } from '@/components/transactions/ai-sparkle-icon'; import { CategorySelect } from '@/components/transactions/category-select'; +import { AiSparkleIcon } from '@/components/ui/ai-sparkle-icon'; import { Tooltip, TooltipContent, diff --git a/resources/js/components/transactions/transaction-filters.tsx b/resources/js/components/transactions/transaction-filters.tsx index 060fae14..093631d5 100644 --- a/resources/js/components/transactions/transaction-filters.tsx +++ b/resources/js/components/transactions/transaction-filters.tsx @@ -6,8 +6,8 @@ import { ChevronsUpDown, Tag, X } from 'lucide-react'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; import { AccountName } from '@/components/accounts/account-name'; -import { AiSparkleIcon } from '@/components/transactions/ai-sparkle-icon'; import { SavedFilters } from '@/components/transactions/saved-filters'; +import { AiSparkleIcon } from '@/components/ui/ai-sparkle-icon'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; diff --git a/resources/js/components/transactions/ai-sparkle-icon.tsx b/resources/js/components/ui/ai-sparkle-icon.tsx similarity index 84% rename from resources/js/components/transactions/ai-sparkle-icon.tsx rename to resources/js/components/ui/ai-sparkle-icon.tsx index 733a66d3..cb7047c2 100644 --- a/resources/js/components/transactions/ai-sparkle-icon.tsx +++ b/resources/js/components/ui/ai-sparkle-icon.tsx @@ -2,7 +2,10 @@ import { cn } from '@/lib/utils'; import { Sparkles } from 'lucide-react'; /** - * The Gemini-style multi-color sparkle used to mark an AI-guessed category. + * The Gemini-style multi-color sparkle that marks anything powered by AI + * (AI-guessed categories, AI-generated rules, AI filters, ...). This is the + * single shared icon to reuse wherever a feature involves AI. + * * The gradient is applied to the SVG via a referenced linearGradient so the * stroke and fill render as a true multi-color gradient (not a flat color). */ diff --git a/resources/js/pages/settings/automation-rules.tsx b/resources/js/pages/settings/automation-rules.tsx index dd436b4c..7bc37f68 100644 --- a/resources/js/pages/settings/automation-rules.tsx +++ b/resources/js/pages/settings/automation-rules.tsx @@ -18,6 +18,7 @@ import { useMemo, useState } from 'react'; import { index as automationRulesIndex } from '@/actions/App/Http/Controllers/Settings/AutomationRuleController'; import { ApplyAutomationRuleDialog } from '@/components/automation-rules/apply-automation-rule-dialog'; import { AutomationRuleActionBadges } from '@/components/automation-rules/automation-rule-action-badges'; +import { AutomationRuleTitle } from '@/components/automation-rules/automation-rule-title'; import { CreateAutomationRuleDialog } from '@/components/automation-rules/create-automation-rule-dialog'; import { DeleteAutomationRuleDialog } from '@/components/automation-rules/delete-automation-rule-dialog'; import { EditAutomationRuleDialog } from '@/components/automation-rules/edit-automation-rule-dialog'; @@ -230,9 +231,7 @@ export default function AutomationRules() { accessorKey: 'title', header: __('Title'), cell: ({ row }) => { - return ( -
{row.getValue('title')}
- ); + return ; }, }, { diff --git a/resources/js/types/automation-rule.ts b/resources/js/types/automation-rule.ts index 52e79ad6..fe80f41f 100644 --- a/resources/js/types/automation-rule.ts +++ b/resources/js/types/automation-rule.ts @@ -2,11 +2,14 @@ import type { Category } from './category'; import type { Label } from './label'; import { UUID } from './uuid'; +export type RuleOrigin = 'user' | 'ai'; + export interface AutomationRule { id: UUID; user_id: UUID; title: string; priority: number; + origin: RuleOrigin; rules_json: Record; action_category_id: UUID | null; action_note: string | null;