This commit is contained in:
Simon Knott 2025-02-13 14:29:12 +01:00
parent 37d842c21e
commit 7967e6ab96
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
5 changed files with 60 additions and 46 deletions

View file

@ -80,7 +80,7 @@ async function doFetch(event: FetchEvent): Promise<Response> {
if (event.request.headers.get('x-pw-serviceworker') === 'forward') { if (event.request.headers.get('x-pw-serviceworker') === 'forward') {
const request = new Request(event.request); const request = new Request(event.request);
request.headers.delete('x-pw-serviceworker') request.headers.delete('x-pw-serviceworker');
return fetch(request); return fetch(request);
} }

View file

@ -1,5 +1,20 @@
/**
* 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 { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import Markdown from 'react-markdown' import Markdown from 'react-markdown';
import './aiConversation.css'; import './aiConversation.css';
import { clsx } from '@web/uiUtils'; import { clsx } from '@web/uiUtils';
import type { Conversation, LLMMessage } from './llm'; import type { Conversation, LLMMessage } from './llm';
@ -15,48 +30,48 @@ export function AIConversation({ history, conversation }: { history: LLMMessage[
}, [conversation]); }, [conversation]);
return ( return (
<div className="chat-container"> <div className='chat-container'>
<div className="messages-container"> <div className='messages-container'>
{history.filter(({ role }) => role !== 'developer').map((message, index) => ( {history.filter(({ role }) => role !== 'developer').map((message, index) => (
<div <div
key={'' + index} key={'' + index}
className={clsx('message', message.role === 'user' && 'user-message')} className={clsx('message', message.role === 'user' && 'user-message')}
> >
{message.role === 'assistant' && ( {message.role === 'assistant' && (
<div className="message-icon"> <div className='message-icon'>
<img src="playwright-logo.svg" /> <img src='playwright-logo.svg' />
</div> </div>
)} )}
<div className="message-content"> <div className='message-content'>
<Markdown>{message.displayContent ?? message.content}</Markdown> <Markdown>{message.displayContent ?? message.content}</Markdown>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="input-form"> <div className='input-form'>
<textarea <textarea
name='content' name='content'
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={e => setInput(e.target.value)}
onKeyDown={e => { onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
onSubmit(); onSubmit();
} }
}} }}
placeholder="Ask a question..." placeholder='Ask a question...'
className="message-input" className='message-input'
/> />
{conversation.isSending() ? ( {conversation.isSending() ? (
<button type="button" className="send-button" onClick={(evt) => { <button type='button' className='send-button' onClick={evt => {
evt.preventDefault() evt.preventDefault();
conversation.abortSending(); conversation.abortSending();
}}> }}>
Cancel Cancel
</button> </button>
) : ( ) : (
<button className="send-button" disabled={!input.trim()} onClick={onSubmit}> <button className='send-button' disabled={!input.trim()} onClick={onSubmit}>
Send Send
</button> </button>
)} )}

View file

@ -127,7 +127,7 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
</div>} </div>}
<span style={{ position: 'absolute', right: '5px' }}> <span style={{ position: 'absolute', right: '5px' }}>
{llmAvailable {llmAvailable
? <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> ? <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>
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={diff} />} : <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={diff} />}
</span> </span>
</div> </div>
@ -160,12 +160,12 @@ export const ErrorsTab: React.FunctionComponent<{
export function AIErrorConversation({ conversationId, error, pageSnapshot, diff }: { conversationId: string, error: string, pageSnapshot?: string, diff?: string }) { export function AIErrorConversation({ conversationId, error, pageSnapshot, diff }: { conversationId: string, error: string, pageSnapshot?: string, diff?: string }) {
const [history, conversation] = useLLMConversation( const [history, conversation] = useLLMConversation(
conversationId, conversationId,
[ [
`My Playwright test failed. What's going wrong?`, `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.`, `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.` `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') ].join('\n')
); );
React.useEffect(() => { React.useEffect(() => {

View file

@ -224,33 +224,32 @@ export function LLMProvider({ children }: React.PropsWithChildren<{}>) {
const cookiePairs = useCookies(); const cookiePairs = useCookies();
const chat = React.useMemo(() => { const chat = React.useMemo(() => {
const cookies = Object.fromEntries(cookiePairs); const cookies = Object.fromEntries(cookiePairs);
console.log({ cookies })
if (cookies.openai_api_key) if (cookies.openai_api_key)
return new LLMChat(new OpenAI(cookies.openai_api_key, cookies.openai_base_url)); return new LLMChat(new OpenAI(cookies.openai_api_key, cookies.openai_base_url));
if (cookies.anthropic_api_key) if (cookies.anthropic_api_key)
return new LLMChat(new Anthropic(cookies.anthropic_api_key, cookies.anthropic_base_url)); return new LLMChat(new Anthropic(cookies.anthropic_api_key, cookies.anthropic_base_url));
}, [cookiePairs]); }, [cookiePairs]);
return <llmContext.Provider value={chat}>{children}</llmContext.Provider>; return <llmContext.Provider value={chat}>{children}</llmContext.Provider>;
}; }
export function useLLMChat() { export function useLLMChat() {
return React.useContext(llmContext); return React.useContext(llmContext);
}; }
export function useLLMConversation(id: string, systemPrompt: string) { export function useLLMConversation(id: string, systemPrompt: string) {
const chat = useLLMChat(); const chat = useLLMChat();
if (!chat) if (!chat)
throw new Error('No LLM chat available, make sure theres a LLMProvider above'); throw new Error('No LLM chat available, make sure theres a LLMProvider above');
const conversation = React.useMemo(() => chat.getConversation(id, systemPrompt), [chat, id]); const conversation = React.useMemo(() => chat.getConversation(id, systemPrompt), [chat, id]);
const [history, setHistory] = React.useState(conversation.history); const [history, setHistory] = React.useState(conversation.history);
React.useEffect(() => { React.useEffect(() => {
function update() { function update() {
setHistory([...conversation.history]); setHistory([...conversation.history]);
} }
update(); update();
const subscription = conversation.onChange.event(update); const subscription = conversation.onChange.event(update);
return subscription.dispose; return subscription.dispose;
}, [conversation]); }, [conversation]);
return [history, conversation] as const; return [history, conversation] as const;
}; }

View file

@ -250,8 +250,8 @@ export function useFlash(): [boolean, EffectCallback] {
} }
export function useCookies() { export function useCookies() {
return document.cookie.split("; ").filter(v => v.includes("=")).map(kv => { return document.cookie.split('; ').filter(v => v.includes('=')).map(kv => {
const separator = kv.indexOf("="); const separator = kv.indexOf('=');
return [kv.substring(0, separator), kv.substring(separator + 1)]; return [kv.substring(0, separator), kv.substring(separator + 1)];
}) });
} }