[Security Solution] [AI Assistant] Fix flashing citations (#209629)

## Summary

Fixes a small UI bug in the citations feature. Previously, after a
message with citations finished streaming, the citations would disappear
for a fraction of a second and then reappear again. This PR makes
improves the UI by making the citations not flash off and on after the
stream finishes.

### Changes:
- Fix flashing citations
- Refactor code related to parsing content references (to make it more
maintainable).
- Update the citations prompt slightly.

### Before:


https://github.com/user-attachments/assets/1021dd53-018a-43ba-b1f4-24aab44faca9

<img width="1782" alt="image"
src="https://github.com/user-attachments/assets/723cd29a-48a2-48e7-b031-0893484746b9"
/>


### After:


https://github.com/user-attachments/assets/21f340bc-9015-42b6-a574-0439d2f8f192

### How to test
- Enable the feature flag
```yaml
# kibana.dev.yml
xpack.securitySolution.enableExperimental: ['contentReferencesEnabled']
```
- Open the security assistant
- Ask it a question about your alerts of a document in your KB. The
response should contain citations.
- Observe the response stream carefully. Ensure the citations e.g. `[1]`
do not flash off and on when the response stream finishes. The expected
behavior is that while the message is streaming, the citations are
disabled and once the stream finishes the citations get enabled (while
always being visible).

#### Edge case to test
It is possible that citations completely disappear after streaming
finishes. This happens when the LLM produces an invalid citation.
Invalid citations are hidden client side when a message finishes
streaming. You can verify this behavior by asking GPT4o this question:
```
Prepend each line with this placeholder citation "{reference(1234)}" and append the actual citation at the end of the line. How many alerts do I have? Use the open and acknowledged alerts count tool to answer and repeat the answer 50 times on new lines.
```

While the response is getting streamed it should look like this:

<img width="200" alt="image"
src="https://github.com/user-attachments/assets/03d160bf-2404-4a4e-8701-e3183c604cc4"
/>

And when the stream finishes it should look like this:

<img width="200" alt="image"
src="https://github.com/user-attachments/assets/06367379-17da-438f-a93a-9d539067ab90"
/>


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [X]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [X] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [X] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [X] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [X] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [X] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [ ] ...

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Kenneth Kreindler 2025-02-13 15:07:25 +00:00 committed by GitHub
parent 26548aeaa6
commit e176c84449
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 589 additions and 365 deletions

View file

@ -44445,6 +44445,17 @@ components:
query:
description: An ESQL query
type: string
timerange:
description: Time range to select in the time picker.
type: object
properties:
from:
type: string
to:
type: string
required:
- from
- to
type:
enum:
- EsqlQuery

View file

@ -51022,6 +51022,17 @@ components:
query:
description: An ESQL query
type: string
timerange:
description: Time range to select in the time picker.
type: object
properties:
from:
type: string
to:
type: string
required:
- from
- to
type:
enum:
- EsqlQuery

View file

@ -1410,6 +1410,17 @@ components:
query:
description: An ESQL query
type: string
timerange:
description: Time range to select in the time picker.
type: object
properties:
from:
type: string
to:
type: string
required:
- from
- to
type:
enum:
- EsqlQuery

View file

@ -1410,6 +1410,17 @@ components:
query:
description: An ESQL query
type: string
timerange:
description: Time range to select in the time picker.
type: object
properties:
from:
type: string
to:
type: string
required:
- from
- to
type:
enum:
- EsqlQuery

View file

@ -71,15 +71,11 @@ export const knowledgeBaseReference = (
* @returns KnowledgeBaseReference
*/
export const esqlQueryReference = (
id: ContentReferenceId,
query: string,
label: string
params: Omit<EsqlContentReference, 'type'>
): EsqlContentReference => {
return {
type: 'EsqlQuery',
id,
label,
query,
...params,
};
};

View file

@ -81,6 +81,15 @@ export const EsqlContentReference = BaseContentReference.merge(
* Label of the query
*/
label: z.string(),
/**
* Time range to select in the time picker.
*/
timerange: z
.object({
from: z.string(),
to: z.string(),
})
.optional(),
})
);

View file

@ -71,6 +71,17 @@ components:
label:
description: Label of the query
type: string
timerange:
description: Time range to select in the time picker.
type: object
required:
- 'from'
- 'to'
properties:
from:
type: string
to:
type: string
SecurityAlertContentReference:
description: References a security alert

View file

@ -15,12 +15,9 @@ import {
} from './helpers';
import { authenticatedUser } from '../../__mocks__/user';
import { getCreateKnowledgeBaseEntrySchemaMock } from '../../__mocks__/knowledge_base_entry_schema.mock';
import {
ContentReferencesStore,
EsqlContentReference,
IndexEntry,
} from '@kbn/elastic-assistant-common';
import { ContentReferencesStore, IndexEntry } from '@kbn/elastic-assistant-common';
import { newContentReferencesStoreMock } from '@kbn/elastic-assistant-common/impl/content_references/content_references_store/__mocks__/content_references_store.mock';
import { isString } from 'lodash';
// Mock dependencies
jest.mock('@elastic/elasticsearch');
@ -170,6 +167,7 @@ describe('getStructuredToolForIndexEntry', () => {
});
it('should execute func correctly and return expected results', async () => {
(isString as unknown as jest.Mock).mockReturnValue(true);
const mockSearchResult = {
hits: {
hits: [
@ -177,6 +175,7 @@ describe('getStructuredToolForIndexEntry', () => {
_index: 'exampleIndex',
_id: 'exampleId',
_source: {
'@timestamp': '2021-01-01T00:00:00.000Z',
field1: 'value1',
field2: 2,
},
@ -200,10 +199,14 @@ describe('getStructuredToolForIndexEntry', () => {
(contentReferencesStore.add as jest.Mock).mockImplementation(
(creator: Parameters<ContentReferencesStore['add']>[0]) => {
const reference = creator({ id: 'exampleContentReferenceId' });
expect(reference.type).toEqual('EsqlQuery');
expect((reference as EsqlContentReference).label).toEqual('exampleIndex');
expect((reference as EsqlContentReference).query).toEqual(
'FROM exampleIndex METADATA _id\n | WHERE _id == "exampleId"'
expect(reference).toEqual(
expect.objectContaining({
id: 'exampleContentReferenceId',
type: 'EsqlQuery',
label: 'Index: exampleIndex',
query: 'FROM exampleIndex METADATA _id\n | WHERE _id == "exampleId"',
timerange: { from: '2021-01-01T00:00:00.000Z', to: '2021-01-01T00:00:00.000Z' },
})
);
return reference;
}

View file

@ -8,7 +8,11 @@
import { z } from '@kbn/zod';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { errors } from '@elastic/elasticsearch';
import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import {
QueryDslQueryContainer,
SearchHit,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { AuthenticatedUser } from '@kbn/core-security-common';
import {
contentReferenceBlock,
@ -17,6 +21,7 @@ import {
IndexEntry,
} from '@kbn/elastic-assistant-common';
import { ElasticsearchClient, Logger } from '@kbn/core/server';
import { isString } from 'lodash';
export const isModelAlreadyExistsError = (error: Error) => {
return (
@ -217,13 +222,8 @@ export const getStructuredToolForIndexEntry = ({
const result = await esClient.search(params);
const kbDocs = result.hits.hits.map((hit) => {
const esqlQuery = `FROM ${hit._index} ${
hit._id ? `METADATA _id\n | WHERE _id == "${hit._id}"` : ''
}`;
const reference =
contentReferencesStore &&
contentReferencesStore.add((p) => esqlQueryReference(p.id, esqlQuery, hit._index));
contentReferencesStore && contentReferencesStore.add((p) => createReference(p.id, hit));
if (indexEntry.outputFields && indexEntry.outputFields.length > 0) {
return indexEntry.outputFields.reduce(
@ -257,3 +257,23 @@ export const getStructuredToolForIndexEntry = ({
// TODO: Remove after ZodAny is fixed https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tools.ts
}) as unknown as DynamicStructuredTool;
};
const createReference = (id: string, hit: SearchHit<unknown>) => {
const hitIndex = hit._index;
const hitId = hit._id;
const esqlQuery = `FROM ${hitIndex} ${hitId ? `METADATA _id\n | WHERE _id == "${hitId}"` : ''}`;
let timerange;
const source = hit._source as Record<string, unknown>;
if ('@timestamp' in source && isString(source['@timestamp']) && hitId) {
timerange = { from: source['@timestamp'], to: source['@timestamp'] };
}
return esqlQueryReference({
id,
query: esqlQuery,
label: `Index: ${hit._index}`,
timerange,
});
};

View file

@ -7,7 +7,7 @@
export const KNOWLEDGE_HISTORY =
'If available, use the Knowledge History provided to try and answer the question. If not provided, you can try and query for additional knowledge via the KnowledgeBaseRetrievalTool.';
export const INCLUDE_CITATIONS = `\n\nAnnotate your answer with relevant citations. For example: "The sky is blue. {reference(prSit)}"\n\n`;
export const INCLUDE_CITATIONS = `\n\nAnnotate your answer with relevant citations. Here are some example responses with citations: \n1. "Machine learning is increasingly used in cyber threat detection. {reference(prSit)}" \n2. "The alert has a risk score of 72. {reference(OdRs2)}"\n\nOnly use the citations returned by tools\n\n`;
export const DEFAULT_SYSTEM_PROMPT = `You are a security analyst and expert in resolving security incidents. Your role is to assist by answering questions about Elastic Security. Do not answer questions unrelated to Elastic Security. ${KNOWLEDGE_HISTORY} {include_citations_prompt_placeholder}`;
// system prompt from @afirstenberg
const BASE_GEMINI_PROMPT =

View file

@ -5,13 +5,15 @@
* 2.0.
*/
import type { ContentReferences } from '@kbn/elastic-assistant-common';
import { contentReferenceComponentFactory } from './content_reference_component_factory';
import type { ContentReference } from '@kbn/elastic-assistant-common';
import { ContentReferenceComponentFactory } from './content_reference_component_factory';
import React from 'react';
import { render, screen } from '@testing-library/react';
import type { ContentReferenceNode } from '../content_reference_parser';
const testContentReferenceNode = { contentReferenceId: '1' } as ContentReferenceNode;
import type {
InvalidContentReferenceNode,
ResolvedContentReferenceNode,
UnresolvedContentReferenceNode,
} from '../content_reference_parser';
jest.mock('../../../../common/lib/kibana', () => ({
useNavigation: jest.fn().mockReturnValue({
@ -34,89 +36,80 @@ describe('contentReferenceComponentFactory', () => {
[
'EsqlQueryReference',
{
'1': {
id: '1',
type: 'EsqlQuery',
query: '',
label: '',
},
} as ContentReferences,
testContentReferenceNode,
id: '1',
type: 'EsqlQuery',
query: '',
label: '',
} as ContentReference,
],
[
'KnowledgeBaseEntryReference',
{
'1': {
id: '1',
type: 'KnowledgeBaseEntry',
knowledgeBaseEntryId: '',
knowledgeBaseEntryName: '',
},
} as ContentReferences,
testContentReferenceNode,
id: '1',
type: 'KnowledgeBaseEntry',
knowledgeBaseEntryId: '',
knowledgeBaseEntryName: '',
} as ContentReference,
],
[
'ProductDocumentationReference',
{
'1': {
id: '1',
type: 'ProductDocumentation',
title: '',
url: '',
},
} as ContentReferences,
testContentReferenceNode,
id: '1',
type: 'ProductDocumentation',
title: '',
url: '',
} as ContentReference,
],
[
'SecurityAlertReference',
{
'1': {
id: '1',
type: 'SecurityAlert',
alertId: '',
},
} as ContentReferences,
testContentReferenceNode,
id: '1',
type: 'SecurityAlert',
alertId: '',
} as ContentReference,
],
[
'SecurityAlertsPageReference',
{
'1': {
id: '1',
type: 'SecurityAlertsPage',
},
} as ContentReferences,
testContentReferenceNode,
id: '1',
type: 'SecurityAlertsPage',
} as ContentReference,
],
])(
"Renders component: '%s'",
async (
testId: string,
contentReferences: ContentReferences,
contentReferenceNode: ContentReferenceNode
) => {
const Component = contentReferenceComponentFactory({
contentReferences,
contentReferencesVisible: true,
loading: false,
});
"Renders correct component for '%s'",
async (testId: string, contentReference: ContentReference) => {
const resolvedContentReferenceNode: ResolvedContentReferenceNode<ContentReference> = {
contentReferenceId: '1',
contentReferenceCount: 1,
contentReferenceBlock: '{reference(123)}',
contentReference,
type: 'contentReference',
};
render(<Component {...contentReferenceNode} />);
render(
<ContentReferenceComponentFactory
contentReferencesVisible
contentReferenceNode={resolvedContentReferenceNode}
/>
);
expect(screen.getByTestId(testId)).toBeInTheDocument();
}
);
it('renders nothing when specific contentReference is undefined', async () => {
const Component = contentReferenceComponentFactory({
contentReferences: {},
contentReferencesVisible: true,
loading: false,
});
it('renders nothing when specific contentReferenceNode is invalid', () => {
const invalidContentReferenceNode: InvalidContentReferenceNode = {
contentReferenceId: '1',
contentReferenceCount: undefined,
contentReferenceBlock: '{reference(123)}',
contentReference: undefined,
type: 'contentReference',
};
const { container } = render(
<Component
{...({ contentReferenceId: '1', contentReferenceCount: 1 } as ContentReferenceNode)}
<ContentReferenceComponentFactory
contentReferencesVisible
contentReferenceNode={invalidContentReferenceNode}
/>
);
@ -124,59 +117,23 @@ describe('contentReferenceComponentFactory', () => {
expect(screen.queryByText('[1]')).not.toBeInTheDocument();
});
it('renders placeholder if contentReferences are undefined and is loading', async () => {
const Component = contentReferenceComponentFactory({
contentReferences: undefined,
contentReferencesVisible: true,
loading: true,
});
it('renders placeholder if contentReferenceNode is unresolved', () => {
const unresolvedContentReferenceNode: UnresolvedContentReferenceNode = {
contentReferenceId: '1',
contentReferenceCount: 1,
contentReferenceBlock: '{reference(123)}',
contentReference: undefined,
type: 'contentReference',
};
render(
<Component
{...({ contentReferenceId: '1', contentReferenceCount: 1 } as ContentReferenceNode)}
<ContentReferenceComponentFactory
contentReferencesVisible
contentReferenceNode={unresolvedContentReferenceNode}
/>
);
expect(screen.getByTestId('ContentReferenceButton')).toBeInTheDocument();
expect(screen.getByText('[1]')).toBeInTheDocument();
});
it('renders nothing if contentReferences are undefined and is not loading', async () => {
const Component = contentReferenceComponentFactory({
contentReferences: undefined,
contentReferencesVisible: true,
loading: false,
});
const { container } = render(
<Component
{...({ contentReferenceId: '1', contentReferenceCount: 1 } as ContentReferenceNode)}
/>
);
expect(container).toBeEmptyDOMElement();
expect(screen.queryByText('[1]')).not.toBeInTheDocument();
});
it('renders nothing if contentReferenceId is empty string', async () => {
const Component = contentReferenceComponentFactory({
contentReferences: {
'1': {
id: '1',
type: 'SecurityAlertsPage',
},
} as ContentReferences,
contentReferencesVisible: true,
loading: false,
});
const { container } = render(
<Component
{...({ contentReferenceId: '', contentReferenceCount: -1 } as ContentReferenceNode)}
/>
);
expect(container).toBeEmptyDOMElement();
expect(screen.queryByText('[-1]')).not.toBeInTheDocument();
});
});

View file

@ -5,9 +5,19 @@
* 2.0.
*/
import type { ContentReferences } from '@kbn/elastic-assistant-common';
import type {
ContentReferences,
EsqlContentReference,
KnowledgeBaseEntryContentReference,
ProductDocumentationContentReference,
SecurityAlertContentReference,
SecurityAlertsPageContentReference,
} from '@kbn/elastic-assistant-common';
import React from 'react';
import type { ContentReferenceNode } from '../content_reference_parser';
import type {
ContentReferenceNode,
ResolvedContentReferenceNode,
} from '../content_reference_parser';
import { KnowledgeBaseEntryReference } from './knowledge_base_entry_reference';
import { SecurityAlertReference } from './security_alert_reference';
import { SecurityAlertsPageReference } from './security_alerts_page_reference';
@ -15,78 +25,75 @@ import { ContentReferenceButton } from './content_reference_button';
import { ProductDocumentationReference } from './product_documentation_reference';
import { EsqlQueryReference } from './esql_query_reference';
export interface ContentReferenceComponentFactory {
contentReferences?: ContentReferences;
/** While a message is being streamed, content references are null. When a message has finished streaming, content references are either defined or undefined */
export type StreamingOrFinalContentReferences = ContentReferences | undefined | null;
export interface Props {
contentReferencesVisible: boolean;
loading: boolean;
contentReferenceNode: ContentReferenceNode;
}
export const contentReferenceComponentFactory = ({
contentReferences,
export const ContentReferenceComponentFactory: React.FC<Props> = ({
contentReferencesVisible,
loading,
}: ContentReferenceComponentFactory) => {
const ContentReferenceComponent = (
contentReferenceNode: ContentReferenceNode
): React.ReactNode => {
if (!contentReferencesVisible) return null;
if (!contentReferenceNode.contentReferenceId) return null;
contentReferenceNode,
}: Props) => {
if (!contentReferencesVisible) return null;
const defaultNode = (
if (contentReferenceNode.contentReferenceCount === undefined) return null;
if (contentReferenceNode.contentReference === undefined) {
return (
<ContentReferenceButton
disabled
contentReferenceCount={contentReferenceNode.contentReferenceCount}
/>
);
}
if (!contentReferences && loading) return defaultNode;
const contentReference = contentReferences?.[contentReferenceNode.contentReferenceId];
if (!contentReference) return null;
switch (contentReference.type) {
case 'KnowledgeBaseEntry':
return (
<KnowledgeBaseEntryReference
contentReferenceNode={contentReferenceNode}
knowledgeBaseEntryContentReference={contentReference}
/>
);
case 'SecurityAlert':
return (
<SecurityAlertReference
contentReferenceNode={contentReferenceNode}
securityAlertContentReference={contentReference}
/>
);
case 'SecurityAlertsPage':
return (
<SecurityAlertsPageReference
contentReferenceNode={contentReferenceNode}
securityAlertsPageContentReference={contentReference}
/>
);
case 'ProductDocumentation':
return (
<ProductDocumentationReference
contentReferenceNode={contentReferenceNode}
productDocumentationContentReference={contentReference}
/>
);
case 'EsqlQuery':
return (
<EsqlQueryReference
contentReferenceNode={contentReferenceNode}
esqlContentReference={contentReference}
/>
);
default:
return defaultNode;
switch (contentReferenceNode.contentReference.type) {
case 'KnowledgeBaseEntry': {
return (
<KnowledgeBaseEntryReference
contentReferenceNode={
contentReferenceNode as ResolvedContentReferenceNode<KnowledgeBaseEntryContentReference>
}
/>
);
}
};
ContentReferenceComponent.displayName = 'ContentReferenceComponent';
return ContentReferenceComponent;
case 'SecurityAlert':
return (
<SecurityAlertReference
contentReferenceNode={
contentReferenceNode as ResolvedContentReferenceNode<SecurityAlertContentReference>
}
/>
);
case 'SecurityAlertsPage':
return (
<SecurityAlertsPageReference
contentReferenceNode={
contentReferenceNode as ResolvedContentReferenceNode<SecurityAlertsPageContentReference>
}
/>
);
case 'ProductDocumentation':
return (
<ProductDocumentationReference
contentReferenceNode={
contentReferenceNode as ResolvedContentReferenceNode<ProductDocumentationContentReference>
}
/>
);
case 'EsqlQuery': {
return (
<EsqlQueryReference
contentReferenceNode={
contentReferenceNode as ResolvedContentReferenceNode<EsqlContentReference>
}
/>
);
}
default:
return null;
}
};

View file

@ -8,19 +8,15 @@
import type { EsqlContentReference } from '@kbn/elastic-assistant-common';
import React, { useCallback } from 'react';
import { EuiLink } from '@elastic/eui';
import type { ContentReferenceNode } from '../content_reference_parser';
import type { ResolvedContentReferenceNode } from '../content_reference_parser';
import { PopoverReference } from './popover_reference';
import { useKibana } from '../../../../common/lib/kibana';
interface Props {
contentReferenceNode: ContentReferenceNode;
esqlContentReference: EsqlContentReference;
contentReferenceNode: ResolvedContentReferenceNode<EsqlContentReference>;
}
export const EsqlQueryReference: React.FC<Props> = ({
contentReferenceNode,
esqlContentReference,
}) => {
export const EsqlQueryReference: React.FC<Props> = ({ contentReferenceNode }) => {
const {
discover: { locator },
application: { navigateToApp },
@ -34,8 +30,9 @@ export const EsqlQueryReference: React.FC<Props> = ({
}
const url = await locator.getLocation({
query: {
esql: esqlContentReference.query,
esql: contentReferenceNode.contentReference.query,
},
timeRange: contentReferenceNode.contentReference.timerange,
});
navigateToApp(url.app, {
@ -43,7 +40,7 @@ export const EsqlQueryReference: React.FC<Props> = ({
openInNewTab: true,
});
},
[locator, esqlContentReference.query, navigateToApp]
[locator, contentReferenceNode, navigateToApp]
);
return (
@ -51,7 +48,7 @@ export const EsqlQueryReference: React.FC<Props> = ({
contentReferenceCount={contentReferenceNode.contentReferenceCount}
data-test-subj="EsqlQueryReference"
>
<EuiLink onClick={onClick}>{esqlContentReference.label}</EuiLink>
<EuiLink onClick={onClick}>{contentReferenceNode.contentReference.label}</EuiLink>
</PopoverReference>
);
};

View file

@ -9,30 +9,26 @@ import type { KnowledgeBaseEntryContentReference } from '@kbn/elastic-assistant-
import React, { useCallback } from 'react';
import { EuiLink } from '@elastic/eui';
import { KNOWLEDGE_BASE_ENTRY_REFERENCE_LABEL } from './translations';
import type { ContentReferenceNode } from '../content_reference_parser';
import type { ResolvedContentReferenceNode } from '../content_reference_parser';
import { PopoverReference } from './popover_reference';
import { useKibana } from '../../../../common/lib/kibana';
interface Props {
contentReferenceNode: ContentReferenceNode;
knowledgeBaseEntryContentReference: KnowledgeBaseEntryContentReference;
contentReferenceNode: ResolvedContentReferenceNode<KnowledgeBaseEntryContentReference>;
}
export const KnowledgeBaseEntryReference: React.FC<Props> = ({
contentReferenceNode,
knowledgeBaseEntryContentReference,
}) => {
export const KnowledgeBaseEntryReference: React.FC<Props> = ({ contentReferenceNode }) => {
const { navigateToApp } = useKibana().services.application;
const onClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
navigateToApp('management', {
path: `kibana/securityAiAssistantManagement?tab=knowledge_base&entry_search_term=${knowledgeBaseEntryContentReference.knowledgeBaseEntryId}`,
path: `kibana/securityAiAssistantManagement?tab=knowledge_base&entry_search_term=${contentReferenceNode.contentReference.knowledgeBaseEntryId}`,
openInNewTab: true,
});
},
[navigateToApp, knowledgeBaseEntryContentReference]
[navigateToApp, contentReferenceNode]
);
return (
@ -41,7 +37,7 @@ export const KnowledgeBaseEntryReference: React.FC<Props> = ({
data-test-subj="KnowledgeBaseEntryReference"
>
<EuiLink onClick={onClick}>
{`${KNOWLEDGE_BASE_ENTRY_REFERENCE_LABEL}: ${knowledgeBaseEntryContentReference.knowledgeBaseEntryName}`}
{`${KNOWLEDGE_BASE_ENTRY_REFERENCE_LABEL}: ${contentReferenceNode.contentReference.knowledgeBaseEntryName}`}
</EuiLink>
</PopoverReference>
);

View file

@ -8,25 +8,21 @@
import type { ProductDocumentationContentReference } from '@kbn/elastic-assistant-common';
import React from 'react';
import { EuiLink } from '@elastic/eui';
import type { ContentReferenceNode } from '../content_reference_parser';
import type { ResolvedContentReferenceNode } from '../content_reference_parser';
import { PopoverReference } from './popover_reference';
interface Props {
contentReferenceNode: ContentReferenceNode;
productDocumentationContentReference: ProductDocumentationContentReference;
contentReferenceNode: ResolvedContentReferenceNode<ProductDocumentationContentReference>;
}
export const ProductDocumentationReference: React.FC<Props> = ({
contentReferenceNode,
productDocumentationContentReference,
}) => {
export const ProductDocumentationReference: React.FC<Props> = ({ contentReferenceNode }) => {
return (
<PopoverReference
contentReferenceCount={contentReferenceNode.contentReferenceCount}
data-test-subj="ProductDocumentationReference"
>
<EuiLink href={productDocumentationContentReference.url} target="_blank">
{productDocumentationContentReference.title}
<EuiLink href={contentReferenceNode.contentReference.url} target="_blank">
{contentReferenceNode.contentReference.title}
</EuiLink>
</PopoverReference>
);

View file

@ -9,30 +9,26 @@ import type { SecurityAlertContentReference } from '@kbn/elastic-assistant-commo
import React, { useCallback } from 'react';
import { EuiLink } from '@elastic/eui';
import { SECURITY_ALERT_REFERENCE_LABEL } from './translations';
import type { ContentReferenceNode } from '../content_reference_parser';
import type { ResolvedContentReferenceNode } from '../content_reference_parser';
import { PopoverReference } from './popover_reference';
import { useKibana } from '../../../../common/lib/kibana';
interface Props {
contentReferenceNode: ContentReferenceNode;
securityAlertContentReference: SecurityAlertContentReference;
contentReferenceNode: ResolvedContentReferenceNode<SecurityAlertContentReference>;
}
export const SecurityAlertReference: React.FC<Props> = ({
contentReferenceNode,
securityAlertContentReference,
}) => {
export const SecurityAlertReference: React.FC<Props> = ({ contentReferenceNode }) => {
const { navigateToApp } = useKibana().services.application;
const onClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
navigateToApp('security', {
path: `alerts/redirect/${securityAlertContentReference.alertId}`,
path: `alerts/redirect/${contentReferenceNode.contentReference.alertId}`,
openInNewTab: true,
});
},
[navigateToApp, securityAlertContentReference]
[navigateToApp, contentReferenceNode]
);
return (

View file

@ -8,21 +8,17 @@
import type { SecurityAlertsPageContentReference } from '@kbn/elastic-assistant-common';
import React, { useCallback } from 'react';
import { EuiLink } from '@elastic/eui';
import type { ContentReferenceNode } from '../content_reference_parser';
import type { ResolvedContentReferenceNode } from '../content_reference_parser';
import { PopoverReference } from './popover_reference';
import { SECURITY_ALERTS_PAGE_REFERENCE_LABEL } from './translations';
import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
import { FILTER_OPEN, FILTER_ACKNOWLEDGED } from '../../../../../common/types';
interface Props {
contentReferenceNode: ContentReferenceNode;
securityAlertsPageContentReference: SecurityAlertsPageContentReference;
contentReferenceNode: ResolvedContentReferenceNode<SecurityAlertsPageContentReference>;
}
export const SecurityAlertsPageReference: React.FC<Props> = ({
contentReferenceNode,
securityAlertsPageContentReference,
}) => {
export const SecurityAlertsPageReference: React.FC<Props> = ({ contentReferenceNode }) => {
const openAlertsPageWithFilters = useNavigateToAlertsPageWithFilters();
const onClick = useCallback(

View file

@ -8,12 +8,14 @@
import unified from 'unified';
import markdown from 'remark-parse-no-trim';
import type { Parent } from 'mdast';
import { ContentReferenceParser } from './content_reference_parser';
import { contentReferenceParser } from './content_reference_parser';
describe('ContentReferenceParser', () => {
it('extracts references from poem', async () => {
const file = unified().use([[markdown, {}], ContentReferenceParser])
.parse(`With a wagging tail and a wet, cold nose,{reference(ccaSI)}
it('extracts references from poem', () => {
const file = unified().use([
[markdown, {}],
contentReferenceParser({ contentReferences: null }),
]).parse(`With a wagging tail and a wet, cold nose,{reference(ccaSI)}
A furry friend, from head to toes.{reference(ccaSI)}
Loyal companion, always near,{reference(ccaSI)}
Chasing squirrels, full of cheer.{reference(ccaSI)}
@ -37,8 +39,11 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
);
});
it('extracts reference after linebreak', async () => {
const file = unified().use([[markdown, {}], ContentReferenceParser]).parse(`First line
it('extracts reference after linebreak', () => {
const file = unified().use([
[markdown, {}],
contentReferenceParser({ contentReferences: null }),
]).parse(`First line
{reference(FTQJp)}
`) as Parent;
@ -50,9 +55,9 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
);
});
it('eats empty content reference', async () => {
it('eats empty content reference', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('There is an empty content reference.{reference()}') as Parent;
expect(file.children[0].children).toEqual(
@ -60,16 +65,51 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
expect.objectContaining({ type: 'text', value: 'There is an empty content reference.' }),
expect.objectContaining({
type: 'contentReference',
contentReferenceCount: -1,
contentReferenceId: '',
contentReferenceCount: 1,
}),
])
);
});
it('eats space preceding content reference', async () => {
it('invalid content reference has correct contentReferenceCount', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([
[markdown, {}],
contentReferenceParser({
contentReferences: {
valid1: { id: 'valid1', type: 'SecurityAlertsPage' },
valid2: { id: 'valid2', type: 'SecurityAlertsPage' },
},
}),
])
.parse(
'There {reference(valid1)} is one invalid content reference {reference(invalid)} and two valid ones. {reference(valid2)}'
) as Parent;
expect(file.children[0].children).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'contentReference',
contentReferenceCount: 1,
contentReferenceId: 'valid1',
}),
expect.objectContaining({
type: 'contentReference',
contentReferenceCount: undefined,
contentReferenceId: 'invalid',
}),
expect.objectContaining({
type: 'contentReference',
contentReferenceCount: 2,
contentReferenceId: 'valid2',
}),
])
);
});
it('eats space preceding content reference', () => {
const file = unified()
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('Delete space after punctuation. {reference(example)}') as Parent;
expect(file.children[0].children).toEqual(
@ -80,22 +120,42 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
);
});
it('parses when there is no space preceding the content reference', async () => {
it('parses when there is no space preceding the content reference', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('No preceding space.{reference(example)}') as Parent;
expect(file.children[0].children).toEqual(
expect.arrayContaining([expect.objectContaining({ type: 'contentReference' })])
);
});
it('correct content reference count when contentReferences is null', () => {
const file = unified()
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('No preceding space.{reference(example)} {reference(example2)}') as Parent;
expect(file.children[0].children).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: 'text', value: 'No preceding space.' }),
expect.objectContaining({ type: 'contentReference' }),
expect.objectContaining({
type: 'contentReference',
contentReferenceId: 'example',
contentReferenceCount: 1,
contentReference: undefined,
}),
expect.objectContaining({
type: 'contentReference',
contentReferenceId: 'example2',
contentReferenceCount: 2,
contentReference: undefined,
}),
])
);
});
it('handles single citation block', async () => {
it('handles single citation block', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('Hello world {reference(example)} hello wolrd') as Parent;
expect(file.children[0].children).toEqual([
@ -155,9 +215,9 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
]);
});
it('handles multiple citation blocks with different referenceIds', async () => {
it('handles multiple citation blocks with different referenceIds', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('Hello world {reference(example)} hello world {reference(example2)}') as Parent;
expect(file.children[0].children).toEqual([
@ -236,9 +296,9 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
]);
});
it('handles multiple citation blocks with same referenceIds', async () => {
it('handles multiple citation blocks with same referenceIds', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('Hello world {reference(example)} hello world {reference(example)}') as Parent;
expect(file.children[0].children).toEqual([
@ -317,9 +377,9 @@ Their love's a beacon, shining bright.{reference(ccaSI)}`) as Parent;
]);
});
it('handles partial citation blocks', async () => {
it('handles partial citation blocks', () => {
const file = unified()
.use([[markdown, {}], ContentReferenceParser])
.use([[markdown, {}], contentReferenceParser({ contentReferences: null })])
.parse('Hello world {reference(example)} hello world {reference(') as Parent;
expect(file.children[0].children).toEqual([

View file

@ -6,122 +6,183 @@
*/
import type { RemarkTokenizer } from '@elastic/eui';
import type { ContentReferenceBlock } from '@kbn/elastic-assistant-common';
import type { ContentReference, ContentReferenceBlock } from '@kbn/elastic-assistant-common';
import type { Plugin } from 'unified';
import type { Node } from 'unist';
import type { StreamingOrFinalContentReferences } from './components/content_reference_component_factory';
export interface ContentReferenceNode extends Node {
/** A ContentReferenceNode that has been extracted from the message and the content reference details are available. */
export interface ResolvedContentReferenceNode<T extends ContentReference> extends Node {
type: 'contentReference';
contentReferenceId: string;
contentReferenceCount: number;
contentReferenceBlock: ContentReferenceBlock;
contentReference: T;
}
/** A ContentReferenceNode that has been extracted from the message but the content reference details are not available on the client **yet**. When the message finishes streaming, the details will become available. */
export interface UnresolvedContentReferenceNode extends Node {
type: 'contentReference';
contentReferenceId: string;
contentReferenceCount: number;
contentReferenceBlock: ContentReferenceBlock;
contentReference: undefined;
}
/** A ContentReferenceNode that has been extracted from the message but the content reference details are erroneous. */
export interface InvalidContentReferenceNode extends Node {
type: 'contentReference';
contentReferenceId: string;
contentReferenceCount: undefined;
contentReferenceBlock: ContentReferenceBlock;
contentReference: undefined;
}
export type ContentReferenceNode =
| ResolvedContentReferenceNode<ContentReference>
| UnresolvedContentReferenceNode
| InvalidContentReferenceNode;
interface Params {
contentReferences: StreamingOrFinalContentReferences;
}
/** Matches `{reference` and ` {reference(` */
const REFERENCE_START_PATTERN = '\\u0020?\\{reference';
export const ContentReferenceParser: Plugin = function ContentReferenceParser() {
const Parser = this.Parser;
const tokenizers = Parser.prototype.inlineTokenizers;
const methods = Parser.prototype.inlineMethods;
export const contentReferenceParser: (params: Params) => Plugin = ({ contentReferences }) =>
function ContentReferenceParser() {
const Parser = this.Parser;
const tokenizers = Parser.prototype.inlineTokenizers;
const methods = Parser.prototype.inlineMethods;
let currentContentReferenceCount = 1;
const contentReferenceCounts: Record<string, number> = {};
let currentContentReferenceCount = 1;
const contentReferenceCounts: Record<string, number> = {};
const tokenizeCustomCitation: RemarkTokenizer = function tokenizeCustomCitation(
eat,
value,
silent
) {
const [match] = value.match(new RegExp(`^${REFERENCE_START_PATTERN}`)) || [];
const tokenizeCustomCitation: RemarkTokenizer = function tokenizeCustomCitation(
eat,
value,
silent
) {
const [match] = value.match(new RegExp(`^${REFERENCE_START_PATTERN}`)) || [];
if (!match) return false;
if (!match) return false;
if (value[match.length] !== '(') return false;
if (value[match.length] !== '(') return false;
let index = match.length;
let index = match.length;
function readArg(open: string, close: string) {
if (value[index] !== open) return '';
index++;
function readArg(open: string, close: string) {
if (value[index] !== open) return '';
index++;
let body = '';
let openBrackets = 0;
let body = '';
let openBrackets = 0;
for (; index < value.length; index++) {
const char = value[index];
if (char === close && openBrackets === 0) {
index++;
for (; index < value.length; index++) {
const char = value[index];
if (char === close && openBrackets === 0) {
index++;
return body;
} else if (char === close) {
openBrackets--;
} else if (char === open) {
openBrackets++;
return body;
} else if (char === close) {
openBrackets--;
} else if (char === open) {
openBrackets++;
}
body += char;
}
body += char;
return '';
}
return '';
}
const contentReferenceId = readArg('(', ')');
const contentReferenceId = readArg('(', ')');
const closeChar = value[index];
if (closeChar !== '}') return false;
const closeChar = value[index];
if (closeChar !== '}') return false;
const now = eat.now();
const now = eat.now();
if (!contentReferenceId) {
this.file.info('No content reference id found', {
line: now.line,
column: now.column + match.length + 1,
});
}
if (!contentReferenceId) {
this.file.info('No content reference id found', {
line: now.line,
column: now.column + match.length + 1,
});
}
if (silent) {
return true;
}
if (silent) {
return true;
}
now.column += match.length + 1;
now.offset += match.length + 1;
now.column += match.length + 1;
now.offset += match.length + 1;
const contentReferenceBlock: ContentReferenceBlock = `{reference(${contentReferenceId})}`;
const contentReference = contentReferences?.[contentReferenceId];
const contentReferenceBlock: ContentReferenceBlock = `{reference(${contentReferenceId})}`;
const getContentReferenceCount = () => {
// If the content reference id is already in the contentReferenceCounts, return the existing count
if (contentReferenceId in contentReferenceCounts) {
return contentReferenceCounts[contentReferenceId];
}
// If the content reference id is not in the contentReferenceCounts, increment the currentContentReferenceCount and return the new count
contentReferenceCounts[contentReferenceId] = currentContentReferenceCount++;
return contentReferenceCounts[contentReferenceId];
};
const getContentReferenceCount = (id: string) => {
if (!id) {
const toEat = `${match.startsWith(' ') ? ' ' : ''}${contentReferenceBlock}`;
if (contentReferences === null) {
// The message is still streaming, so the content reference details are not available yet
const contentReferenceNode: UnresolvedContentReferenceNode = {
type: 'contentReference',
contentReferenceId,
contentReferenceCount: getContentReferenceCount(),
contentReferenceBlock,
contentReference: undefined,
};
return eat(toEat)(contentReferenceNode);
}
if (contentReference === undefined) {
// The message has finished streaming, but the content reference details were not found
const contentReferenceNode: InvalidContentReferenceNode = {
type: 'contentReference',
contentReferenceId,
contentReferenceCount: undefined,
contentReferenceBlock,
contentReference,
};
return eat(toEat)(contentReferenceNode);
}
// The message has finished streaming and the content reference details were found
const contentReferenceNode: ResolvedContentReferenceNode<ContentReference> = {
type: 'contentReference',
contentReferenceId,
contentReferenceCount: getContentReferenceCount(),
contentReferenceBlock,
contentReference,
};
return eat(toEat)(contentReferenceNode);
};
tokenizeCustomCitation.notInLink = true;
tokenizeCustomCitation.locator = (value, fromIndex) => {
const nextIndex = value
.substring(fromIndex)
.match(new RegExp(REFERENCE_START_PATTERN))?.index;
if (nextIndex === undefined) {
return -1;
}
if (id in contentReferenceCounts) {
return contentReferenceCounts[id];
}
contentReferenceCounts[id] = currentContentReferenceCount++;
return contentReferenceCounts[id];
return nextIndex + 1;
};
const toEat = `${match.startsWith(' ') ? ' ' : ''}${contentReferenceBlock}`;
const contentReferenceNode: ContentReferenceNode = {
type: 'contentReference',
contentReferenceId,
contentReferenceCount: getContentReferenceCount(contentReferenceId),
contentReferenceBlock,
};
return eat(toEat)(contentReferenceNode);
tokenizers.contentReference = tokenizeCustomCitation;
methods.splice(methods.indexOf('text'), 0, 'contentReference');
};
tokenizeCustomCitation.notInLink = true;
tokenizeCustomCitation.locator = (value, fromIndex) => {
const nextIndex = value.substring(fromIndex).match(new RegExp(REFERENCE_START_PATTERN))?.index;
if (nextIndex === undefined) {
return -1;
}
return nextIndex + 1;
};
tokenizers.contentReference = tokenizeCustomCitation;
methods.splice(methods.indexOf('text'), 0, 'contentReference');
};

View file

@ -88,6 +88,7 @@ export const getComments: GetAssistantMessages = ({
regenerateMessage={regenerateMessageOfConversation}
setIsStreaming={setIsStreaming}
transformMessage={() => ({ content: '' } as unknown as ContentMessage)}
contentReferences={null}
isFetching
// we never need to append to a code block in the loading comment, which is what this index is used for
index={999}
@ -131,6 +132,7 @@ export const getComments: GetAssistantMessages = ({
refetchCurrentConversation={refetchCurrentConversation}
regenerateMessage={regenerateMessageOfConversation}
setIsStreaming={setIsStreaming}
contentReferences={null}
transformMessage={() => ({ content: '' } as unknown as ContentMessage)}
// we never need to append to a code block in the system comment, which is what this index is used for
index={999}
@ -176,7 +178,7 @@ export const getComments: GetAssistantMessages = ({
children: (
<StreamComment
abortStream={abortStream}
contentReferences={message.metadata?.contentReferences}
contentReferences={null}
contentReferencesVisible={contentReferencesVisible}
contentReferencesEnabled={contentReferencesEnabled}
index={index}

View file

@ -13,11 +13,28 @@ import { StreamComment } from '.';
import { useStream } from './use_stream';
import type { Connector } from '@kbn/actions-plugin/server/application/connector/types';
import type { AsApiContract } from '@kbn/actions-plugin/common';
const mockSetComplete = jest.fn();
jest.mock('../../../detection_engine/rule_management/api/hooks/use_fetch_connectors_query');
jest.mock('./use_stream');
jest.mock('../../../common/lib/kibana', () => ({
useNavigation: jest.fn().mockReturnValue({
navigateTo: jest.fn(),
}),
useKibana: jest.fn().mockReturnValue({
services: {
discover: {
locator: jest.fn(),
},
application: {
navigateToApp: jest.fn(),
},
},
}),
}));
const content = 'Test Content';
const mockAbortStream = jest.fn();
const testProps = {
@ -30,6 +47,7 @@ const testProps = {
regenerateMessage: jest.fn(),
setIsStreaming: jest.fn(),
transformMessage: jest.fn(),
contentReferences: undefined,
};
const mockReader = jest.fn() as unknown as ReadableStreamDefaultReader<Uint8Array>;
@ -61,6 +79,61 @@ describe('StreamComment', () => {
expect(screen.getByText(content)).toBeInTheDocument();
});
it('renders citations correctly when content references are defined', () => {
render(
<StreamComment
{...{
...testProps,
content: 'the sky is blue {reference(1234)}',
contentReferencesEnabled: true,
contentReferencesVisible: true,
contentReferences: {
'1234': {
id: '1234',
type: 'SecurityAlertsPage',
},
},
}}
/>
);
expect(screen.getByText('[1]')).toBeInTheDocument();
expect(screen.getByTestId('ContentReferenceButton')).toBeEnabled();
});
it('renders citations correctly when content references null', () => {
render(
<StreamComment
{...{
...testProps,
content: 'the sky is blue {reference(1234)}',
contentReferencesEnabled: true,
contentReferencesVisible: true,
contentReferences: null,
}}
/>
);
expect(screen.getByText('[1]')).toBeInTheDocument();
expect(screen.getByTestId('ContentReferenceButton')).not.toBeEnabled();
});
it('renders citations correctly when content references are undefined', () => {
render(
<StreamComment
{...{
...testProps,
content: 'the sky is blue {reference(1234)}',
contentReferencesEnabled: true,
contentReferencesVisible: true,
contentReferences: undefined,
}}
/>
);
expect(screen.queryByText('[1]')).not.toBeInTheDocument();
});
it('renders cursor when content is loading', () => {
render(<StreamComment {...testProps} isFetching={true} />);
expect(screen.getByTestId('cursor')).toBeInTheDocument();

View file

@ -7,18 +7,18 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { ContentReferences } from '@kbn/elastic-assistant-common';
import type { ContentMessage } from '..';
import { useStream } from './use_stream';
import { StopGeneratingButton } from './buttons/stop_generating_button';
import { RegenerateResponseButton } from './buttons/regenerate_response_button';
import { MessagePanel } from './message_panel';
import { MessageText } from './message_text';
import type { StreamingOrFinalContentReferences } from '../content_reference/components/content_reference_component_factory';
interface Props {
abortStream: () => void;
content?: string;
contentReferences?: ContentReferences;
contentReferences: StreamingOrFinalContentReferences;
contentReferencesVisible?: boolean;
contentReferencesEnabled?: boolean;
isError?: boolean;
@ -116,8 +116,8 @@ export const StreamComment = ({
contentReferences={contentReferences}
contentReferencesEnabled={contentReferencesEnabled}
index={index}
loading={isAnythingLoading}
contentReferencesVisible={contentReferencesVisible}
loading={isAnythingLoading}
/>
}
error={error ? new Error(error) : undefined}

View file

@ -20,15 +20,15 @@ import { css } from '@emotion/react';
import type { Code, InlineCode, Parent, Text } from 'mdast';
import React, { useMemo } from 'react';
import type { Node } from 'unist';
import type { ContentReferences } from '@kbn/elastic-assistant-common';
import { customCodeBlockLanguagePlugin } from '../custom_codeblock/custom_codeblock_markdown_plugin';
import { CustomCodeBlock } from '../custom_codeblock/custom_code_block';
import { ContentReferenceParser } from '../content_reference/content_reference_parser';
import { contentReferenceComponentFactory } from '../content_reference/components/content_reference_component_factory';
import { contentReferenceParser } from '../content_reference/content_reference_parser';
import type { StreamingOrFinalContentReferences } from '../content_reference/components/content_reference_component_factory';
import { ContentReferenceComponentFactory } from '../content_reference/components/content_reference_component_factory';
interface Props {
content: string;
contentReferences?: ContentReferences;
contentReferences: StreamingOrFinalContentReferences;
contentReferencesVisible: boolean;
contentReferencesEnabled: boolean;
index: number;
@ -105,8 +105,7 @@ const loadingCursorPlugin = () => {
};
interface GetPluginDependencies {
contentReferences?: ContentReferences;
loading: boolean;
contentReferences: StreamingOrFinalContentReferences;
contentReferencesVisible: boolean;
contentReferencesEnabled: boolean;
}
@ -114,7 +113,6 @@ interface GetPluginDependencies {
const getPluginDependencies = ({
contentReferences,
contentReferencesVisible,
loading,
contentReferencesEnabled,
}: GetPluginDependencies) => {
const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
@ -127,11 +125,14 @@ const getPluginDependencies = ({
...components,
...(contentReferencesEnabled
? {
contentReference: contentReferenceComponentFactory({
contentReferences,
contentReferencesVisible,
loading,
}),
contentReference: (contentReferenceNode) => {
return (
<ContentReferenceComponentFactory
contentReferencesVisible={contentReferencesVisible}
contentReferenceNode={contentReferenceNode}
/>
);
},
}
: {}),
cursor: Cursor,
@ -169,7 +170,7 @@ const getPluginDependencies = ({
loadingCursorPlugin,
customCodeBlockLanguagePlugin,
...parsingPlugins,
...(contentReferencesEnabled ? [ContentReferenceParser] : []),
...(contentReferencesEnabled ? [contentReferenceParser({ contentReferences })] : []),
],
processingPluginList: processingPlugins,
};
@ -194,9 +195,8 @@ export function MessageText({
contentReferences,
contentReferencesVisible,
contentReferencesEnabled,
loading,
}),
[contentReferences, contentReferencesVisible, contentReferencesEnabled, loading]
[contentReferences, contentReferencesVisible, contentReferencesEnabled]
);
return (