diff --git a/package-lock.json b/package-lock.json index eb69ce2f362f2..e1edd82bf3198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "formidable": "^2.1.1", "immutable": "^4.3.7", "license-checker": "^25.0.1", + "markdown-to-jsx": "^7.7.3", "mime": "^3.0.0", "node-stream-zip": "^1.15.0", "react": "^18.1.0", @@ -5897,7 +5898,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -5953,6 +5953,18 @@ "semver": "bin/semver" } }, + "node_modules/markdown-to-jsx": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.3.tgz", + "integrity": "sha512-o35IhJDFP6Fv60zPy+hbvZSQMmgvSGdK5j8NRZ7FeZMY+Bgqw+dSg7SC1ZEzC26++CiOUCqkbq96/c3j/FfTEQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -6784,7 +6796,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8975,6 +8986,7 @@ "packages/trace-viewer": { "version": "0.0.0", "dependencies": { + "markdown-to-jsx": "^7.7.3", "yaml": "^2.6.0" } }, diff --git a/package.json b/package.json index 8b54d54e6cc7d..f27ad12a76f7d 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "formidable": "^2.1.1", "immutable": "^4.3.7", "license-checker": "^25.0.1", + "markdown-to-jsx": "^7.7.3", "mime": "^3.0.0", "node-stream-zip": "^1.15.0", "react": "^18.1.0", diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index e0f05bac4123f..3f322e71258b3 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -142,6 +142,16 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ server.routePath('/', (_, response) => { response.statusCode = 302; response.setHeader('Location', urlPath); + + if (process.env.OPENAI_API_KEY) + response.appendHeader('Set-Cookie', `openai_api_key=${process.env.OPENAI_API_KEY}`); + if (process.env.OPENAI_BASE_URL) + response.appendHeader('Set-Cookie', `openai_base_url=${process.env.OPENAI_BASE_URL}`); + if (process.env.ANTHROPIC_API_KEY) + response.appendHeader('Set-Cookie', `anthropic_api_key=${process.env.ANTHROPIC_API_KEY}`); + if (process.env.ANTHROPIC_BASE_URL) + response.appendHeader('Set-Cookie', `anthropic_base_url=${process.env.ANTHROPIC_BASE_URL}`); + response.end(); return true; }); diff --git a/packages/trace-viewer/package.json b/packages/trace-viewer/package.json index 57a3dfd4094f3..fde4eb6766a7c 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "dependencies": { - "yaml": "^2.6.0" + "yaml": "^2.6.0", + "markdown-to-jsx": "^7.7.3" } } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index cd23082d912c2..cdce3261feac0 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -78,6 +78,12 @@ async function doFetch(event: FetchEvent): Promise { if (event.request.url.startsWith('chrome-extension://')) return fetch(event.request); + if (event.request.headers.get('x-pw-serviceworker') === 'forward') { + const request = new Request(event.request); + request.headers.delete('x-pw-serviceworker'); + return fetch(request); + } + const request = event.request; const client = await self.clients.get(event.clientId); diff --git a/packages/trace-viewer/src/ui/aiConversation.css b/packages/trace-viewer/src/ui/aiConversation.css new file mode 100644 index 0000000000000..214496017abec --- /dev/null +++ b/packages/trace-viewer/src/ui/aiConversation.css @@ -0,0 +1,128 @@ +/** + * 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. + */ + +.chat-container { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + color: #e0e0e0; +} + +.chat-disclaimer { + text-align: center; + color: var(--vscode-editorBracketMatch-border); + margin: 0px; +} + +.chat-container hr { + width: 100%; + border: none; + border-top: 1px solid var(--vscode-titleBar-inactiveBackground); +} + +.messages-container { + flex: 1; + overflow-y: auto; + padding: 16px; + + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + gap: 12px; + max-width: 85%; +} + +.user-message { + flex-direction: row-reverse; + margin-left: auto; + width: fit-content +} + +.message-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--vscode-titleBar-inactiveBackground); + flex-shrink: 0; +} + +.message-content { + background-color: var(--vscode-titleBar-inactiveBackground); + color: var(--vscode-titleBar-activeForeground); + padding: 2px 8px; +} + +.message-content pre { + text-wrap: auto; + overflow-wrap: anywhere; +} + +.user-message .message-content { + background-color: var(--vscode-titleBar-activeBackground); +} + +/* Input form styles */ +.input-form { + position: sticky; + bottom: 0; + display: flex; + height: 64px; + gap: 8px; + padding: 10px; + background-color: var(--vscode-sideBar-background); + border-top: 1px solid var(--vscode-sideBarSectionHeader-border); +} + +.message-input { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--vscode-settings-textInputBorder); + background-color: var(--vscode-settings-textInputBackground); + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +.message-input:focus { + border-color: #0078d4; +} + +.send-button { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + background-color: var(--vscode-button-background); + border: none; + color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.send-button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.send-button:disabled { + background-color: var(--vscode-disabledForeground); + cursor: not-allowed; +} diff --git a/packages/trace-viewer/src/ui/aiConversation.tsx b/packages/trace-viewer/src/ui/aiConversation.tsx new file mode 100644 index 0000000000000..548ed626f026c --- /dev/null +++ b/packages/trace-viewer/src/ui/aiConversation.tsx @@ -0,0 +1,84 @@ +/** + * 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 Markdown from 'markdown-to-jsx'; +import './aiConversation.css'; +import { clsx } from '@web/uiUtils'; +import { useLLMConversation } from './llm'; + +export function AIConversation({ conversationId }: { conversationId: string }) { + const [history, conversation] = useLLMConversation(conversationId); + const [input, setInput] = useState(''); + + const onSubmit = useCallback(() => { + setInput(content => { + conversation.send(content); + return ''; + }); + }, [conversation]); + + return ( +
+

Chat based on {conversation.chat.api.name}. Check for mistakes.

+
+
+ {history.filter(({ role }) => role !== 'developer').map((message, index) => ( +
+ {message.role === 'assistant' && ( +
+ +
+ )} +
+ {message.displayContent ?? message.content} +
+
+ ))} +
+ +
+