diff --git a/packages/trace-viewer/src/ui/errorsTab.tsx b/packages/trace-viewer/src/ui/errorsTab.tsx index ce914f795c..c047cfed5c 100644 --- a/packages/trace-viewer/src/ui/errorsTab.tsx +++ b/packages/trace-viewer/src/ui/errorsTab.tsx @@ -27,7 +27,7 @@ import { fixTestPrompt } from '@web/components/prompts'; import type { GitCommitInfo } from '@testIsomorphic/types'; import { AIConversation } from './aiConversation'; import { ToolbarButton } from '@web/components/toolbarButton'; -import { useLLMChat, useLLMConversation } from './llm'; +import { useIsLLMAvailable, useLLMChat, useLLMConversation } from './llm'; import { useAsyncMemo } from '@web/uiUtils'; const GitCommitInfoContext = React.createContext(undefined); @@ -99,7 +99,7 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined): function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSource }: { message: string, error: ErrorDescription, errorId: string, sdkLanguage: Language, pageSnapshot?: string, revealInSource: (error: ErrorDescription) => void }) { const [showLLM, setShowLLM] = React.useState(false); - const llmAvailable = !!useLLMChat(); + const llmAvailable = useIsLLMAvailable(); const gitCommitInfo = useGitCommitInfo(); const diff = gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff']; @@ -127,7 +127,7 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou } {llmAvailable - ? setShowLLM(v => !v)} style={{ width: '96px', justifyContent: 'center' }} title='Fix with AI' className='copy-to-clipboard-text-button'>{showLLM ? 'Hide AI' : 'Fix with AI'} + ? : } @@ -138,6 +138,29 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou ; } +function FixWithAIButton({ conversationId, value, onChange }: { conversationId: string, value: boolean, onChange: React.Dispatch> }) { + const chat = useLLMChat(); + + return { + if (!chat.getConversation(conversationId)) { + chat.startConversation(conversationId, [ + `My Playwright test failed. What's going wrong?`, + `Please give me a suggestion how to fix it, and then explain what went wrong. Be very concise and apply Playwright best practices.`, + `Don't include many headings in your output. Make sure what you're saying is correct, and take into account whether there might be a bug in the app.` + ].join('\n')); + } + + onChange(v => !v); + }} + style={{ width: '96px', justifyContent: 'center' }} + title='Fix with AI' + className='copy-to-clipboard-text-button' + > + {value ? 'Hide AI' : 'Fix with AI'} + ; +} + export const ErrorsTab: React.FunctionComponent<{ errorsModel: ErrorsTabModel, actions: modelUtil.ActionTraceEventInContext[], @@ -159,14 +182,7 @@ export const ErrorsTab: React.FunctionComponent<{ }; export function AIErrorConversation({ conversationId, error, pageSnapshot, diff }: { conversationId: string, error: string, pageSnapshot?: string, diff?: string }) { - const [history, conversation] = useLLMConversation( - conversationId, - [ - `My Playwright test failed. What's going wrong?`, - `Please give me a suggestion how to fix it, and then explain what went wrong. Be very concise and apply Playwright best practices.`, - `Don't include many headings in your output. Make sure what you're saying is correct, and take into account whether there might be a bug in the app.` - ].join('\n') - ); + const [history, conversation] = useLLMConversation(conversationId); React.useEffect(() => { let content = `Here's the error: ${error}`; diff --git a/packages/trace-viewer/src/ui/llm.tsx b/packages/trace-viewer/src/ui/llm.tsx index 333cb265d8..162c33fd4c 100644 --- a/packages/trace-viewer/src/ui/llm.tsx +++ b/packages/trace-viewer/src/ui/llm.tsx @@ -170,12 +170,14 @@ class LLMChat { constructor(readonly api: LLM) {} - getConversation(id: string, systemPrompt: string) { - if (!this.conversations.has(id)) { - const conversation = new Conversation(this, systemPrompt); - this.conversations.set(id, conversation); // TODO: cleanup - } - return this.conversations.get(id)!; + getConversation(id: string) { + return this.conversations.get(id); + } + + startConversation(id: string, systemPrompt: string) { + const conversation = new Conversation(this, systemPrompt); + this.conversations.set(id, conversation); // TODO: cleanup + return conversation; } } @@ -237,18 +239,25 @@ export function LLMProvider({ children }: React.PropsWithChildren<{}>) { } export function useLLMChat() { - return React.useContext(llmContext); -} - -export function useLLMConversation(id: string, systemPrompt: string) { - const chat = useLLMChat(); + const chat = React.useContext(llmContext); if (!chat) throw new Error('No LLM chat available, make sure theres a LLMProvider above'); - const conversation = React.useMemo(() => chat.getConversation(id, systemPrompt), [chat, id]); // eslint-disable-line react-hooks/exhaustive-deps + return chat; +} + +export function useIsLLMAvailable() { + return !!React.useContext(llmContext); +} + +export function useLLMConversation(id: string) { + const chat = useLLMChat(); + const conversation = React.useMemo(() => chat.getConversation(id), [chat, id]); + if (!conversation) + throw new Error('No conversation found for id: ' + id); const [history, setHistory] = React.useState(conversation.history); React.useEffect(() => { function update() { - setHistory([...conversation.history]); + setHistory([...conversation!.history]); } update(); const subscription = conversation.onChange.event(update);