diff --git a/README.md b/README.md index 8d5c62dd..aa0021b8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,42 @@ -PEV2: A VueJS component to show a graphical vizualization of a PostgreSQL execution -plan. +PEV2: A VueJS component to show a graphical vizualization of a PostgreSQL execution plan. + +# Features & Screenshots + +## 1. Graphical Plan Visualization (Main View) +The core interactive tree visualization showing query execution flow, node execution stats, settings and operational details. + +Graphical Plan Visualization + +
+ 2. Sortable Node Grid +

A tabular, sortable grid listing of plan nodes, making it easy to identify the most resource-intensive operations at a glance.

+ Sortable Node Grid +
+ +
+ 3. Plan Execution Statistics +

Detailed performance analytics, planning/execution timelines, triggers, and memory or I/O timings.

+ Plan Execution Statistics +
+ +
+ 4. AI Engine Configuration & Credentials +

Select your preferred AI engine (Google Gemini, OpenAI, Anthropic, or local Ollama) and configure endpoints and API keys stored securely in your browser.

+ AI Engine Configuration +
+ +
+ 5. AI Active Plan Analysis +

The AI assistant active analysis spinner screen when reading the execution plan and checking optimal tuning actions.

+ Active Plan Analysis +
+ +
+ 6. AI Optimization & Recommendations Report +

Custom formatted markdown report with actionable index recommendations, bottlenecks outline, and query rewrite suggestions. Multiple executions are saved locally, so you can compare the tuning suggestions of different AI models and providers.

+ AI Recommendations Report +
-![PEV2 screenshot](pev2_screenshot.png) # Usage diff --git a/index.html b/index.html index 5d08dc85..d928f66d 100644 --- a/index.html +++ b/index.html @@ -6,13 +6,9 @@ http-equiv="Content-Security-Policy" content=" default-src 'self' 'unsafe-inline' data:; - connect-src 'self' https://api.github.com; - " - /> - + connect-src 'self' https://api.github.com https://generativelanguage.googleapis.com https://api.openai.com https://api.anthropic.com http://localhost:11434; + "> + Postgres EXPLAIN Visualizer 2 (pev2) diff --git a/package-lock.json b/package-lock.json index 4167793a..e5874267 100644 --- a/package-lock.json +++ b/package-lock.json @@ -610,9 +610,9 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -7443,9 +7443,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "requires": { "balanced-match": "^4.0.2" diff --git a/package.json b/package.json index e8820adb..11a06b49 100644 --- a/package.json +++ b/package.json @@ -79,5 +79,8 @@ "lint-staged": { "*.{vue,js,jsx,cjs,mjs,ts,tsx,cts,mts}": "eslint --cache --fix", "*.{js,css,md}": "prettier --write" + }, + "allowScripts": { + "@parcel/watcher@2.5.6": true } } diff --git a/pev2_screenshot-ai-report.png b/pev2_screenshot-ai-report.png new file mode 100644 index 00000000..c3652f0b Binary files /dev/null and b/pev2_screenshot-ai-report.png differ diff --git a/pev2_screenshot-ai-setup.png b/pev2_screenshot-ai-setup.png new file mode 100644 index 00000000..a6190a7d Binary files /dev/null and b/pev2_screenshot-ai-setup.png differ diff --git a/pev2_screenshot-ai-working.png b/pev2_screenshot-ai-working.png new file mode 100644 index 00000000..06be4d83 Binary files /dev/null and b/pev2_screenshot-ai-working.png differ diff --git a/pev2_screenshot-grid.png b/pev2_screenshot-grid.png new file mode 100644 index 00000000..505a004a Binary files /dev/null and b/pev2_screenshot-grid.png differ diff --git a/pev2_screenshot-stats.png b/pev2_screenshot-stats.png new file mode 100644 index 00000000..e8335bfa Binary files /dev/null and b/pev2_screenshot-stats.png differ diff --git a/pev2_screenshot.png b/pev2_screenshot.png index f2e84a61..99db2b0a 100644 Binary files a/pev2_screenshot.png and b/pev2_screenshot.png differ diff --git a/src/components/AiAnalysis.vue b/src/components/AiAnalysis.vue new file mode 100644 index 00000000..93611a10 --- /dev/null +++ b/src/components/AiAnalysis.vue @@ -0,0 +1,718 @@ + + + + + diff --git a/src/components/Plan.vue b/src/components/Plan.vue index b5ab6853..1b9885f0 100644 --- a/src/components/Plan.vue +++ b/src/components/Plan.vue @@ -26,6 +26,7 @@ import LogoImage from "@/components/LogoImage.vue" import PlanNode from "@/components/PlanNode.vue" import PlanStats from "@/components/PlanStats.vue" import Stats from "@/components/Stats.vue" +import AiAnalysis from "@/components/AiAnalysis.vue" import AnimatedEdge from "@/components/AnimatedEdge.vue" import { findNodeById } from "@/services/help-service" import { HighlightType, Property } from "@/enums" @@ -63,6 +64,7 @@ const selectedNodeId = ref(NaN) const selectedNode = ref(undefined) const highlightedNodeId = ref(NaN) const gridIsNotNew = localStorage.getItem("gridIsNotNew") +const aiIsNotNew = localStorage.getItem("aiIsNotNew") const ready = ref(false) const viewOptions = reactive({ @@ -500,6 +502,21 @@ function updateNodeSize(node: Node, size: [number, number]) { >Stats + +
+ +
diff --git a/src/components/index.ts b/src/components/index.ts index b85f3a2b..126ebfd1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ import Plan from "./Plan.vue" +import AiAnalysis from "./AiAnalysis.vue" -export { Plan } +export { Plan, AiAnalysis } diff --git a/src/services/__tests__/ai-service.spec.ts b/src/services/__tests__/ai-service.spec.ts new file mode 100644 index 00000000..7c7262e2 --- /dev/null +++ b/src/services/__tests__/ai-service.spec.ts @@ -0,0 +1,77 @@ +import { describe, expect, test } from "vitest" +import { hashString, parseMarkdown } from "../ai-service" + +describe("AI Service Helpers", () => { + describe("hashString", () => { + test("handles empty string", () => { + expect(hashString("")).toBe("") + }) + + test("returns consistent hashes for identical strings", () => { + const input = "SELECT * FROM users WHERE id = 1" + expect(hashString(input)).toBe(hashString(input)) + }) + + test("produces different hashes for different strings", () => { + expect(hashString("select 1")).not.toBe(hashString("select 2")) + }) + + test("produces correct string hashes", () => { + const input = "test" + expect(hashString(input)).toBe("2487m") + }) + }) + + describe("parseMarkdown", () => { + test("returns empty string for empty input", () => { + expect(parseMarkdown("")).toBe("") + }) + + test("escapes HTML to prevent XSS", () => { + const input = "" + const output = parseMarkdown(input) + expect(output).not.toContain("