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:
kay delaney 2024-09-04 16:56:20 +01:00 committed by GitHub
parent 0a337ff3b3
commit d8b6a5bde2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 147 additions and 175 deletions

View File

@ -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"]

View File

@ -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(),

View File

@ -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}
/> />
} }

View File

@ -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>

View File

@ -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

View File

@ -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>
); );

View File

@ -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,

View File

@ -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',
} }
/** /**

View File

@ -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"

View File

@ -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ľőşę"