2023-09-02 05:12:05 +02:00
/ * *
* Copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
import { ErrorMessage } from '@web/components/errorMessage' ;
import * as React from 'react' ;
import type * as modelUtil from './modelUtil' ;
import { PlaceholderPanel } from './placeholderPanel' ;
import { renderAction } from './actionList' ;
import type { Language } from '@isomorphic/locatorGenerators' ;
2023-11-16 20:37:57 +01:00
import type { StackFrame } from '@protocol/channels' ;
2025-02-10 17:47:27 +01:00
import { CopyToClipboardTextButton } from './copyToClipboard' ;
import { attachmentURL } from './attachmentsTab' ;
import { fixTestPrompt } from '@web/components/prompts' ;
import type { GitCommitInfo } from '@testIsomorphic/types' ;
2025-02-12 14:29:08 +01:00
import { AIConversation } from './aiConversation' ;
import { ToolbarButton } from '@web/components/toolbarButton' ;
2025-02-13 12:11:03 +01:00
import { useLLMChat , useLLMConversation } from './llm' ;
2025-02-12 14:29:08 +01:00
import { useAsyncMemo } from '@web/uiUtils' ;
2025-02-10 17:47:27 +01:00
const GitCommitInfoContext = React . createContext < GitCommitInfo | undefined > ( undefined ) ;
export function GitCommitInfoProvider ( { children , gitCommitInfo } : React . PropsWithChildren < { gitCommitInfo : GitCommitInfo } > ) {
return < GitCommitInfoContext.Provider value = { gitCommitInfo } > { children } < / GitCommitInfoContext.Provider > ;
}
export function useGitCommitInfo() {
return React . useContext ( GitCommitInfoContext ) ;
}
2025-02-12 14:29:08 +01:00
function usePageSnapshot ( actions : modelUtil.ActionTraceEventInContext [ ] ) {
return useAsyncMemo < string | undefined > ( async ( ) = > {
2025-02-10 17:47:27 +01:00
for ( const action of actions ) {
for ( const attachment of action . attachments ? ? [ ] ) {
if ( attachment . name === 'pageSnapshot' ) {
2025-02-12 14:29:08 +01:00
const response = await fetch ( attachmentURL ( { . . . attachment , traceUrl : action.context.traceUrl } ) ) ;
return await response . text ( ) ;
2025-02-10 17:47:27 +01:00
}
}
}
2025-02-12 14:29:08 +01:00
} , [ actions ] , undefined ) ;
}
2025-02-10 17:47:27 +01:00
2025-02-12 14:29:08 +01:00
const CopyPromptButton : React.FC < {
error : string ;
pageSnapshot? : string ;
diff? : string ;
} > = ( { error , pageSnapshot , diff } ) = > {
2025-02-10 17:47:27 +01:00
const prompt = React . useMemo (
( ) = > fixTestPrompt (
error ,
2025-02-12 14:29:08 +01:00
diff ,
2025-02-10 17:47:27 +01:00
pageSnapshot
) ,
2025-02-12 14:29:08 +01:00
[ error , diff , pageSnapshot ]
2025-02-10 17:47:27 +01:00
) ;
return (
< CopyToClipboardTextButton
value = { prompt }
description = 'Fix with AI'
copiedDescription = { < > Copied < span className = 'codicon codicon-copy' style = { { marginLeft : '5px' } } / > < / > }
style = { { width : '90px' , justifyContent : 'center' } }
/ >
) ;
} ;
2023-11-16 20:37:57 +01:00
2024-09-17 02:33:52 +02:00
export type ErrorDescription = {
2023-11-16 20:37:57 +01:00
action? : modelUtil.ActionTraceEventInContext ;
stack? : StackFrame [ ] ;
} ;
2023-09-02 05:12:05 +02:00
type ErrorsTabModel = {
2023-11-16 20:37:57 +01:00
errors : Map < string , ErrorDescription > ;
2023-09-02 05:12:05 +02:00
} ;
export function useErrorsTabModel ( model : modelUtil.MultiTraceModel | undefined ) : ErrorsTabModel {
return React . useMemo ( ( ) = > {
2023-11-16 20:37:57 +01:00
if ( ! model )
return { errors : new Map ( ) } ;
2023-12-22 23:19:53 +01:00
const errors = new Map < string , ErrorDescription > ( ) ;
for ( const error of model . errorDescriptors )
errors . set ( error . message , error ) ;
return { errors } ;
2023-09-02 05:12:05 +02:00
} , [ model ] ) ;
}
2025-02-13 12:14:17 +01:00
function Error ( { message , error , errorId , sdkLanguage , pageSnapshot , revealInSource } : { message : string , error : ErrorDescription , errorId : string , sdkLanguage : Language , pageSnapshot? : string , revealInSource : ( error : ErrorDescription ) = > void } ) {
2025-02-13 10:44:44 +01:00
const [ showLLM , setShowLLM ] = React . useState ( false ) ;
const llmAvailable = ! ! useLLMChat ( ) ;
const gitCommitInfo = useGitCommitInfo ( ) ;
const diff = gitCommitInfo ? . [ 'pull.diff' ] ? ? gitCommitInfo ? . [ 'revision.diff' ] ;
let location : string | undefined ;
let longLocation : string | undefined ;
const stackFrame = error . stack ? . [ 0 ] ;
if ( stackFrame ) {
const file = stackFrame . file . replace ( /.*[/\\](.*)/ , '$1' ) ;
location = file + ':' + stackFrame . line ;
longLocation = stackFrame . file + ':' + stackFrame . line ;
}
return < div style = { { display : 'flex' , flexDirection : 'column' } } >
< div className = 'hbox' style = { {
alignItems : 'center' ,
padding : '5px 10px' ,
minHeight : 36 ,
fontWeight : 'bold' ,
color : 'var(--vscode-errorForeground)' ,
flex : 0 ,
} } >
{ error . action && renderAction ( error . action , { sdkLanguage } ) }
{ location && < div className = 'action-location' >
@ < span title = { longLocation } onClick = { ( ) = > revealInSource ( error ) } > { location } < / span >
< / div > }
< span style = { { position : 'absolute' , right : '5px' } } >
{ llmAvailable
2025-02-13 10:58:10 +01:00
? < ToolbarButton onClick = { ( ) = > setShowLLM ( v = > ! v ) } style = { { width : "96px" , justifyContent : 'center' } } title = "Fix with AI" className = 'copy-to-clipboard-text-button' > { showLLM ? 'Hide AI' : 'Fix with AI' } < / ToolbarButton >
2025-02-13 10:44:44 +01:00
: < CopyPromptButton error = { message } pageSnapshot = { pageSnapshot } diff = { diff } / > }
< / span >
< / div >
< ErrorMessage error = { message } / >
{ showLLM && < AIErrorConversation error = { message } pageSnapshot = { pageSnapshot } conversationId = { errorId } diff = { diff } / > }
< / div > ;
}
2023-09-02 05:12:05 +02:00
export const ErrorsTab : React.FunctionComponent < {
errorsModel : ErrorsTabModel ,
2025-02-10 17:47:27 +01:00
actions : modelUtil.ActionTraceEventInContext [ ] ,
2025-02-12 14:29:08 +01:00
wallTime : number ,
2023-09-02 05:12:05 +02:00
sdkLanguage : Language ,
2024-02-01 17:23:07 +01:00
revealInSource : ( error : ErrorDescription ) = > void ,
2025-02-12 14:29:08 +01:00
} > = ( { errorsModel , sdkLanguage , revealInSource , actions , wallTime } ) = > {
const pageSnapshot = usePageSnapshot ( actions ) ;
2025-02-13 10:44:44 +01:00
2023-09-02 05:12:05 +02:00
if ( ! errorsModel . errors . size )
return < PlaceholderPanel text = 'No errors' / > ;
2023-10-05 23:59:59 +02:00
return < div className = 'fill' style = { { overflow : 'auto' } } >
2025-02-13 12:14:17 +01:00
{ [ . . . errorsModel . errors . entries ( ) ] . map ( ( [ message , error ] ) = > {
const errorId = ` error- ${ wallTime } - ${ message } ` ;
return < Error key = { errorId } errorId = { errorId } message = { message } error = { error } revealInSource = { revealInSource } sdkLanguage = { sdkLanguage } pageSnapshot = { pageSnapshot } / > ;
} ) }
2023-09-02 05:12:05 +02:00
< / div > ;
} ;
2025-02-12 14:29:08 +01:00
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' )
) ;
2025-02-13 12:11:03 +01:00
React . useEffect ( ( ) = > {
let content = ` Here's the error: ${ error } ` ;
let displayContent = ` Help me with the error above. ` ;
2025-02-12 14:29:08 +01:00
if ( diff )
2025-02-13 12:11:03 +01:00
content += ` \ n \ nCode diff: \ n ${ diff } ` ;
2025-02-12 14:29:08 +01:00
if ( pageSnapshot )
2025-02-13 12:11:03 +01:00
content += ` \ n \ nPage snapshot: \ n ${ pageSnapshot } ` ;
2025-02-12 14:29:08 +01:00
if ( diff )
2025-02-13 12:11:03 +01:00
displayContent += ` Take the code diff ${ pageSnapshot ? ' and page snapshot' : '' } into account. ` ;
2025-02-12 14:29:08 +01:00
else if ( pageSnapshot )
2025-02-13 12:11:03 +01:00
displayContent += ` Take the page snapshot into account. ` ;
2025-02-12 14:29:08 +01:00
2025-02-13 12:11:03 +01:00
conversation . send ( content , displayContent ) ;
2025-02-13 12:14:17 +01:00
} , [ conversation ] ) ;
2025-02-12 14:29:08 +01:00
2025-02-13 12:11:03 +01:00
return < AIConversation history = { history } conversation = { conversation } / > ;
2025-02-12 14:29:08 +01:00
}