mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 04:00:55 +08:00

* i18n: removes useTranslate hook * chore: fix duplicate imports * chore: fix import sorting and hook dependencies
209 lines
6.4 KiB
TypeScript
209 lines
6.4 KiB
TypeScript
import { css } from '@emotion/css';
|
|
import { useState, useCallback } from 'react';
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
import { Trans, t } from '@grafana/i18n';
|
|
import { Alert, Button, Icon, Input, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
|
|
|
|
import { STOP_GENERATION_TEXT } from './GenAIButton';
|
|
import { GenerationHistoryCarousel } from './GenerationHistoryCarousel';
|
|
import { QuickFeedback } from './QuickFeedback';
|
|
import { StreamStatus, useLLMStream } from './hooks';
|
|
import { AutoGenerateItem, EventTrackingSrc, reportAutoGenerateInteraction } from './tracking';
|
|
import { getFeedbackMessage, Message, DEFAULT_LLM_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 [customFeedback, setCustomPrompt] = useState('');
|
|
|
|
const onResponse = useCallback(
|
|
(response: string) => {
|
|
updateHistory(sanitizeReply(response));
|
|
},
|
|
[updateHistory]
|
|
);
|
|
|
|
const { setMessages, stopGeneration, reply, streamStatus, error } = useLLMStream({
|
|
model: DEFAULT_LLM_MODEL,
|
|
temperature,
|
|
onResponse,
|
|
});
|
|
|
|
const reportInteraction = (item: AutoGenerateItem, otherMetadata?: object) =>
|
|
reportAutoGenerateInteraction(eventTrackingSrc, item, otherMetadata);
|
|
|
|
const onSubmitCustomFeedback = (text: string) => {
|
|
onGenerateWithFeedback(text);
|
|
reportInteraction(AutoGenerateItem.customFeedback, { customFeedback: text });
|
|
};
|
|
|
|
const onStopGeneration = () => {
|
|
stopGeneration();
|
|
reply && onResponse(reply);
|
|
};
|
|
|
|
const onApply = () => {
|
|
onApplySuggestion(history[currentIndex - 1]);
|
|
};
|
|
|
|
const onNavigate = (index: number) => {
|
|
setCurrentIndex(index);
|
|
reportInteraction(index > currentIndex ? AutoGenerateItem.backHistoryItem : AutoGenerateItem.forwardHistoryItem);
|
|
};
|
|
|
|
const onGenerateWithFeedback = (suggestion: string) => {
|
|
setMessages((messages) => [...messages, ...getFeedbackMessage(history[currentIndex - 1], suggestion)]);
|
|
|
|
if (suggestion in QuickFeedbackType) {
|
|
reportInteraction(AutoGenerateItem.quickFeedback, { quickFeedbackItem: suggestion });
|
|
}
|
|
};
|
|
|
|
const onKeyDownCustomFeedbackInput = (e: React.KeyboardEvent<HTMLInputElement>) =>
|
|
e.key === 'Enter' && onSubmitCustomFeedback(customFeedback);
|
|
|
|
const onChangeCustomFeedback = (e: React.FormEvent<HTMLInputElement>) => setCustomPrompt(e.currentTarget.value);
|
|
|
|
const onClickSubmitCustomFeedback = () => onSubmitCustomFeedback(customFeedback);
|
|
|
|
const onClickDocs = () => reportInteraction(AutoGenerateItem.linkToDocs);
|
|
|
|
const isStreamGenerating = streamStatus === StreamStatus.GENERATING;
|
|
const showError = error && !isStreamGenerating;
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
{showError && (
|
|
<Alert title="">
|
|
<Stack direction="column">
|
|
<p>
|
|
<Trans i18nKey="gen-ai.incomplete-request-error">
|
|
Sorry, I was unable to complete your request. Please try again.
|
|
</Trans>
|
|
</p>
|
|
</Stack>
|
|
</Alert>
|
|
)}
|
|
|
|
<GenerationHistoryCarousel history={history} index={currentIndex} onNavigate={onNavigate} />
|
|
|
|
<div className={styles.actionButtons}>
|
|
<QuickFeedback onSuggestionClick={onGenerateWithFeedback} isGenerating={isStreamGenerating} />
|
|
</div>
|
|
|
|
<Input
|
|
placeholder={t('dashboard.gen-aihistory.placeholder-tell-ai-what-to-do-next', 'Tell AI what to do next...')}
|
|
suffix={
|
|
<Button
|
|
icon="enter"
|
|
variant="secondary"
|
|
fill="text"
|
|
aria-label={t('dashboard.gen-aihistory.aria-label-send-custom-feedback', 'Send custom feedback')}
|
|
onClick={onClickSubmitCustomFeedback}
|
|
disabled={!customFeedback}
|
|
>
|
|
<Trans i18nKey="gen-ai.send-custom-feedback">Send</Trans>
|
|
</Button>
|
|
}
|
|
value={customFeedback}
|
|
onChange={onChangeCustomFeedback}
|
|
onKeyDown={onKeyDownCustomFeedbackInput}
|
|
/>
|
|
|
|
<div className={styles.applySuggestion}>
|
|
<Stack justifyContent="flex-end" direction="row">
|
|
{isStreamGenerating ? (
|
|
<Button icon="fa fa-spinner" onClick={onStopGeneration}>
|
|
{STOP_GENERATION_TEXT}
|
|
</Button>
|
|
) : (
|
|
<Button icon="check" onClick={onApply}>
|
|
<Trans i18nKey="gen-ai.apply-suggestion">Apply</Trans>
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</div>
|
|
|
|
<div className={styles.footer}>
|
|
<Icon name="exclamation-circle" className={styles.infoColor} />
|
|
<Text variant="bodySmall" color="secondary">
|
|
<Trans i18nKey="dashboard.gen-aihistory.footer-text">
|
|
This content is AI-generated using the{' '}
|
|
<TextLink
|
|
variant="bodySmall"
|
|
inline={true}
|
|
href="https://grafana.com/docs/grafana-cloud/alerting-and-irm/machine-learning/llm-plugin/"
|
|
external
|
|
onClick={onClickDocs}
|
|
>
|
|
Grafana LLM plugin
|
|
</TextLink>
|
|
</Trans>
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
container: css({
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
width: 520,
|
|
maxHeight: 350,
|
|
// This is the space the footer height
|
|
paddingBottom: 25,
|
|
}),
|
|
applySuggestion: css({
|
|
paddingTop: theme.spacing(2),
|
|
}),
|
|
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,
|
|
}),
|
|
actionButtons: css({
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
padding: '24px 0 8px 0',
|
|
}),
|
|
});
|