import { css } from '@emotion/css'; import React, { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, Button, HorizontalGroup, Icon, IconButton, Input, Text, TextLink, useStyles2, VerticalGroup, } from '@grafana/ui'; import { STOP_GENERATION_TEXT } from './GenAIButton'; import { GenerationHistoryCarousel } from './GenerationHistoryCarousel'; import { QuickFeedback } from './QuickFeedback'; import { StreamStatus, useOpenAIStream } from './hooks'; import { AutoGenerateItem, EventTrackingSrc, reportAutoGenerateInteraction } from './tracking'; import { getFeedbackMessage, Message, DEFAULT_OAI_MODEL, QuickFeedbackType, sanitizeReply } from './utils'; export interface GenAIHistoryProps { history: string[]; messages: Message[]; onApplySuggestion: (suggestion: string) => void; updateHistory: (historyEntry: string) => void; eventTrackingSrc: EventTrackingSrc; } const temperature = 0.5; export const GenAIHistory = ({ eventTrackingSrc, history, messages, onApplySuggestion, updateHistory, }: GenAIHistoryProps) => { const styles = useStyles2(getStyles); const [currentIndex, setCurrentIndex] = useState(1); const [showError, setShowError] = useState(false); const [customFeedback, setCustomPrompt] = useState(''); const { setMessages, setStopGeneration, reply, streamStatus, error } = useOpenAIStream( DEFAULT_OAI_MODEL, temperature ); const isStreamGenerating = streamStatus === StreamStatus.GENERATING; const reportInteraction = (item: AutoGenerateItem, otherMetadata?: object) => 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) => { onGenerateWithFeedback(text); reportInteraction(AutoGenerateItem.customFeedback, { customFeedback: text }); }; const onApply = () => { if (isStreamGenerating) { setStopGeneration(true); if (reply !== '') { updateHistory(sanitizeReply(reply)); } } else { onApplySuggestion(history[currentIndex - 1]); } }; const onNavigate = (index: number) => { setCurrentIndex(index); reportInteraction(index > currentIndex ? AutoGenerateItem.backHistoryItem : AutoGenerateItem.forwardHistoryItem); }; const onGenerateWithFeedback = (suggestion: string | QuickFeedbackType) => { if (suggestion !== QuickFeedbackType.Regenerate) { messages = [...messages, ...getFeedbackMessage(history[currentIndex], suggestion)]; } setMessages(messages); if (suggestion in QuickFeedbackType) { reportInteraction(AutoGenerateItem.quickFeedback, { quickFeedbackItem: suggestion }); } }; const onKeyDownCustomFeedbackInput = (e: React.KeyboardEvent) => e.key === 'Enter' && onSubmitCustomFeedback(customFeedback); const onChangeCustomFeedback = (e: React.FormEvent) => setCustomPrompt(e.currentTarget.value); const onClickSubmitCustomFeedback = () => onSubmitCustomFeedback(customFeedback); const onClickDocs = () => reportInteraction(AutoGenerateItem.linkToDocs); return (
{showError && (
Sorry, I was unable to complete your request. Please try again.
)} } value={customFeedback} onChange={onChangeCustomFeedback} onKeyDown={onKeyDownCustomFeedbackInput} />
This content is AI-generated using the{' '} Grafana LLM plugin
); }; const getStyles = (theme: GrafanaTheme2) => ({ container: css({ display: 'flex', flexDirection: 'column', width: 520, // This is the space the footer height paddingBottom: 35, }), applySuggestion: css({ marginTop: theme.spacing(1), }), actions: css({ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', }), footer: css({ // Absolute positioned since Toggletip doesn't support footer position: 'absolute', bottom: 0, left: 0, width: '100%', display: 'flex', flexDirection: 'row', margin: 0, padding: theme.spacing(1), paddingLeft: theme.spacing(2), alignItems: 'center', gap: theme.spacing(1), borderTop: `1px solid ${theme.colors.border.weak}`, marginTop: theme.spacing(2), }), infoColor: css({ color: theme.colors.info.main, }), });