import { BarChart, Shield } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { useParams } from 'common' import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' import { LintCategoryBadge, lintInfoMap } from 'components/interfaces/Linter/Linter.utils' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Lint, useProjectLintsQuery } from 'data/lint/lint-query' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { AiIconAnimation, Button, Card, CardContent, CardHeader, CardTitle, Sheet, SheetContent, SheetHeader, SheetSection, SheetTitle, } from 'ui' import { Row } from 'ui-patterns' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import LintDetail from 'components/interfaces/Linter/LintDetail' // Returns the first sentence of the provided text. Uses Intl.Segmenter when available // and falls back to a regex that avoids splitting on intra-word dots (e.g. `public.messages`). const getFirstSentence = (text: string | undefined | null): string | undefined | null => { if (!text) return text try { if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) { // @ts-ignore - TS may not have Intl.Segmenter in lib by default const segmenter = new Intl.Segmenter(undefined, { granularity: 'sentence' }) // @ts-ignore const iterator = segmenter.segment(text)[Symbol.iterator]() const first = iterator.next() if (!first.done && first.value && first.value.segment) { return String(first.value.segment).trim() } } } catch (_) { // no-op; fall back to regex below } // Fallback: find first sentence terminator that is followed by whitespace or end of string. // This avoids splitting on things like `public.messages` where '.' is not followed by whitespace. const match = text.match(/[.!?](?=\s|$)/) if (match && typeof match.index === 'number') { return text.slice(0, match.index + 1).trim() } return text } export const AdvisorWidget = () => { const { ref: projectRef } = useParams() const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery({ projectRef }) const snap = useAiAssistantStateSnapshot() const [selectedLint, setSelectedLint] = useState(null) const securityLints = useMemo( () => (lints ?? []).filter((lint: Lint) => lint.categories.includes('SECURITY')), [lints] ) const performanceLints = useMemo( () => (lints ?? []).filter((lint: Lint) => lint.categories.includes('PERFORMANCE')), [lints] ) const securityErrorCount = useMemo( () => securityLints.filter((lint: Lint) => lint.level === LINTER_LEVELS.ERROR).length, [securityLints] ) const performanceErrorCount = useMemo( () => performanceLints.filter((lint: Lint) => lint.level === LINTER_LEVELS.ERROR).length, [performanceLints] ) const combinedIssues: Lint[] = useMemo(() => { const combined = [...securityLints, ...performanceLints] return combined.filter((lint) => lint.level === LINTER_LEVELS.ERROR) }, [securityLints, performanceLints]) const totalErrors = securityErrorCount + performanceErrorCount const titleContent = useMemo(() => { if (totalErrors === 0) return

Assistant found no issues

const issuesText = totalErrors === 1 ? 'issue' : 'issues' const numberDisplay = totalErrors.toString() return (

Assistant found {numberDisplay} {issuesText}

) }, [totalErrors]) const createLintSummaryPrompt = useCallback((lint: Lint) => { const title = lintInfoMap.find((item) => item.name === lint.name)?.title ?? lint.title const entity = (lint.metadata && (lint.metadata.entity || (lint.metadata.schema && lint.metadata.name && `${lint.metadata.schema}.${lint.metadata.name}`))) || 'N/A' const schema = lint.metadata?.schema ?? 'N/A' const issue = lint.detail ? lint.detail.replace(/\`/g, '`') : 'N/A' const description = lint.description ? lint.description.replace(/\`/g, '`') : 'N/A' return `Summarize the issue and suggest fixes for the following lint item: Title: ${title} Entity: ${entity} Schema: ${schema} Issue Details: ${issue} Description: ${description}` }, []) const handleAskAssistant = useCallback(() => { snap.toggleAssistant() }, [snap]) const handleCardClick = useCallback((lint: Lint) => { setSelectedLint(lint) }, []) return (
{isLoadingLints ? ( ) : (
{titleContent}
)} {isLoadingLints ? (
) : combinedIssues.length > 0 ? ( <> {combinedIssues.map((lint) => { return ( { handleCardClick(lint) }} >
{lint.categories[0] === 'SECURITY' ? ( ) : ( )} {lint.categories[0]}
} onClick={(e) => { e.stopPropagation() e.preventDefault() snap.newChat({ name: 'Summarize lint', open: true, initialInput: createLintSummaryPrompt(lint), }) }} tooltip={{ content: { side: 'bottom', text: 'Help me fix this issue' }, }} />
{lint.detail ? getFirstSentence(lint.detail) : lint.title}
) })}
{selectedLint && ( setSelectedLint(null)}>
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title}
{selectedLint && projectRef && ( )}
)} ) : (

No security or performance errors found

)}
) }