SurfSense/surfsense_web/components/assistant-ui/mention-chip.tsx
DESKTOP-RTLN3BA\$punk c8374e6c5b
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions
feat: improved document, folder mentions rendering
2026-05-09 22:15:51 -07:00

92 lines
2.9 KiB
TypeScript

"use client";
import type { MouseEventHandler, ReactNode } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
/**
* A single, minimal chip-button used in two places:
*
* 1. User-message mention chips (rendered for every `@`-mention the user
* inserted in the composer).
* 2. AI-answer file/folder paths (rendered when the assistant emits
* `/documents/.../file.xml` or `/<mount>/.../file.ext`).
*
* Both contexts want the same visual language: a compact, button-styled
* chip with an icon, a truncated label, and an optional tooltip. Sharing
* one component keeps the chat surface visually coherent and means a UX
* tweak (radius, hover, icon size) lands in both places at once.
*
* Styling rules (per shadcn skill):
* - Semantic tokens only (`border`, `bg-background`, `bg-accent`,
* `text-foreground`, `text-muted-foreground`). No raw colors.
* - Layout via `gap-*`, never `space-x-*`.
* - `cn()` for conditional classes.
* - No manual `z-index` — the tooltip handles its own stacking.
*/
export interface MentionChipProps {
/**
* Visual prefix. Keep this small (e.g. `size-3.5`); the chip controls
* its own height and oversized icons will push the label out of place.
*/
icon: ReactNode;
/** Label shown inside the chip; truncated with `…` past the max width. */
label: string;
/**
* Full title or path shown on hover. Omit to suppress the tooltip
* entirely (e.g. when the label already conveys the full identity).
*/
tooltip?: ReactNode;
/**
* When provided, the chip behaves like a button (focusable, hover
* effect, pointer cursor). Omit for a purely decorative chip.
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
className?: string;
/** Optional override for the accessible name; defaults to `label`. */
ariaLabel?: string;
}
export function MentionChip({
icon,
label,
tooltip,
onClick,
disabled,
className,
ariaLabel,
}: MentionChipProps) {
const isInteractive = Boolean(onClick) && !disabled;
const chip = (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-label={ariaLabel ?? label}
className={cn(
"inline-flex max-w-[220px] items-center gap-1.5 rounded-md border bg-background px-2 py-0.5 align-middle text-xs font-medium text-foreground leading-5 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isInteractive
? "cursor-pointer hover:bg-accent hover:text-accent-foreground"
: "cursor-default",
disabled && "opacity-60",
className
)}
>
<span className="inline-flex shrink-0 text-muted-foreground">{icon}</span>
<span className="truncate">{label}</span>
</button>
);
if (!tooltip) return chip;
return (
<Tooltip>
<TooltipTrigger asChild>{chip}</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs break-all">
{tooltip}
</TooltipContent>
</Tooltip>
);
}