mirror of
https://github.com/grafana/grafana.git
synced 2026-08-12 14:15:00 -05:00
217 lines
5.7 KiB
TypeScript
217 lines
5.7 KiB
TypeScript
import { css } from '@emotion/css';
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
import { Button, Spinner, useStyles2, Tooltip, Toggletip, Text } from '@grafana/ui';
|
|
|
|
import { GenAIHistory } from './GenAIHistory';
|
|
import { StreamStatus, useOpenAIStream } from './hooks';
|
|
import { AutoGenerateItem, EventTrackingSrc, reportAutoGenerateInteraction } from './tracking';
|
|
import { OAI_MODEL, DEFAULT_OAI_MODEL, Message, sanitizeReply } from './utils';
|
|
|
|
export interface GenAIButtonProps {
|
|
// Button label text
|
|
text?: string;
|
|
toggleTipTitle?: string;
|
|
// Button click handler
|
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
// Messages to send to the LLM plugin
|
|
messages: Message[] | (() => Message[]);
|
|
// Callback function that the LLM plugin streams responses to
|
|
onGenerate: (response: string) => void;
|
|
// Temperature for the LLM plugin. Default is 1.
|
|
// Closer to 0 means more conservative, closer to 1 means more creative.
|
|
temperature?: number;
|
|
model?: OAI_MODEL;
|
|
// Event tracking source. Send as `src` to Rudderstack event
|
|
eventTrackingSrc: EventTrackingSrc;
|
|
// Whether the button should be disabled
|
|
disabled?: boolean;
|
|
}
|
|
export const STOP_GENERATION_TEXT = 'Stop generating';
|
|
|
|
export const GenAIButton = ({
|
|
text = 'Auto-generate',
|
|
toggleTipTitle = '',
|
|
onClick: onClickProp,
|
|
model = DEFAULT_OAI_MODEL,
|
|
messages,
|
|
onGenerate,
|
|
temperature = 1,
|
|
eventTrackingSrc,
|
|
disabled,
|
|
}: GenAIButtonProps) => {
|
|
const styles = useStyles2(getStyles);
|
|
|
|
const {
|
|
messages: streamMessages,
|
|
setMessages,
|
|
setStopGeneration,
|
|
reply,
|
|
value,
|
|
error,
|
|
streamStatus,
|
|
} = useOpenAIStream(model, temperature);
|
|
|
|
const [history, setHistory] = useState<string[]>([]);
|
|
const [showHistory, setShowHistory] = useState(true);
|
|
|
|
const hasHistory = history.length > 0;
|
|
const isFirstHistoryEntry = streamStatus === StreamStatus.GENERATING && !hasHistory;
|
|
const isButtonDisabled = disabled || (value && !value.enabled && !error);
|
|
const reportInteraction = (item: AutoGenerateItem) => reportAutoGenerateInteraction(eventTrackingSrc, item);
|
|
|
|
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
if (streamStatus === StreamStatus.GENERATING) {
|
|
setStopGeneration(true);
|
|
} else {
|
|
if (!hasHistory) {
|
|
onClickProp?.(e);
|
|
setMessages(typeof messages === 'function' ? messages() : messages);
|
|
} else {
|
|
if (setShowHistory) {
|
|
setShowHistory(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
const buttonItem = error
|
|
? AutoGenerateItem.erroredRetryButton
|
|
: isFirstHistoryEntry
|
|
? AutoGenerateItem.stopGenerationButton
|
|
: hasHistory
|
|
? AutoGenerateItem.improveButton
|
|
: AutoGenerateItem.autoGenerateButton;
|
|
reportInteraction(buttonItem);
|
|
};
|
|
|
|
const pushHistoryEntry = useCallback(
|
|
(historyEntry: string) => {
|
|
if (history.indexOf(historyEntry) === -1) {
|
|
setHistory([historyEntry, ...history]);
|
|
}
|
|
},
|
|
[history]
|
|
);
|
|
|
|
useEffect(() => {
|
|
// Todo: Consider other options for `"` sanitation
|
|
if (isFirstHistoryEntry && reply) {
|
|
onGenerate(sanitizeReply(reply));
|
|
}
|
|
}, [streamStatus, reply, onGenerate, isFirstHistoryEntry]);
|
|
|
|
useEffect(() => {
|
|
if (streamStatus === StreamStatus.COMPLETED) {
|
|
pushHistoryEntry(sanitizeReply(reply));
|
|
}
|
|
}, [history, streamStatus, reply, pushHistoryEntry]);
|
|
|
|
// The button is disabled if the plugin is not installed or enabled
|
|
if (!value?.enabled) {
|
|
return null;
|
|
}
|
|
|
|
const onApplySuggestion = (suggestion: string) => {
|
|
reportInteraction(AutoGenerateItem.applySuggestion);
|
|
onGenerate(suggestion);
|
|
setShowHistory(false);
|
|
};
|
|
|
|
const getIcon = () => {
|
|
if (isFirstHistoryEntry) {
|
|
return undefined;
|
|
}
|
|
if (error || (value && !value?.enabled)) {
|
|
return 'exclamation-circle';
|
|
}
|
|
return 'ai';
|
|
};
|
|
|
|
const getText = () => {
|
|
let buttonText = text;
|
|
|
|
if (error) {
|
|
buttonText = 'Retry';
|
|
}
|
|
|
|
if (isFirstHistoryEntry) {
|
|
buttonText = STOP_GENERATION_TEXT;
|
|
}
|
|
|
|
if (hasHistory) {
|
|
buttonText = 'Improve';
|
|
}
|
|
|
|
return buttonText;
|
|
};
|
|
|
|
const button = (
|
|
<Button
|
|
icon={getIcon()}
|
|
onClick={onClick}
|
|
fill="text"
|
|
size="sm"
|
|
disabled={isButtonDisabled}
|
|
variant={error ? 'destructive' : 'primary'}
|
|
>
|
|
{getText()}
|
|
</Button>
|
|
);
|
|
|
|
const renderButtonWithToggletip = () => {
|
|
if (hasHistory) {
|
|
const title = <Text element="p">{toggleTipTitle}</Text>;
|
|
|
|
return (
|
|
<Toggletip
|
|
title={title}
|
|
content={
|
|
<GenAIHistory
|
|
history={history}
|
|
messages={streamMessages}
|
|
onApplySuggestion={onApplySuggestion}
|
|
updateHistory={pushHistoryEntry}
|
|
eventTrackingSrc={eventTrackingSrc}
|
|
/>
|
|
}
|
|
placement="bottom-start"
|
|
fitContent={true}
|
|
show={showHistory ? undefined : false}
|
|
>
|
|
{button}
|
|
</Toggletip>
|
|
);
|
|
}
|
|
|
|
return button;
|
|
};
|
|
|
|
return (
|
|
<div className={styles.wrapper}>
|
|
{isFirstHistoryEntry && <Spinner size="sm" className={styles.spinner} />}
|
|
{!hasHistory && (
|
|
<Tooltip
|
|
show={error ? undefined : false}
|
|
interactive
|
|
content={
|
|
'Failed to generate content using OpenAI. Please try again or if the problem persist, contact your organization admin.'
|
|
}
|
|
>
|
|
{button}
|
|
</Tooltip>
|
|
)}
|
|
{hasHistory && renderButtonWithToggletip()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
wrapper: css({
|
|
display: 'flex',
|
|
}),
|
|
spinner: css({
|
|
color: theme.colors.text.link,
|
|
}),
|
|
});
|