feat(ai): share AI sparkle icon and mark AI-generated rules (#538)
## What - **Extract the AI sparkle into a shared component.** Moves the Gemini-style gradient sparkle from `components/transactions/ai-sparkle-icon` → `components/ui/ai-sparkle-icon`. It's now the single icon to reuse wherever a feature involves AI. Existing transaction-list usages (`category-cell`, `transaction-filters`) point at the new path; no visual change there. - **Mark AI-generated automation rules.** In the Automation Rules table, the rule **title** is now prefixed with the sparkle (tooltip + `aria-label "Created by AI"`) when the rule's `origin` is `ai`. Extracted into a small `AutomationRuleTitle` component. The backend `origin` enum (`RuleOrigin`: user/ai) already shipped to `main`; this PR is the frontend surface plus the `origin` field on the TS `AutomationRule` type. ## Tests - New `automation-rule-title.test.tsx`: sparkle shown for `origin: 'ai'`, absent for `'user'`. - `bun run format`, `bun run lint` clean; vitest green. ## Note Per review, the marker sits before the rule **name**, not next to the category badge.
This commit is contained in:
parent
dbec1c4c13
commit
7dde67c606
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(<AutomationRuleTitle rule={makeRule('ai')} />);
|
||||
|
||||
expect(screen.getByText('Supermarket')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Created by AI')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no marker for user-created rules', () => {
|
||||
render(<AutomationRuleTitle rule={makeRule('user')} />);
|
||||
|
||||
expect(screen.getByText('Supermarket')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('Created by AI'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<div className="flex items-center gap-1.5 font-medium">
|
||||
{isAiGenerated && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="inline-flex shrink-0"
|
||||
aria-label={label}
|
||||
>
|
||||
<AiSparkleIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{rule.title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
*/
|
||||
|
|
@ -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 (
|
||||
<div className="font-medium">{row.getValue('title')}</div>
|
||||
);
|
||||
return <AutomationRuleTitle rule={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
action_category_id: UUID | null;
|
||||
action_note: string | null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue