mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
DashGPT: Fixes issue with generation on Safari (#90337)
* DashGPT: Fixes issue with generation on Safari * Fix infinite loop issue + more refactoring * Solve linter --------- Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
parent
0a337ff3b3
commit
d8b6a5bde2
@ -3119,9 +3119,7 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/features/dashboard/components/GenAI/GenAIHistory.tsx:5381": [
|
"public/app/features/dashboard/components/GenAI/GenAIHistory.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard/components/GenAI/MinimalisticPagination.tsx:5381": [
|
"public/app/features/dashboard/components/GenAI/MinimalisticPagination.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||||
|
@ -48,7 +48,7 @@ describe('GenAIButton', () => {
|
|||||||
streamStatus: StreamStatus.IDLE,
|
streamStatus: StreamStatus.IDLE,
|
||||||
reply: 'Some completed genereated text',
|
reply: 'Some completed genereated text',
|
||||||
setMessages: jest.fn(),
|
setMessages: jest.fn(),
|
||||||
setStopGeneration: jest.fn(),
|
stopGeneration: jest.fn(),
|
||||||
value: {
|
value: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
stream: new Observable().subscribe(),
|
stream: new Observable().subscribe(),
|
||||||
@ -74,9 +74,9 @@ describe('GenAIButton', () => {
|
|||||||
messages: [],
|
messages: [],
|
||||||
error: undefined,
|
error: undefined,
|
||||||
streamStatus: StreamStatus.IDLE,
|
streamStatus: StreamStatus.IDLE,
|
||||||
reply: 'Some completed genereated text',
|
reply: 'Some completed generated text',
|
||||||
setMessages: setMessagesMock,
|
setMessages: setMessagesMock,
|
||||||
setStopGeneration: setShouldStopMock,
|
stopGeneration: setShouldStopMock,
|
||||||
value: {
|
value: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
stream: new Observable().subscribe(),
|
stream: new Observable().subscribe(),
|
||||||
@ -84,7 +84,7 @@ describe('GenAIButton', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render text ', async () => {
|
it('should render text', async () => {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
waitFor(async () => expect(await screen.findByText('Auto-generate')).toBeInTheDocument());
|
waitFor(async () => expect(await screen.findByText('Auto-generate')).toBeInTheDocument());
|
||||||
@ -162,7 +162,7 @@ describe('GenAIButton', () => {
|
|||||||
streamStatus: StreamStatus.GENERATING,
|
streamStatus: StreamStatus.GENERATING,
|
||||||
reply: 'Some incomplete generated text',
|
reply: 'Some incomplete generated text',
|
||||||
setMessages: jest.fn(),
|
setMessages: jest.fn(),
|
||||||
setStopGeneration: setShouldStopMock,
|
stopGeneration: setShouldStopMock,
|
||||||
value: {
|
value: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
stream: new Observable().subscribe(),
|
stream: new Observable().subscribe(),
|
||||||
@ -170,7 +170,7 @@ describe('GenAIButton', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render loading text ', async () => {
|
it('should render loading text', async () => {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
waitFor(async () => expect(await screen.findByText('Auto-generate')).toBeInTheDocument());
|
waitFor(async () => expect(await screen.findByText('Auto-generate')).toBeInTheDocument());
|
||||||
@ -204,7 +204,6 @@ describe('GenAIButton', () => {
|
|||||||
await fireEvent.click(generateButton);
|
await fireEvent.click(generateButton);
|
||||||
|
|
||||||
expect(setShouldStopMock).toHaveBeenCalledTimes(1);
|
expect(setShouldStopMock).toHaveBeenCalledTimes(1);
|
||||||
expect(setShouldStopMock).toHaveBeenCalledWith(true);
|
|
||||||
expect(onGenerate).not.toHaveBeenCalled();
|
expect(onGenerate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -213,18 +212,27 @@ describe('GenAIButton', () => {
|
|||||||
const setShouldStopMock = jest.fn();
|
const setShouldStopMock = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.mocked(useOpenAIStream).mockReturnValue({
|
const reply = 'Some completed generated text';
|
||||||
|
const returnValue = {
|
||||||
messages: [],
|
messages: [],
|
||||||
error: undefined,
|
error: undefined,
|
||||||
streamStatus: StreamStatus.COMPLETED,
|
streamStatus: StreamStatus.COMPLETED,
|
||||||
reply: 'Some completed generated text',
|
reply,
|
||||||
setMessages: jest.fn(),
|
setMessages: jest.fn(),
|
||||||
setStopGeneration: setShouldStopMock,
|
stopGeneration: setShouldStopMock,
|
||||||
value: {
|
value: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
stream: new Observable().subscribe(),
|
stream: new Observable().subscribe(),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.mocked(useOpenAIStream)
|
||||||
|
.mockImplementationOnce((options) => {
|
||||||
|
options?.onResponse?.(reply);
|
||||||
|
return returnValue;
|
||||||
|
})
|
||||||
|
.mockImplementation(() => returnValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render improve text ', async () => {
|
it('should render improve text ', async () => {
|
||||||
@ -260,7 +268,7 @@ describe('GenAIButton', () => {
|
|||||||
streamStatus: StreamStatus.IDLE,
|
streamStatus: StreamStatus.IDLE,
|
||||||
reply: '',
|
reply: '',
|
||||||
setMessages: setMessagesMock,
|
setMessages: setMessagesMock,
|
||||||
setStopGeneration: setShouldStopMock,
|
stopGeneration: setShouldStopMock,
|
||||||
value: {
|
value: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
stream: new Observable().subscribe(),
|
stream: new Observable().subscribe(),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
@ -52,16 +52,36 @@ export const GenAIButton = ({
|
|||||||
}: GenAIButtonProps) => {
|
}: GenAIButtonProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const { setMessages, setStopGeneration, reply, value, error, streamStatus } = useOpenAIStream(model, temperature);
|
|
||||||
|
|
||||||
const [history, setHistory] = useState<string[]>([]);
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const unshiftHistoryEntry = useCallback((historyEntry: string) => {
|
||||||
|
setHistory((h) => [historyEntry, ...h]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onResponse = useCallback(
|
||||||
|
(reply: string) => {
|
||||||
|
const sanitizedReply = sanitizeReply(reply);
|
||||||
|
onGenerate(sanitizedReply);
|
||||||
|
unshiftHistoryEntry(sanitizedReply);
|
||||||
|
},
|
||||||
|
[onGenerate, unshiftHistoryEntry]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { setMessages, stopGeneration, value, error, streamStatus } = useOpenAIStream({
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
onResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
const hasHistory = history.length > 0;
|
const hasHistory = history.length > 0;
|
||||||
const isGenerating = streamStatus === StreamStatus.GENERATING;
|
|
||||||
const isFirstHistoryEntry = !hasHistory;
|
const isFirstHistoryEntry = !hasHistory;
|
||||||
|
|
||||||
|
const isGenerating = streamStatus === StreamStatus.GENERATING;
|
||||||
const isButtonDisabled = disabled || (value && !value.enabled && !error);
|
const isButtonDisabled = disabled || (value && !value.enabled && !error);
|
||||||
const reportInteraction = (item: AutoGenerateItem) => reportAutoGenerateInteraction(eventTrackingSrc, item);
|
const reportInteraction = useCallback(
|
||||||
|
(item: AutoGenerateItem) => reportAutoGenerateInteraction(eventTrackingSrc, item),
|
||||||
|
[eventTrackingSrc]
|
||||||
|
);
|
||||||
|
|
||||||
const showTooltip = error || tooltip ? undefined : false;
|
const showTooltip = error || tooltip ? undefined : false;
|
||||||
const tooltipContent = error
|
const tooltipContent = error
|
||||||
@ -70,7 +90,7 @@ export const GenAIButton = ({
|
|||||||
|
|
||||||
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (streamStatus === StreamStatus.GENERATING) {
|
if (streamStatus === StreamStatus.GENERATING) {
|
||||||
setStopGeneration(true);
|
stopGeneration();
|
||||||
} else {
|
} else {
|
||||||
if (!hasHistory) {
|
if (!hasHistory) {
|
||||||
onClickProp?.(e);
|
onClickProp?.(e);
|
||||||
@ -90,28 +110,6 @@ export const GenAIButton = ({
|
|||||||
reportInteraction(buttonItem);
|
reportInteraction(buttonItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pushHistoryEntry = useCallback(
|
|
||||||
(historyEntry: string) => {
|
|
||||||
if (history.indexOf(historyEntry) === -1) {
|
|
||||||
setHistory([historyEntry, ...history]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[history]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Todo: Consider other options for `"` sanitation
|
|
||||||
if (streamStatus === StreamStatus.COMPLETED && reply) {
|
|
||||||
onGenerate(sanitizeReply(reply));
|
|
||||||
}
|
|
||||||
}, [streamStatus, reply, onGenerate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (streamStatus === StreamStatus.COMPLETED) {
|
|
||||||
pushHistoryEntry(sanitizeReply(reply));
|
|
||||||
}
|
|
||||||
}, [history, streamStatus, reply, pushHistoryEntry]);
|
|
||||||
|
|
||||||
// The button is disabled if the plugin is not installed or enabled
|
// The button is disabled if the plugin is not installed or enabled
|
||||||
if (!value?.enabled) {
|
if (!value?.enabled) {
|
||||||
return null;
|
return null;
|
||||||
@ -127,9 +125,11 @@ export const GenAIButton = ({
|
|||||||
if (isGenerating) {
|
if (isGenerating) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (error || (value && !value?.enabled)) {
|
|
||||||
|
if (error || (value && !value.enabled)) {
|
||||||
return 'exclamation-circle';
|
return 'exclamation-circle';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'ai';
|
return 'ai';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,12 +164,7 @@ export const GenAIButton = ({
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
const getMessages = () => {
|
const getMessages = () => (typeof messages === 'function' ? messages() : messages);
|
||||||
if (typeof messages === 'function') {
|
|
||||||
return messages();
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderButtonWithToggletip = () => {
|
const renderButtonWithToggletip = () => {
|
||||||
if (hasHistory) {
|
if (hasHistory) {
|
||||||
@ -183,7 +178,7 @@ export const GenAIButton = ({
|
|||||||
history={history}
|
history={history}
|
||||||
messages={getMessages()}
|
messages={getMessages()}
|
||||||
onApplySuggestion={onApplySuggestion}
|
onApplySuggestion={onApplySuggestion}
|
||||||
updateHistory={pushHistoryEntry}
|
updateHistory={unshiftHistoryEntry}
|
||||||
eventTrackingSrc={eventTrackingSrc}
|
eventTrackingSrc={eventTrackingSrc}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Alert, Button, Icon, Input, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
import { Alert, Button, Icon, Input, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
||||||
|
import { Trans } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { STOP_GENERATION_TEXT } from './GenAIButton';
|
import { STOP_GENERATION_TEXT } from './GenAIButton';
|
||||||
import { GenerationHistoryCarousel } from './GenerationHistoryCarousel';
|
import { GenerationHistoryCarousel } from './GenerationHistoryCarousel';
|
||||||
@ -32,55 +32,36 @@ export const GenAIHistory = ({
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(1);
|
const [currentIndex, setCurrentIndex] = useState(1);
|
||||||
const [showError, setShowError] = useState(false);
|
|
||||||
const [customFeedback, setCustomPrompt] = useState('');
|
const [customFeedback, setCustomPrompt] = useState('');
|
||||||
|
|
||||||
const { setMessages, setStopGeneration, reply, streamStatus, error } = useOpenAIStream(
|
const onResponse = useCallback(
|
||||||
DEFAULT_OAI_MODEL,
|
(response: string) => {
|
||||||
temperature
|
updateHistory(sanitizeReply(response));
|
||||||
|
},
|
||||||
|
[updateHistory]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isStreamGenerating = streamStatus === StreamStatus.GENERATING;
|
const { setMessages, stopGeneration, reply, streamStatus, error } = useOpenAIStream({
|
||||||
|
model: DEFAULT_OAI_MODEL,
|
||||||
|
temperature,
|
||||||
|
onResponse,
|
||||||
|
});
|
||||||
|
|
||||||
const reportInteraction = (item: AutoGenerateItem, otherMetadata?: object) =>
|
const reportInteraction = (item: AutoGenerateItem, otherMetadata?: object) =>
|
||||||
reportAutoGenerateInteraction(eventTrackingSrc, item, otherMetadata);
|
reportAutoGenerateInteraction(eventTrackingSrc, item, otherMetadata);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isStreamGenerating && reply !== '') {
|
|
||||||
setCurrentIndex(1);
|
|
||||||
}
|
|
||||||
}, [isStreamGenerating, reply]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (streamStatus === StreamStatus.COMPLETED) {
|
|
||||||
updateHistory(sanitizeReply(reply));
|
|
||||||
}
|
|
||||||
}, [streamStatus, reply, updateHistory]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
setShowError(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamStatus === StreamStatus.GENERATING) {
|
|
||||||
setShowError(false);
|
|
||||||
}
|
|
||||||
}, [error, streamStatus]);
|
|
||||||
|
|
||||||
const onSubmitCustomFeedback = (text: string) => {
|
const onSubmitCustomFeedback = (text: string) => {
|
||||||
onGenerateWithFeedback(text);
|
onGenerateWithFeedback(text);
|
||||||
reportInteraction(AutoGenerateItem.customFeedback, { customFeedback: text });
|
reportInteraction(AutoGenerateItem.customFeedback, { customFeedback: text });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onStopGeneration = () => {
|
||||||
|
stopGeneration();
|
||||||
|
reply && onResponse(reply);
|
||||||
|
};
|
||||||
|
|
||||||
const onApply = () => {
|
const onApply = () => {
|
||||||
if (isStreamGenerating) {
|
onApplySuggestion(history[currentIndex - 1]);
|
||||||
setStopGeneration(true);
|
|
||||||
if (reply !== '') {
|
|
||||||
updateHistory(sanitizeReply(reply));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onApplySuggestion(history[currentIndex - 1]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNavigate = (index: number) => {
|
const onNavigate = (index: number) => {
|
||||||
@ -88,14 +69,8 @@ export const GenAIHistory = ({
|
|||||||
reportInteraction(index > currentIndex ? AutoGenerateItem.backHistoryItem : AutoGenerateItem.forwardHistoryItem);
|
reportInteraction(index > currentIndex ? AutoGenerateItem.backHistoryItem : AutoGenerateItem.forwardHistoryItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onGenerateWithFeedback = (suggestion: string | QuickFeedbackType) => {
|
const onGenerateWithFeedback = (suggestion: string) => {
|
||||||
if (suggestion !== QuickFeedbackType.Regenerate) {
|
setMessages((messages) => [...messages, ...getFeedbackMessage(history[currentIndex - 1], suggestion)]);
|
||||||
messages = [...messages, ...getFeedbackMessage(history[currentIndex - 1], suggestion)];
|
|
||||||
} else {
|
|
||||||
messages = [...messages, ...getFeedbackMessage(history[currentIndex - 1], 'Please, regenerate')];
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages(messages);
|
|
||||||
|
|
||||||
if (suggestion in QuickFeedbackType) {
|
if (suggestion in QuickFeedbackType) {
|
||||||
reportInteraction(AutoGenerateItem.quickFeedback, { quickFeedbackItem: suggestion });
|
reportInteraction(AutoGenerateItem.quickFeedback, { quickFeedbackItem: suggestion });
|
||||||
@ -111,23 +86,24 @@ export const GenAIHistory = ({
|
|||||||
|
|
||||||
const onClickDocs = () => reportInteraction(AutoGenerateItem.linkToDocs);
|
const onClickDocs = () => reportInteraction(AutoGenerateItem.linkToDocs);
|
||||||
|
|
||||||
|
const isStreamGenerating = streamStatus === StreamStatus.GENERATING;
|
||||||
|
const showError = error && !isStreamGenerating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{showError && (
|
{showError && (
|
||||||
<Alert title="">
|
<Alert title="">
|
||||||
<Stack direction={'column'}>
|
<Stack direction="column">
|
||||||
<p>Sorry, I was unable to complete your request. Please try again.</p>
|
<p>
|
||||||
|
<Trans i18nKey="gen-ai.incomplete-request-error">
|
||||||
|
Sorry, I was unable to complete your request. Please try again.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<GenerationHistoryCarousel
|
<GenerationHistoryCarousel history={history} index={currentIndex} onNavigate={onNavigate} />
|
||||||
history={history}
|
|
||||||
index={currentIndex}
|
|
||||||
onNavigate={onNavigate}
|
|
||||||
reply={sanitizeReply(reply)}
|
|
||||||
streamStatus={streamStatus}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.actionButtons}>
|
<div className={styles.actionButtons}>
|
||||||
<QuickFeedback onSuggestionClick={onGenerateWithFeedback} isGenerating={isStreamGenerating} />
|
<QuickFeedback onSuggestionClick={onGenerateWithFeedback} isGenerating={isStreamGenerating} />
|
||||||
@ -142,9 +118,9 @@ export const GenAIHistory = ({
|
|||||||
fill="text"
|
fill="text"
|
||||||
aria-label="Send custom feedback"
|
aria-label="Send custom feedback"
|
||||||
onClick={onClickSubmitCustomFeedback}
|
onClick={onClickSubmitCustomFeedback}
|
||||||
disabled={customFeedback === ''}
|
disabled={!customFeedback}
|
||||||
>
|
>
|
||||||
Send
|
<Trans i18nKey="gen-ai.send-custom-feedback">Send</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
value={customFeedback}
|
value={customFeedback}
|
||||||
@ -153,10 +129,16 @@ export const GenAIHistory = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.applySuggestion}>
|
<div className={styles.applySuggestion}>
|
||||||
<Stack justifyContent={'flex-end'} direction={'row'}>
|
<Stack justifyContent="flex-end" direction="row">
|
||||||
<Button icon={!isStreamGenerating ? 'check' : 'fa fa-spinner'} onClick={onApply}>
|
{isStreamGenerating ? (
|
||||||
{isStreamGenerating ? STOP_GENERATION_TEXT : 'Apply'}
|
<Button icon="fa fa-spinner" onClick={onStopGeneration}>
|
||||||
</Button>
|
{STOP_GENERATION_TEXT}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button icon="check" onClick={onApply}>
|
||||||
|
<Trans i18nKey="gen-ai.apply-suggestion">Apply</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -4,39 +4,22 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { Text, useStyles2 } from '@grafana/ui';
|
import { Text, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { MinimalisticPagination } from './MinimalisticPagination';
|
import { MinimalisticPagination } from './MinimalisticPagination';
|
||||||
import { StreamStatus } from './hooks';
|
|
||||||
|
|
||||||
export interface GenerationHistoryCarouselProps {
|
export interface GenerationHistoryCarouselProps {
|
||||||
history: string[];
|
history: string[];
|
||||||
index: number;
|
index: number;
|
||||||
reply: string;
|
|
||||||
streamStatus: StreamStatus;
|
|
||||||
onNavigate: (index: number) => void;
|
onNavigate: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GenerationHistoryCarousel = ({
|
export const GenerationHistoryCarousel = ({ history, index, onNavigate }: GenerationHistoryCarouselProps) => {
|
||||||
history,
|
|
||||||
index,
|
|
||||||
reply,
|
|
||||||
streamStatus,
|
|
||||||
onNavigate,
|
|
||||||
}: GenerationHistoryCarouselProps) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const historySize = history.length;
|
const historySize = history.length;
|
||||||
|
|
||||||
const getHistoryText = () => {
|
|
||||||
if (reply && streamStatus !== StreamStatus.IDLE) {
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
return history[index - 1];
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.contentWrapper}>
|
<div className={styles.contentWrapper}>
|
||||||
<Text element="p" color="secondary">
|
<Text element="p" color="secondary">
|
||||||
{getHistoryText()}
|
{history[index - 1]}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<MinimalisticPagination
|
<MinimalisticPagination
|
||||||
|
@ -6,7 +6,7 @@ import { Button, useStyles2 } from '@grafana/ui';
|
|||||||
import { QuickFeedbackType } from './utils';
|
import { QuickFeedbackType } from './utils';
|
||||||
|
|
||||||
interface QuickActionsProps {
|
interface QuickActionsProps {
|
||||||
onSuggestionClick: (suggestion: QuickFeedbackType) => void;
|
onSuggestionClick: (suggestion: string) => void;
|
||||||
isGenerating: boolean;
|
isGenerating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export const QuickFeedback = ({ onSuggestionClick, isGenerating }: QuickActionsP
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={isGenerating}
|
disabled={isGenerating}
|
||||||
>
|
>
|
||||||
{QuickFeedbackType.Regenerate}
|
{'Regenerate'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@ -22,31 +22,35 @@ export enum StreamStatus {
|
|||||||
|
|
||||||
export const TIMEOUT = 10000;
|
export const TIMEOUT = 10000;
|
||||||
|
|
||||||
// TODO: Add tests
|
interface Options {
|
||||||
export function useOpenAIStream(
|
model: string;
|
||||||
model = DEFAULT_OAI_MODEL,
|
temperature: number;
|
||||||
temperature = 1
|
onResponse?: (response: string) => void;
|
||||||
): {
|
}
|
||||||
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
|
|
||||||
setStopGeneration: React.Dispatch<React.SetStateAction<boolean>>;
|
const defaultOptions = {
|
||||||
|
model: DEFAULT_OAI_MODEL,
|
||||||
|
temperature: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseOpenAIStreamResponse {
|
||||||
|
setMessages: Dispatch<SetStateAction<Message[]>>;
|
||||||
|
stopGeneration: () => void;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
reply: string;
|
reply: string;
|
||||||
streamStatus: StreamStatus;
|
streamStatus: StreamStatus;
|
||||||
error: Error | undefined;
|
error?: Error;
|
||||||
value:
|
value?: {
|
||||||
| {
|
enabled?: boolean | undefined;
|
||||||
enabled: boolean | undefined;
|
stream?: Subscription;
|
||||||
stream?: undefined;
|
};
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
enabled: boolean | undefined;
|
// TODO: Add tests
|
||||||
stream: Subscription;
|
export function useOpenAIStream({ model, temperature, onResponse }: Options = defaultOptions): UseOpenAIStreamResponse {
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
} {
|
|
||||||
// The messages array to send to the LLM, updated when the button is clicked.
|
// The messages array to send to the LLM, updated when the button is clicked.
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [stopGeneration, setStopGeneration] = useState(false);
|
|
||||||
// The latest reply from the LLM.
|
// The latest reply from the LLM.
|
||||||
const [reply, setReply] = useState('');
|
const [reply, setReply] = useState('');
|
||||||
const [streamStatus, setStreamStatus] = useState<StreamStatus>(StreamStatus.IDLE);
|
const [streamStatus, setStreamStatus] = useState<StreamStatus>(StreamStatus.IDLE);
|
||||||
@ -59,11 +63,10 @@ export function useOpenAIStream(
|
|||||||
(e: Error) => {
|
(e: Error) => {
|
||||||
setStreamStatus(StreamStatus.IDLE);
|
setStreamStatus(StreamStatus.IDLE);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setStopGeneration(false);
|
|
||||||
setError(e);
|
setError(e);
|
||||||
notifyError(
|
notifyError(
|
||||||
'Failed to generate content using OpenAI',
|
'Failed to generate content using OpenAI',
|
||||||
`Please try again or if the problem persists, contact your organization admin.`
|
'Please try again or if the problem persists, contact your organization admin.'
|
||||||
);
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
genAILogger.logError(e, { messages: JSON.stringify(messages), model, temperature: String(temperature) });
|
genAILogger.logError(e, { messages: JSON.stringify(messages), model, temperature: String(temperature) });
|
||||||
@ -117,11 +120,8 @@ export function useOpenAIStream(
|
|||||||
complete: () => {
|
complete: () => {
|
||||||
setReply(partialReply);
|
setReply(partialReply);
|
||||||
setStreamStatus(StreamStatus.COMPLETED);
|
setStreamStatus(StreamStatus.COMPLETED);
|
||||||
setTimeout(() => {
|
onResponse?.(partialReply);
|
||||||
setStreamStatus(StreamStatus.IDLE);
|
|
||||||
});
|
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setStopGeneration(false);
|
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -131,22 +131,17 @@ export function useOpenAIStream(
|
|||||||
// Unsubscribe from the stream when the component unmounts.
|
// Unsubscribe from the stream when the component unmounts.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (value?.stream) {
|
value?.stream?.unsubscribe();
|
||||||
value.stream.unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
// Unsubscribe from the stream when user stops the generation.
|
// Unsubscribe from the stream when user stops the generation.
|
||||||
useEffect(() => {
|
const stopGeneration = useCallback(() => {
|
||||||
if (stopGeneration) {
|
value?.stream?.unsubscribe();
|
||||||
value?.stream?.unsubscribe();
|
setStreamStatus(StreamStatus.IDLE);
|
||||||
setStreamStatus(StreamStatus.IDLE);
|
setError(undefined);
|
||||||
setStopGeneration(false);
|
setMessages([]);
|
||||||
setError(undefined);
|
}, [value]);
|
||||||
setMessages([]);
|
|
||||||
}
|
|
||||||
}, [stopGeneration, value?.stream]);
|
|
||||||
|
|
||||||
// If the stream is generating and we haven't received a reply, it times out.
|
// If the stream is generating and we haven't received a reply, it times out.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -156,8 +151,9 @@ export function useOpenAIStream(
|
|||||||
onError(new Error(`OpenAI stream timed out after ${TIMEOUT}ms`));
|
onError(new Error(`OpenAI stream timed out after ${TIMEOUT}ms`));
|
||||||
}, TIMEOUT);
|
}, TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
timeout && clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
}, [streamStatus, reply, onError]);
|
}, [streamStatus, reply, onError]);
|
||||||
|
|
||||||
@ -167,7 +163,7 @@ export function useOpenAIStream(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setMessages,
|
setMessages,
|
||||||
setStopGeneration,
|
stopGeneration,
|
||||||
messages,
|
messages,
|
||||||
reply,
|
reply,
|
||||||
streamStatus,
|
streamStatus,
|
||||||
|
@ -22,7 +22,7 @@ export type Message = llms.openai.Message;
|
|||||||
export enum QuickFeedbackType {
|
export enum QuickFeedbackType {
|
||||||
Shorter = 'Even shorter',
|
Shorter = 'Even shorter',
|
||||||
MoreDescriptive = 'More descriptive',
|
MoreDescriptive = 'More descriptive',
|
||||||
Regenerate = 'Regenerate',
|
Regenerate = 'Please, regenerate',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -922,6 +922,11 @@
|
|||||||
"folder-picker": {
|
"folder-picker": {
|
||||||
"loading": "Loading folders..."
|
"loading": "Loading folders..."
|
||||||
},
|
},
|
||||||
|
"gen-ai": {
|
||||||
|
"apply-suggestion": "Apply",
|
||||||
|
"incomplete-request-error": "Sorry, I was unable to complete your request. Please try again.",
|
||||||
|
"send-custom-feedback": "Send"
|
||||||
|
},
|
||||||
"grafana-ui": {
|
"grafana-ui": {
|
||||||
"drawer": {
|
"drawer": {
|
||||||
"close": "Close"
|
"close": "Close"
|
||||||
|
@ -922,6 +922,11 @@
|
|||||||
"folder-picker": {
|
"folder-picker": {
|
||||||
"loading": "Ŀőäđįʼnģ ƒőľđęřş..."
|
"loading": "Ŀőäđįʼnģ ƒőľđęřş..."
|
||||||
},
|
},
|
||||||
|
"gen-ai": {
|
||||||
|
"apply-suggestion": "Åppľy",
|
||||||
|
"incomplete-request-error": "Ŝőřřy, Ĩ ŵäş ūʼnäþľę ŧő čőmpľęŧę yőūř řęqūęşŧ. Pľęäşę ŧřy äģäįʼn.",
|
||||||
|
"send-custom-feedback": "Ŝęʼnđ"
|
||||||
|
},
|
||||||
"grafana-ui": {
|
"grafana-ui": {
|
||||||
"drawer": {
|
"drawer": {
|
||||||
"close": "Cľőşę"
|
"close": "Cľőşę"
|
||||||
|
Loading…
Reference in New Issue
Block a user