diff --git a/packages/web/src/i18n/en.json b/packages/web/src/i18n/en.json index 09126e3..a084c77 100644 --- a/packages/web/src/i18n/en.json +++ b/packages/web/src/i18n/en.json @@ -114,7 +114,42 @@ "replayDone": "Replay finished: {played}/{total} steps", "replayPartialTitle": "Replay finished: {played}/{total} steps", "replayPartialText": "{failed} steps failed — the page or its elements may have changed", - "replayFailed": "Could not start the replay" + "replayFailed": "Could not start the replay", + "aiPrompt": "AI prompt", + "aiPromptTitle": "Prompt for an AI agent", + "aiPromptHint": "Edit if needed, then copy and paste into your AI coding agent.", + "aiPromptCopy": "Copy prompt", + "ai": { + "intro": "You are an AI coding agent fixing a bug in a web application. Below is a bug report captured by a QA tester with the BugTrail extension.", + "bugSection": "Bug", + "title": "Title", + "pageSection": "Page", + "url": "URL", + "pageTitle": "Page title", + "elementSection": "Element", + "tag": "Tag", + "id": "ID", + "classes": "CSS classes", + "selector": "CSS selector", + "uniqueSelector": "Unique CSS selector", + "text": "Text", + "ariaLabel": "ARIA label", + "rect": "Position and size", + "testAttributes": "Test attributes", + "envSection": "Environment", + "browser": "Browser", + "os": "OS", + "viewport": "Viewport", + "dpr": "Device pixel ratio", + "language": "Language", + "userAgent": "User agent", + "stepsSection": "Recorded steps", + "attachmentsSection": "Attachments", + "screenshot": "Screenshot", + "reportLink": "Report link", + "taskSection": "Your task", + "taskText": "Reproduce the bug using the information above, locate the root cause in the codebase and fix it. Keep the fix minimal and focused; when done, explain what you changed and why." + } }, "settings": { "title": "Settings", diff --git a/packages/web/src/i18n/ru.json b/packages/web/src/i18n/ru.json index 7dc1743..b3ff82b 100644 --- a/packages/web/src/i18n/ru.json +++ b/packages/web/src/i18n/ru.json @@ -114,7 +114,42 @@ "replayDone": "Реплей завершён: {played}/{total} шагов", "replayPartialTitle": "Реплей завершён: {played}/{total} шагов", "replayPartialText": "{failed} шагов не выполнилось — страница или элементы могли измениться", - "replayFailed": "Не удалось запустить реплей" + "replayFailed": "Не удалось запустить реплей", + "aiPrompt": "Промпт для ИИ", + "aiPromptTitle": "Промпт для ИИ-агента", + "aiPromptHint": "При необходимости отредактируйте текст, затем скопируйте и передайте его ИИ-агенту.", + "aiPromptCopy": "Скопировать промпт", + "ai": { + "intro": "Ты — ИИ-агент, который чинит баги в веб-приложении. Ниже отчёт о баге, созданный тестировщиком через расширение BugTrail.", + "bugSection": "Баг", + "title": "Заголовок", + "pageSection": "Страница", + "url": "URL", + "pageTitle": "Заголовок страницы", + "elementSection": "Элемент", + "tag": "Тег", + "id": "ID", + "classes": "CSS-классы", + "selector": "CSS-селектор", + "uniqueSelector": "Уникальный CSS-селектор", + "text": "Текст", + "ariaLabel": "ARIA-метка", + "rect": "Положение и размер", + "testAttributes": "Тестовые атрибуты", + "envSection": "Окружение", + "browser": "Браузер", + "os": "ОС", + "viewport": "Вьюпорт", + "dpr": "Device pixel ratio", + "language": "Язык", + "userAgent": "User agent", + "stepsSection": "Записанные шаги", + "attachmentsSection": "Вложения", + "screenshot": "Скриншот", + "reportLink": "Ссылка на отчёт", + "taskSection": "Задача", + "taskText": "Воспроизведи баг по информации выше, найди причину в кодовой базе и исправь её. Изменения должны быть минимальными и точечными; в конце объясни, что и почему ты изменил." + } }, "settings": { "title": "Настройки", diff --git a/packages/web/src/lib/aiPrompt.ts b/packages/web/src/lib/aiPrompt.ts new file mode 100644 index 0000000..1681054 --- /dev/null +++ b/packages/web/src/lib/aiPrompt.ts @@ -0,0 +1,134 @@ +import type { RecordingStep, ReportDetail } from "@ltt/shared"; + +/** vue-i18n translate function (named interpolation only, which is all we need). */ +type Translate = (key: string, named?: Record) => string; + +export interface PromptLinks { + /** public share URL of the report itself */ + reportUrl: string; + /** public (token-scoped) download URL for an attachment file */ + fileUrl: (fileId: string) => string; +} + +/** + * Builds a ready-to-paste bug-fix prompt for an AI coding agent from a report: + * the tester's comment, page URL, element context, environment, recorded steps + * and attachment links. Section labels follow the panel language (i18n); the + * developer edits the result in the dialog before handing it to the agent. + */ +export function buildAiPrompt(report: ReportDetail, t: Translate, links: PromptLinks): string { + const lines: string[] = []; + const add = (line = "") => lines.push(line); + const field = (label: string, value: unknown) => { + if (value === undefined || value === null || value === "") return; + add(`- ${label}: ${value}`); + }; + + add(t("report.ai.intro")); + add(); + + add(`## ${t("report.ai.bugSection")}`); + add(`- ${t("report.ai.title")}: ${report.title}`); + add(`- ${t("report.status")}: ${t(`reports.status.${report.status}`)}`); + if (report.description) add(`- ${t("report.description")}: ${report.description}`); + add(); + + if (report.page_url || report.page_title) { + add(`## ${t("report.ai.pageSection")}`); + field(t("report.ai.url"), report.page_url); + field(t("report.ai.pageTitle"), report.page_title); + add(); + } + + const element = report.element; + if (element) { + add(`## ${t("report.ai.elementSection")}`); + field(t("report.ai.tag"), element.tag); + field(t("report.ai.id"), element.id); + field(t("report.ai.classes"), element.classes?.join(" ")); + field(t("report.ai.selector"), element.selector); + // same as the plain selector — not worth a second line + field(t("report.ai.uniqueSelector"), element.unique_selector !== element.selector ? element.unique_selector : null); + field(t("report.ai.text"), element.text_snippet ? `"${element.text_snippet}"` : null); + field(t("report.ai.ariaLabel"), element.aria_label); + if (element.rect) { + const r = element.rect; + field( + t("report.ai.rect"), + `x=${Math.round(r.x)} y=${Math.round(r.y)}, ${Math.round(r.w)}×${Math.round(r.h)}px` + ); + } + if (element.test_attributes && Object.keys(element.test_attributes).length) { + field(t("report.ai.testAttributes"), JSON.stringify(element.test_attributes)); + } + add(); + } + + const environment = report.environment ?? {}; + if (Object.keys(environment).length) { + add(`## ${t("report.ai.envSection")}`); + field(t("report.ai.browser"), [environment.browser, environment.browser_version].filter(Boolean).join(" ") || null); + field(t("report.ai.os"), environment.os); + const viewport = environment.viewport as { w?: number; h?: number } | undefined; + if (viewport?.w && viewport?.h) field(t("report.ai.viewport"), `${viewport.w}×${viewport.h}`); + field(t("report.ai.dpr"), environment.dpr); + field(t("report.ai.language"), environment.language); + field(t("report.ai.userAgent"), environment.user_agent); + add(); + } + + if (report.steps?.length) { + add(`## ${t("report.ai.stepsSection")}`); + report.steps.forEach((step, index) => add(`${index + 1}. ${stepLine(step, t)}`)); + add(); + } + + if (report.attachments.length) { + add(`## ${t("report.ai.attachmentsSection")}`); + // absolute URLs: the agent follows them from outside the panel, and the + // token in the path is the authorization + report.attachments.forEach((attachment) => { + const label = attachment.mime.startsWith("video/") + ? t("report.screenRecording") + : attachment.mime.startsWith("image/") + ? t("report.ai.screenshot") + : attachment.filename; + add(`- ${label}: ${new URL(links.fileUrl(attachment.file_id), window.location.origin)}`); + }); + add(); + } + + add(`## ${t("report.ai.reportLink")}`); + add(links.reportUrl); + add(); + + add(`## ${t("report.ai.taskSection")}`); + add(t("report.ai.taskText")); + return lines.join("\n"); +} + +/** One human-readable step line, e.g. `1.2s — Click: