From e500a43772c056429b76258dd83432d307995f90 Mon Sep 17 00:00:00 2001
From: Diego <53939730+DiegoIEC@users.noreply.github.com>
Date: Tue, 24 Mar 2026 15:32:02 +0100
Subject: [PATCH] Analytics dashboard displays scores
---
AIStatsMeasurement.Web/package-lock.json | 471 ++++++++++++++-
AIStatsMeasurement.Web/package.json | 4 +-
AIStatsMeasurement.Web/src/Analytics.tsx | 558 ++++++++++++++++++
AIStatsMeasurement.Web/src/App.css | 76 ---
AIStatsMeasurement.Web/src/App.tsx | 258 +-------
AIStatsMeasurement.Web/src/Navbar.css | 24 +
AIStatsMeasurement.Web/src/Navbar.tsx | 30 +
.../src/RunSinglePrompt.css | 229 +++++++
.../src/RunSinglePrompt.tsx | 320 ++++++++++
Backend/AI_stats_measurement.Backend.csproj | 2 +-
Backend/Clients/ChatGPTWebSearchQuerier.cs | 37 ++
Backend/Clients/GeminiWebSearchQuerier.cs | 41 ++
Backend/Clients/GrokQuerier.cs | 2 -
Backend/Clients/GrokWebSearchQuerier.cs | 91 +++
Backend/Controllers/MetricsController.cs | 63 ++
Backend/Controllers/PromptsController.cs | 26 +-
Backend/Controllers/SourcesController.cs | 110 ++++
Backend/Data/AIMeasureDbContext.cs | 37 +-
Backend/Dto/ChartPointDto.cs | 8 +
Backend/Dto/DashboardMetricsDto.cs | 10 +
Backend/Dto/MetricsFilterDto.cs | 12 +
Backend/Dto/PromptDto.cs | 5 +-
.../20260312141833_InitialCreate1.cs | 148 -----
.../20260317110026_InitialCreate.Designer.cs | 399 +++++++++++++
...ate.cs => 20260317110026_InitialCreate.cs} | 95 ++-
...20260317124535_InitialCreate1.Designer.cs} | 117 +++-
.../20260317124535_InitialCreate1.cs | 59 ++
...20260319103433_InitialCreate2.Designer.cs} | 115 +++-
.../20260319103433_InitialCreate2.cs | 39 ++
.../20260319135710_InitialCreate3.Designer.cs | 393 ++++++++++++
.../20260319135710_InitialCreate3.cs | 22 +
.../20260323152744_InitialCreate4.Designer.cs | 394 +++++++++++++
.../20260323152744_InitialCreate4.cs | 38 ++
.../20260323153545_InitialCreate5.Designer.cs | 394 +++++++++++++
.../20260323153545_InitialCreate5.cs | 22 +
.../AIMeasureDbContextModelSnapshot.cs | 126 ++--
Backend/Models/ExportRow.cs | 118 ++--
Backend/Models/FactCheckResult.cs | 21 +-
Backend/Models/ParsedModelResponse.cs | 28 +-
Backend/Models/ParsedModelResponseSource.cs | 13 +
Backend/Models/Prompt.cs | 9 +-
Backend/Models/Source.cs | 13 +
Backend/Program.cs | 8 +
Backend/Services/AnalyticsService.cs | 120 ++++
Backend/Services/EvaluationPipeline.cs | 58 +-
Backend/Services/FactChecker.cs | 92 +--
Backend/Services/ModelResponseParser.cs | 415 +++++++++++--
Backend/Services/SourceNormalizer.cs | 73 +++
Tests/AnalyticsServiceTest.cs | 22 +
Tests/FactCheckerTests.cs | 182 +-----
Tests/ModelResponseParserTests.cs | 195 ++++--
51 files changed, 5106 insertions(+), 1036 deletions(-)
create mode 100644 AIStatsMeasurement.Web/src/Analytics.tsx
delete mode 100644 AIStatsMeasurement.Web/src/App.css
create mode 100644 AIStatsMeasurement.Web/src/Navbar.css
create mode 100644 AIStatsMeasurement.Web/src/Navbar.tsx
create mode 100644 AIStatsMeasurement.Web/src/RunSinglePrompt.css
create mode 100644 AIStatsMeasurement.Web/src/RunSinglePrompt.tsx
create mode 100644 Backend/Clients/ChatGPTWebSearchQuerier.cs
create mode 100644 Backend/Clients/GeminiWebSearchQuerier.cs
create mode 100644 Backend/Clients/GrokWebSearchQuerier.cs
create mode 100644 Backend/Controllers/MetricsController.cs
create mode 100644 Backend/Controllers/SourcesController.cs
create mode 100644 Backend/Dto/ChartPointDto.cs
create mode 100644 Backend/Dto/DashboardMetricsDto.cs
create mode 100644 Backend/Dto/MetricsFilterDto.cs
delete mode 100644 Backend/Migrations/20260312141833_InitialCreate1.cs
create mode 100644 Backend/Migrations/20260317110026_InitialCreate.Designer.cs
rename Backend/Migrations/{20260312104828_InitialCreate.cs => 20260317110026_InitialCreate.cs} (68%)
rename Backend/Migrations/{20260312141833_InitialCreate1.Designer.cs => 20260317124535_InitialCreate1.Designer.cs} (77%)
create mode 100644 Backend/Migrations/20260317124535_InitialCreate1.cs
rename Backend/Migrations/{20260312104828_InitialCreate.Designer.cs => 20260319103433_InitialCreate2.Designer.cs} (74%)
create mode 100644 Backend/Migrations/20260319103433_InitialCreate2.cs
create mode 100644 Backend/Migrations/20260319135710_InitialCreate3.Designer.cs
create mode 100644 Backend/Migrations/20260319135710_InitialCreate3.cs
create mode 100644 Backend/Migrations/20260323152744_InitialCreate4.Designer.cs
create mode 100644 Backend/Migrations/20260323152744_InitialCreate4.cs
create mode 100644 Backend/Migrations/20260323153545_InitialCreate5.Designer.cs
create mode 100644 Backend/Migrations/20260323153545_InitialCreate5.cs
create mode 100644 Backend/Models/ParsedModelResponseSource.cs
create mode 100644 Backend/Models/Source.cs
create mode 100644 Backend/Services/AnalyticsService.cs
create mode 100644 Backend/Services/SourceNormalizer.cs
create mode 100644 Tests/AnalyticsServiceTest.cs
diff --git a/AIStatsMeasurement.Web/package-lock.json b/AIStatsMeasurement.Web/package-lock.json
index c0ef8ac..9d3b3c8 100644
--- a/AIStatsMeasurement.Web/package-lock.json
+++ b/AIStatsMeasurement.Web/package-lock.json
@@ -10,7 +10,9 @@
"dependencies": {
"lucide-react": "^0.577.0",
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.1",
+ "recharts": "^3.8.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -1010,6 +1012,42 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.4",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
+ "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@@ -1367,6 +1405,18 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1412,6 +1462,69 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1440,7 +1553,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1456,6 +1569,12 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.56.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
@@ -1948,6 +2067,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1982,6 +2110,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2001,9 +2142,130 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2022,6 +2284,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2036,6 +2304,16 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/es-toolkit": {
+ "version": "1.45.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz",
+ "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/esbuild": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
@@ -2285,6 +2563,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2463,6 +2747,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2490,6 +2784,15 @@
"node": ">=0.8.19"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2879,6 +3182,36 @@
"react": "^19.2.4"
}
},
+ "node_modules/react-is": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
+ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -2889,6 +3222,95 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
+ "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2960,6 +3382,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3019,6 +3447,12 @@
"node": ">=8"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -3148,6 +3582,37 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
diff --git a/AIStatsMeasurement.Web/package.json b/AIStatsMeasurement.Web/package.json
index 5a0f457..f2ba603 100644
--- a/AIStatsMeasurement.Web/package.json
+++ b/AIStatsMeasurement.Web/package.json
@@ -12,7 +12,9 @@
"dependencies": {
"lucide-react": "^0.577.0",
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.1",
+ "recharts": "^3.8.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/AIStatsMeasurement.Web/src/Analytics.tsx b/AIStatsMeasurement.Web/src/Analytics.tsx
new file mode 100644
index 0000000..49c68ac
--- /dev/null
+++ b/AIStatsMeasurement.Web/src/Analytics.tsx
@@ -0,0 +1,558 @@
+import { useState } from 'react'
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ Tooltip,
+ LineChart,
+ Line,
+ CartesianGrid,
+ RadarChart,
+ PolarGrid,
+ PolarAngleAxis,
+ Radar
+} from 'recharts'
+
+const nsiOptions = ['CBS', 'OECD', 'STATBANK DENMARK']
+
+const llmOptions = [
+ 'gemini-2.5-flash-lite-preview-09-2025',
+ 'gpt-4o-mini',
+ 'grok-4-1-fast-non-reasoning',
+ 'gemini-3.1-pro-preview',
+ 'gpt-5.4',
+ 'grok-4.20-reasoning'
+]
+
+const themeOptions = [
+ 'Arbeid en sociale zekerheid',
+ 'Bedrijven',
+ 'Bevolking',
+ 'Bouwen en wonen',
+ 'Caribisch Nederland',
+ 'Energie',
+ 'Financiële en zakelijke diensten',
+ 'Gezondheid en welzijn',
+ 'Handel en horeca',
+ 'Industrie',
+ 'Inkomen en bestedingen',
+ 'Internationale handel',
+ 'Landbouw',
+ 'Macro-economie',
+ 'Natuur en milieu',
+ 'Nederland regionaal',
+ 'Onderwijs',
+ 'Overheid',
+ 'Prijzen',
+ 'Veiligheid en recht',
+ 'Verkeer en vervoer',
+ 'Vrije tijd en cultuur',
+ 'Agriculture and fisheries',
+ 'Development',
+ 'Economy',
+ 'Education and skills',
+ 'Environment and climate change',
+ 'Finance and investment',
+ 'Public governance',
+ 'Health',
+ 'Industry, business and entrepreneurship',
+ 'Science, technology and innovation',
+ 'Employment',
+ 'Society',
+ 'Regional, rural and urban development',
+ 'Trade',
+ 'Transport',
+ 'Taxation'
+]
+
+const pageTheme = {
+ background: '#f8fafc',
+ cardBackground: '#ffffff',
+ text: '#0f172a',
+ mutedText: '#475569',
+ border: '#e2e8f0',
+ grid: '#cbd5e1',
+ primary: '#2563eb',
+ secondary: '#10b981',
+ accent: '#f59e0b',
+ radarFill: '#2563eb',
+ radarStroke: '#1d4ed8',
+ tooltipBackground: '#ffffff'
+}
+
+type SummaryDto = {
+ accuracy: number
+ findability: number
+ consistency: number
+ totalmeasurements: number
+}
+
+type BarItemDto = {
+ name: string
+ score: number
+}
+
+type TimelineItemDto = {
+ run: string
+ accuracy: number
+ findability: number
+}
+
+type RadarItemDto = {
+ metric: string
+ value: number
+}
+
+type AnalyticsResponse = {
+ accuracyScore: number
+ findabilityScore: number
+ consistencyScore: number
+ totalMeasurements: number
+ barData?: BarItemDto[]
+ timelineData?: TimelineItemDto[]
+ radarData?: RadarItemDto[]
+}
+
+function ScoreCard({ title, value }: { title: string; value: number }) {
+ return (
+
+
+ {title}
+
+
+
+ {value.toFixed(1)}
+
+
+ )
+}
+
+function Analytics() {
+ const [selectedNsi, setSelectedNsi] = useState('')
+ const [selectedLlm, setSelectedLlm] = useState('')
+ const [selectedTheme, setSelectedTheme] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState('')
+
+ const [summary, setSummary] = useState({
+ accuracy: 0,
+ findability: 0,
+ consistency: 0,
+ totalmeasurements: 0
+ })
+
+ const [barData, setBarData] = useState([])
+ const [timelineData, setTimelineData] = useState([])
+ const [radarData, setRadarData] = useState([])
+
+ const handleSend = async () => {
+ setIsLoading(true)
+ setError('')
+
+ try {
+ const response = await fetch('http://localhost:5201/api/metrics', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ nsi: selectedNsi,
+ llm: selectedLlm,
+ theme: selectedTheme
+ })
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(errorText || 'Failed to load analytics')
+ }
+
+ const data: AnalyticsResponse = await response.json()
+
+ setSummary({
+ accuracy: data.accuracyScore ?? 0,
+ findability: data.findabilityScore ?? 0,
+ consistency: data.consistencyScore ?? 0,
+ totalmeasurements: data.totalMeasurements ?? 0
+ })
+
+ setBarData(
+ data.barData ?? [
+ { name: selectedLlm || 'Selected LLM', score: data.accuracyScore ?? 0 }
+ ]
+ )
+
+ setTimelineData(
+ data.timelineData ?? [
+ {
+ run: 'Current',
+ accuracy: data.accuracyScore ?? 0,
+ findability: data.findabilityScore ?? 0
+ }
+ ]
+ )
+
+ setRadarData(
+ data.radarData ?? [
+ { metric: 'Accuracy score', value: data.accuracyScore ?? 0 },
+ { metric: 'Source score', value: data.findabilityScore ?? 0 },
+ { metric: 'Consistency score', value: data.consistencyScore ?? 0 }
+ ]
+ )
+} catch (err) {
+ if (err instanceof Error) {
+ setError(err.message)
+ } else {
+ setError('Something went wrong')
+ }
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
Analytics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Selected filters
+
+ NSI: {selectedNsi || 'None'}
+
+
+ LLM: {selectedLlm || 'None'}
+
+
+ Theme: {selectedTheme || 'None'}
+
+
+ {error && (
+
+ Error: {error}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ Score per LLM
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Timeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Metrics overview
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Analytics
\ No newline at end of file
diff --git a/AIStatsMeasurement.Web/src/App.css b/AIStatsMeasurement.Web/src/App.css
deleted file mode 100644
index 517c62a..0000000
--- a/AIStatsMeasurement.Web/src/App.css
+++ /dev/null
@@ -1,76 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
-
-.app-container {
- max-width: 1100px;
- margin: 0 auto;
- padding: 32px;
-}
-
-.prompt-select {
- margin-top: 20px;
-}
-
-.select-input {
- width: 100%;
- padding: 10px;
- margin-top: 8px;
- border-radius: 6px;
- border: 1px solid #ccc;
-}
-
-.question-preview {
- margin-top: 12px;
- padding: 10px;
- background: #f3f4f6;
- border-radius: 6px;
-}
-
-.run-button {
- margin-top: 20px;
- padding: 10px 16px;
- background: #2563eb;
- color: white;
- border: none;
- border-radius: 6px;
- cursor: pointer;
-}
-
-.results-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
- gap: 20px;
- margin-top: 20px;
-}
-
-.result-card {
- padding: 18px;
- border-radius: 10px;
- text-align: left;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- min-height: 420px;
- overflow-wrap: break-word;
- word-break: break-word;
-}
-
-.raw-text {
- font-size: 14px;
- margin-top: 6px;
- line-height: 1.5;
- overflow-wrap: break-word;
- word-break: break-word;
-}
diff --git a/AIStatsMeasurement.Web/src/App.tsx b/AIStatsMeasurement.Web/src/App.tsx
index 1671960..532c161 100644
--- a/AIStatsMeasurement.Web/src/App.tsx
+++ b/AIStatsMeasurement.Web/src/App.tsx
@@ -1,250 +1,18 @@
-import { type FormEvent, useState, useEffect } from 'react'
-import './App.css'
-
-type Prompt = {
- id: number
- theme: string
- subject: string
- question: string
-}
-
-type ExportRow = {
- id: number
- theme: string
- question: string
- expectedAnswer: number
- expectedSource: string
- actualAnswer: number
- actualSource: string[]
- provider: string
- rawText: string | null
- exception: string | null
- squareMeanRootError: number
- relativeError: number
- answerIsCorrect: boolean
- sourceIsCorrect: boolean
- averageRelativeError: number
- averageAnswer: number
- averageAnswerCorrectness: number
- averageSourceCorrectness: number
- createdUtc: string
-}
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import Navbar from "./Navbar.tsx";
+import Analytics from "./Analytics.tsx";
+import RunSinglePrompt from "./RunSinglePrompt.tsx";
function App() {
- const [prompts, setPrompts] = useState([])
- const [selectedPromptId, setSelectedPromptId] = useState(null)
- const [submittedQuestion, setSubmittedQuestion] = useState('')
- const [isLoading, setIsLoading] = useState(false)
- const [results, setResults] = useState([])
- const [error, setError] = useState('')
-
- useEffect(() => {
- fetch('http://localhost:5201/api/prompts')
- .then(res => res.json())
- .then(data => setPrompts(data))
- .catch(() => console.log('Failed loading prompts'))
- }, [])
-
- const handlePromptSelect = (id: number) => {
- setSelectedPromptId(id)
-
- const prompt = prompts.find(p => p.id === id)
- if (prompt) {
- setSubmittedQuestion(prompt.question)
- }
- }
-
- const handleSubmit = async (e: FormEvent) => {
- e.preventDefault()
-
- if (!selectedPromptId || isLoading) return
-
- setIsLoading(true)
- setError('')
- setResults([])
-
- try {
- const response = await fetch('http://localhost:5201/api/llm/run', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify([selectedPromptId]),
- })
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(errorText || 'Request failed')
- }
-
- const data: ExportRow[] = await response.json()
- setResults(data)
- } catch (err) {
- if (err instanceof Error) {
- setError(err.message)
- } else {
- setError('Er ging iets mis.')
- }
- } finally {
- setIsLoading(false)
- }
- }
-
return (
-
-
LLM Statistics Monitoring Tool
-
-
-
-
-
-
-
- {submittedQuestion && (
-
- Question: {submittedQuestion}
-
- )}
-
-
-
- {error &&
{error}
}
-
- {results.length > 0 && (
-
-
Responses
-
-
- {results.map((result, index) => {
- const color =
- result.provider.includes('gpt')
- ? '#2563eb'
- : result.provider.includes('gemini')
- ? '#16a34a'
- : '#9333ea'
-
- const background =
- result.provider.includes('gpt')
- ? '#eff6ff'
- : result.provider.includes('gemini')
- ? '#f0fdf4'
- : '#faf5ff'
-
- return (
-
-
{result.provider}
-
-
- Actual answer: {result.actualAnswer}
-
-
-
- Expected answer: {result.expectedAnswer}
-
-
-
-
Actual source:
- {result.actualSource && result.actualSource.length > 0 ? (
-
- {result.actualSource.map((src, i) => (
- -
-
- {src}
-
-
- ))}
-
- ) : (
-
no reference found
- )}
-
-
-
- Expected source:{' '}
-
- {result.expectedSource}
-
-
-
-
- Relative error: {(result.relativeError * 100).toFixed(1) + "%"}
-
-
-
- Answer correct: {result.answerIsCorrect ? "yes" : "no"}
-
-
-
- Source correct: {result.sourceIsCorrect ? "yes" : "no"}
-
-
-
- Average answer: {result.averageAnswer}
-
-
-
- Average relative error: {result.averageRelativeError}
-
-
-
- Average answer correctness: {(result.averageAnswerCorrectness * 100).toFixed(1) + "%"}
-
-
-
- Average source correctness: {(result.averageSourceCorrectness * 100).toFixed(1) + "%"}
-
-
-
-
Raw text:
-
{result.rawText}
-
-
- {result.exception && (
-
- Exception: {result.exception}
-
- )}
-
- )
- })}
-
-
- )}
-
- )
+
+
+
+ } />
+ } />
+
+
+ );
}
-export default App
+export default App;
\ No newline at end of file
diff --git a/AIStatsMeasurement.Web/src/Navbar.css b/AIStatsMeasurement.Web/src/Navbar.css
new file mode 100644
index 0000000..b240101
--- /dev/null
+++ b/AIStatsMeasurement.Web/src/Navbar.css
@@ -0,0 +1,24 @@
+.navbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #1e293b;
+ padding: 12px 24px;
+ color: white;
+}
+
+.logo {
+ margin: 0;
+}
+
+.nav-links a {
+ margin-left: 16px;
+ text-decoration: none;
+ color: #cbd5f5;
+ font-weight: 500;
+}
+
+.nav-links a.active {
+ color: white;
+ border-bottom: 2px solid white;
+}
\ No newline at end of file
diff --git a/AIStatsMeasurement.Web/src/Navbar.tsx b/AIStatsMeasurement.Web/src/Navbar.tsx
new file mode 100644
index 0000000..3b5a7d3
--- /dev/null
+++ b/AIStatsMeasurement.Web/src/Navbar.tsx
@@ -0,0 +1,30 @@
+import { Link, useLocation } from "react-router-dom";
+import "./Navbar.css";
+
+function Navbar() {
+ const location = useLocation();
+
+ return (
+
+ );
+}
+
+export default Navbar;
\ No newline at end of file
diff --git a/AIStatsMeasurement.Web/src/RunSinglePrompt.css b/AIStatsMeasurement.Web/src/RunSinglePrompt.css
new file mode 100644
index 0000000..58b3b6d
--- /dev/null
+++ b/AIStatsMeasurement.Web/src/RunSinglePrompt.css
@@ -0,0 +1,229 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: Inter, system-ui, Arial, sans-serif;
+ background: #f8fafc;
+ color: #0f172a;
+}
+
+.app-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 32px 20px 60px;
+}
+
+h1 {
+ margin-bottom: 24px;
+ font-size: 2rem;
+}
+
+h2 {
+ margin-bottom: 18px;
+}
+
+.prompt-select,
+.question-preview,
+.error-message {
+ background: white;
+ border-radius: 18px;
+ padding: 16px;
+ margin-bottom: 16px;
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
+ border: 1px solid #e2e8f0;
+}
+
+.select-input {
+ width: 100%;
+ margin-top: 8px;
+ padding: 12px 14px;
+ border-radius: 12px;
+ border: 1px solid #cbd5e1;
+ background: #fff;
+ font-size: 1rem;
+}
+
+.run-button {
+ margin: 12px 0 20px;
+ border: none;
+ border-radius: 14px;
+ padding: 12px 20px;
+ background: #0f172a;
+ color: white;
+ font-weight: 700;
+ cursor: pointer;
+ transition: transform 0.15s ease, opacity 0.15s ease;
+}
+
+.run-button:hover {
+ transform: translateY(-1px);
+}
+
+.run-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.error-message {
+ color: #b91c1c;
+ background: #fef2f2;
+ border-color: #fecaca;
+}
+
+.results-section {
+ margin-top: 24px;
+}
+
+.results-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
+ gap: 20px;
+}
+
+.result-card {
+ border-radius: 24px;
+ padding: 20px;
+ box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
+}
+
+.result-card h3 {
+ margin-top: 0;
+ margin-bottom: 16px;
+ font-size: 1.2rem;
+ text-transform: capitalize;
+}
+
+.result-card p {
+ line-height: 1.5;
+}
+
+.sources-section {
+ margin-top: 16px;
+ margin-bottom: 16px;
+}
+
+.source-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 12px;
+ margin-top: 10px;
+}
+
+.expected-grid {
+ grid-template-columns: 1fr;
+}
+
+.source-card {
+ background: rgba(255, 255, 255, 0.88);
+ border: 1px solid #e2e8f0;
+ border-radius: 18px;
+ padding: 14px;
+ box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
+ transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
+ backdrop-filter: blur(8px);
+}
+
+.source-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 26px rgba(15, 23, 42, 0.1);
+ border-color: #cbd5e1;
+}
+
+.source-card-header {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+}
+
+.source-type-badge {
+ display: inline-block;
+ padding: 4px 10px;
+ border-radius: 999px;
+ background: #e0e7ff;
+ color: #4338ca;
+ font-size: 12px;
+ font-weight: 700;
+ text-transform: capitalize;
+}
+
+.expected-badge {
+ background: #dcfce7;
+ color: #166534;
+}
+
+.source-card-body {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.source-name {
+ margin: 0;
+ font-weight: 700;
+ color: #111827;
+ word-break: break-word;
+}
+
+.source-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: fit-content;
+ padding: 9px 12px;
+ border-radius: 12px;
+ background: #f8fafc;
+ color: #2563eb;
+ text-decoration: none;
+ font-weight: 600;
+ border: 1px solid #dbeafe;
+ transition: background 0.18s ease, border-color 0.18s ease;
+}
+
+.source-link:hover {
+ background: #eff6ff;
+ border-color: #bfdbfe;
+}
+
+.source-no-link,
+.no-source-text {
+ margin: 0;
+ color: #64748b;
+}
+
+.raw-text-block {
+ margin-top: 16px;
+ background: rgba(255, 255, 255, 0.65);
+ border: 1px solid #e2e8f0;
+ border-radius: 16px;
+ padding: 14px;
+}
+
+.source-url {
+ margin: 0;
+ font-size: 0.85rem;
+ color: #475569;
+ word-break: break-all;
+ line-height: 1.4;
+ background: #f1f5f9;
+ padding: 8px 10px;
+ border-radius: 10px;
+ border: 1px solid #e2e8f0;
+}
+
+.raw-text {
+ margin-top: 8px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ color: #334155;
+}
+
+.exception-text {
+ margin-top: 14px;
+ color: #b91c1c;
+ background: #fff1f2;
+ border: 1px solid #fecdd3;
+ padding: 12px;
+ border-radius: 14px;
+}
diff --git a/AIStatsMeasurement.Web/src/RunSinglePrompt.tsx b/AIStatsMeasurement.Web/src/RunSinglePrompt.tsx
new file mode 100644
index 0000000..1e52dd0
--- /dev/null
+++ b/AIStatsMeasurement.Web/src/RunSinglePrompt.tsx
@@ -0,0 +1,320 @@
+import { type FormEvent, useState, useEffect } from 'react'
+
+import './RunSinglePrompt.css'
+
+type Prompt = {
+ id: number
+ theme: string
+ subject: string
+ question: string
+}
+
+type ExportRow = {
+ id: number
+ theme: string
+ question: string
+ expectedAnswer: number
+ expectedSource: string
+ actualAnswer: number
+ actualSource: number[]
+ provider: string
+ rawText: string | null
+ exception: string | null
+ squareMeanRootError: number
+ relativeError: number
+ answerIsCorrect: boolean
+ sourceIsCorrect: boolean
+ createdUtc: string
+}
+
+type SourceDto = {
+ id: number
+ name: string | null
+ url: string | null
+ type: string | null
+}
+
+type ResultWithSources = ExportRow & {
+ actualSourceDetails: SourceDto[]
+}
+
+const fetchSourcesByIds = async (ids: number[]): Promise => {
+ if (!ids.length) return []
+
+ const response = await fetch('http://localhost:5201/api/sources/getByIds', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(ids)
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch sources')
+ }
+
+ return response.json()
+}
+
+function RunSinglePrompt() {
+ const [prompts, setPrompts] = useState([])
+ const [selectedPromptId, setSelectedPromptId] = useState(null)
+ const [submittedQuestion, setSubmittedQuestion] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+ const [results, setResults] = useState([])
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ fetch('http://localhost:5201/api/prompts')
+ .then((res) => res.json())
+ .then((data) => setPrompts(data))
+ .catch(() => console.log('Failed loading prompts'))
+ }, [])
+
+ const handlePromptSelect = (id: number) => {
+ setSelectedPromptId(id)
+
+ const prompt = prompts.find((p) => p.id === id)
+ if (prompt) {
+ setSubmittedQuestion(prompt.question)
+ } else {
+ setSubmittedQuestion('')
+ }
+ }
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault()
+
+ if (!selectedPromptId || isLoading) return
+
+ setIsLoading(true)
+ setError('')
+ setResults([])
+
+ try {
+ const response = await fetch('http://localhost:5201/api/llm/run', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify([selectedPromptId])
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(errorText || 'Request failed')
+ }
+
+ const data: ExportRow[] = await response.json()
+
+ const allSourceIds = [...new Set(data.flatMap((r) => r.actualSource ?? []))]
+
+ const sourceDtos = await fetchSourcesByIds(allSourceIds)
+
+ const sourceMap = new Map(
+ sourceDtos.map((source) => [source.id, source])
+ )
+
+ const enrichedResults: ResultWithSources[] = data.map((result) => ({
+ ...result,
+ actualSourceDetails: (result.actualSource ?? [])
+ .map((id) => sourceMap.get(id))
+ .filter((source): source is SourceDto => Boolean(source))
+ }))
+
+ setResults(enrichedResults)
+ } catch (err) {
+ if (err instanceof Error) {
+ setError(err.message)
+ } else {
+ setError('Something went wrong.')
+ }
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
LLM Statistics Monitoring Tool
+
+
+
+
+
+
+
+ {submittedQuestion && (
+
+ Question: {submittedQuestion}
+
+ )}
+
+
+
+ {error &&
{error}
}
+
+ {results.length > 0 && (
+
+
Responses
+
+
+ {results.map((result, index) => {
+ const color = result.provider.includes('gpt')
+ ? '#2563eb'
+ : result.provider.includes('gemini')
+ ? '#16a34a'
+ : '#9333ea'
+
+ const background = result.provider.includes('gpt')
+ ? '#eff6ff'
+ : result.provider.includes('gemini')
+ ? '#f0fdf4'
+ : '#faf5ff'
+
+ return (
+
+
{result.provider}
+
+
+ Expected answer: {result.expectedAnswer}
+
+
+
+ Actual answer: {result.actualAnswer}
+
+
+
+
Expected source:
+
+
+
+
+
+ NSI Database
+
+
+
+
+
+
+
+
+
+
Actual sources:
+
+ {result.actualSourceDetails.length > 0 ? (
+
+ {result.actualSourceDetails.map((src) => (
+
+
+
+ {src.type || 'unknown'}
+
+
+
+
+
+ {src.name || `Source #${src.id}`}
+
+
+ {src.url ? (
+ <>
+
{src.url}
+
+
+ Open source
+
+ >
+ ) : (
+
No URL available
+ )}
+
+
+ ))}
+
+ ) : (
+
No reference found
+ )}
+
+
+
+ Relative error:{' '}
+ {(result.relativeError * 100).toFixed(1) + '%'}
+
+
+
+ Answer correct:{' '}
+ {result.answerIsCorrect ? 'yes' : 'no'}
+
+
+
+ Source correct:{' '}
+ {result.sourceIsCorrect ? 'yes' : 'no'}
+
+
+
+
Raw text:
+
{result.rawText ?? 'No raw text available'}
+
+
+ {result.exception && (
+
+ Exception: {result.exception}
+
+ )}
+
+ )
+ })}
+
+
+ )}
+
+ )
+}
+
+export default RunSinglePrompt
diff --git a/Backend/AI_stats_measurement.Backend.csproj b/Backend/AI_stats_measurement.Backend.csproj
index 11242a7..2ce2fb7 100644
--- a/Backend/AI_stats_measurement.Backend.csproj
+++ b/Backend/AI_stats_measurement.Backend.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/Backend/Clients/ChatGPTWebSearchQuerier.cs b/Backend/Clients/ChatGPTWebSearchQuerier.cs
new file mode 100644
index 0000000..8ac2315
--- /dev/null
+++ b/Backend/Clients/ChatGPTWebSearchQuerier.cs
@@ -0,0 +1,37 @@
+#pragma warning disable OPENAI001
+using AI_stats_measurement.Interface;
+using OpenAI.Responses;
+
+namespace AI_stats_measurement.Backend.Clients
+{
+ public class ChatGPTWebSearchQuerier : ILlmQuerier
+ {
+ private readonly ResponsesClient _client;
+
+ public string Name => "gpt-5.4";
+
+ public ChatGPTWebSearchQuerier(IConfiguration config)
+ {
+ _client = new ResponsesClient(config["LlmKeys:OpenAI"]);
+ }
+
+ public async Task AskAsync(Prompt prompt, CancellationToken ct = default)
+ {
+ var options = new CreateResponseOptions
+ {
+ Model = Name
+ };
+
+ options.Tools.Add(ResponseTool.CreateWebSearchTool());
+
+ options.InputItems.Add(
+ ResponseItem.CreateUserMessageItem(
+ $"{prompt.Instruction}\n\n{prompt.Question}")
+ );
+
+ var response = await _client.CreateResponseAsync(options, ct);
+
+ return response.Value.GetOutputText();
+ }
+ }
+}
diff --git a/Backend/Clients/GeminiWebSearchQuerier.cs b/Backend/Clients/GeminiWebSearchQuerier.cs
new file mode 100644
index 0000000..312ef9f
--- /dev/null
+++ b/Backend/Clients/GeminiWebSearchQuerier.cs
@@ -0,0 +1,41 @@
+using AI_stats_measurement.Interface;
+using Google.GenAI;
+using Google.GenAI.Types;
+
+namespace AI_stats_measurement.Backend.Clients
+{
+ public class GeminiWebSearchQuerier : ILlmQuerier
+ {
+ private readonly Client _client;
+
+ public string Name => "gemini-3.1-pro-preview";
+
+ public GeminiWebSearchQuerier(IConfiguration config)
+ {
+ _client = new Client(apiKey: config["LlmKeys:Gemini"]);
+ }
+
+ public async Task AskAsync(Prompt prompt, CancellationToken ct = default)
+ {
+ var config = new GenerateContentConfig
+ {
+ Tools = new List
+ {
+ new Tool
+ {
+ GoogleSearch = new GoogleSearch()
+ }
+ }
+ };
+
+ var response = await _client.Models.GenerateContentAsync(
+ model: Name,
+ contents: $"{prompt.Instruction}\n\n{prompt.Question}",
+ config: config,
+ cancellationToken: ct
+ );
+
+ return response.Candidates[0].Content.Parts[0].Text ?? "";
+ }
+ }
+}
diff --git a/Backend/Clients/GrokQuerier.cs b/Backend/Clients/GrokQuerier.cs
index 4a5e16f..1563282 100644
--- a/Backend/Clients/GrokQuerier.cs
+++ b/Backend/Clients/GrokQuerier.cs
@@ -1,6 +1,4 @@
using AI_stats_measurement.Interface;
-using OpenAI;
-using OpenAI.Chat;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
diff --git a/Backend/Clients/GrokWebSearchQuerier.cs b/Backend/Clients/GrokWebSearchQuerier.cs
new file mode 100644
index 0000000..e50c59e
--- /dev/null
+++ b/Backend/Clients/GrokWebSearchQuerier.cs
@@ -0,0 +1,91 @@
+using AI_stats_measurement.Interface;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+
+namespace AI_stats_measurement.Backend.Clients
+{
+ public class GrokWebSearchQuerier : ILlmQuerier
+ {
+ private readonly HttpClient _httpClient;
+ private readonly string _apiKey;
+
+ public string Name => "grok-4.20-reasoning";
+
+ public GrokWebSearchQuerier(IConfiguration config)
+ {
+ _apiKey = config["LlmKeys:Grok"] ?? throw new InvalidOperationException("Missing Grok API key.");
+ _httpClient = new HttpClient
+ {
+ BaseAddress = new Uri("https://api.x.ai/")
+ };
+ }
+
+ public async Task AskAsync(Prompt prompt, CancellationToken ct = default)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Post, "v1/responses");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
+
+ var body = new
+ {
+ model = Name,
+ input = new object[]
+ {
+ new
+ {
+ role = "system",
+ content = prompt.Instruction
+ },
+ new
+ {
+ role = "user",
+ content = prompt.Question
+ }
+ },
+ tools = new object[]
+ {
+ new
+ {
+ type = "web_search"
+ }
+ }
+ };
+
+ request.Content = new StringContent(
+ JsonSerializer.Serialize(body),
+ Encoding.UTF8,
+ "application/json");
+
+ var response = await _httpClient.SendAsync(request, ct);
+ var jsonText = await response.Content.ReadAsStringAsync(ct);
+
+ response.EnsureSuccessStatusCode();
+
+ using var doc = JsonDocument.Parse(jsonText);
+ var root = doc.RootElement;
+
+ if (root.TryGetProperty("output", out var output))
+ {
+ foreach (var item in output.EnumerateArray())
+ {
+ if (item.TryGetProperty("type", out var itemType) &&
+ itemType.GetString() == "message" &&
+ item.TryGetProperty("content", out var content))
+ {
+ foreach (var part in content.EnumerateArray())
+ {
+ if (part.TryGetProperty("type", out var partType) &&
+ partType.GetString() == "output_text" &&
+ part.TryGetProperty("text", out var text))
+ {
+ return text.GetString() ?? "";
+ }
+ }
+ }
+ }
+ }
+
+ return "";
+ }
+ }
+}
diff --git a/Backend/Controllers/MetricsController.cs b/Backend/Controllers/MetricsController.cs
new file mode 100644
index 0000000..44df2e2
--- /dev/null
+++ b/Backend/Controllers/MetricsController.cs
@@ -0,0 +1,63 @@
+using AI_stats_measurement.Backend.Dto;
+using AI_stats_measurement.Backend.Services;
+using AI_stats_measurement.Data;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace AI_stats_measurement.Backend.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class MetricsController : ControllerBase
+ {
+ private readonly AIMeasureDbContext _context;
+ private readonly AnalyticsService _analyticsService;
+
+ public MetricsController(
+ AIMeasureDbContext context,
+ AnalyticsService analyticsService)
+ {
+ _context = context;
+ _analyticsService = analyticsService;
+ }
+
+ [HttpPost]
+ public ActionResult GetMetrics([FromBody] MetricsFilterDto filter)
+ {
+ var factsQuery = _context.FactCheckResults
+ .Include(f => f.ParsedModelResponse)
+ .ThenInclude(p => p.ModelResponse)
+ .ThenInclude(m => m.Prompt)
+ .AsQueryable();
+
+ if (!string.IsNullOrWhiteSpace(filter.Theme))
+ {
+ factsQuery = factsQuery.Where(f =>
+ f.ParsedModelResponse.ModelResponse.Prompt.Theme == filter.Theme);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.Llm))
+ {
+ factsQuery = factsQuery.Where(f =>
+ f.ParsedModelResponse.ModelResponse.Provider == filter.Llm);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.Nsi))
+ {
+ factsQuery = factsQuery.Where(f =>
+ f.ParsedModelResponse.ModelResponse.Prompt.Provider == filter.Nsi);
+ }
+
+ var facts = factsQuery.ToList();
+
+ var metrics = _analyticsService.GetMetrics(
+ facts,
+ filter.Nsi,
+ filter.Llm,
+ filter.Theme
+ );
+
+ return Ok(metrics);
+ }
+ }
+}
diff --git a/Backend/Controllers/PromptsController.cs b/Backend/Controllers/PromptsController.cs
index 156b541..6cae18a 100644
--- a/Backend/Controllers/PromptsController.cs
+++ b/Backend/Controllers/PromptsController.cs
@@ -7,6 +7,7 @@
using Microsoft.EntityFrameworkCore;
using AI_stats_measurement.Data;
using AI_stats_measurement.Backend.Dto;
+using AI_stats_measurement.Backend.Models;
namespace AI_stats_measurement.Backend.Controllers
{
@@ -74,7 +75,7 @@ public async Task PutPrompt(int id, Prompt prompt)
// POST: api/Prompts
[HttpPost]
- public async Task>> PostPrompts([FromBody] Listdtos)
+ public async Task>> PostPrompts([FromBody] List dtos)
{
if (dtos == null || dtos.Count == 0)
return BadRequest("No prompts provided.");
@@ -83,14 +84,35 @@ public async Task>> PostPrompts([FromBody] List
+ s.Name == sourceName &&
+ s.Url == sourceUrl);
+
+ if (source == null)
+ {
+ source = new Source
+ {
+ Name = sourceName,
+ Type = sourceType,
+ Url = sourceUrl
+ };
+
+ _context.Sources.Add(source);
+ }
+
var prompt = new Prompt(
+ dto.Provider,
dto.Instruction,
dto.Theme,
dto.Periode,
dto.Subject,
dto.Question,
dto.Answer,
- dto.Source,
+ source,
dto.AnswerLocation
);
diff --git a/Backend/Controllers/SourcesController.cs b/Backend/Controllers/SourcesController.cs
new file mode 100644
index 0000000..d0b52d8
--- /dev/null
+++ b/Backend/Controllers/SourcesController.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using AI_stats_measurement.Backend.Models;
+using AI_stats_measurement.Data;
+
+namespace AI_stats_measurement.Backend.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class SourcesController : ControllerBase
+ {
+ private readonly AIMeasureDbContext _context;
+
+ public SourcesController(AIMeasureDbContext context)
+ {
+ _context = context;
+ }
+
+ // GET: api/Sources
+ [HttpGet]
+ public async Task>> GetSources()
+ {
+ return await _context.Sources.ToListAsync();
+ }
+
+ [HttpPost("getByIds")]
+ public async Task>> GetSources([FromBody] List ids)
+ {
+ if (ids == null || !ids.Any())
+ return BadRequest("No ids provided");
+
+ var sources = await _context.Sources
+ .Where(s => ids.Contains(s.Id))
+ .ToListAsync();
+
+ if (!sources.Any())
+ return NotFound();
+
+ return Ok(sources);
+ }
+
+ // PUT: api/Sources/5
+ // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
+ [HttpPut("{id}")]
+ public async Task PutSource(int id, Source source)
+ {
+ if (id != source.Id)
+ {
+ return BadRequest();
+ }
+
+ _context.Entry(source).State = EntityState.Modified;
+
+ try
+ {
+ await _context.SaveChangesAsync();
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!SourceExists(id))
+ {
+ return NotFound();
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ return NoContent();
+ }
+
+ // POST: api/Sources
+ // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
+ [HttpPost]
+ public async Task> PostSource(Source source)
+ {
+ _context.Sources.Add(source);
+ await _context.SaveChangesAsync();
+
+ return CreatedAtAction("GetSource", new { id = source.Id }, source);
+ }
+
+ // DELETE: api/Sources/5
+ [HttpDelete("{id}")]
+ public async Task DeleteSource(int id)
+ {
+ var source = await _context.Sources.FindAsync(id);
+ if (source == null)
+ {
+ return NotFound();
+ }
+
+ _context.Sources.Remove(source);
+ await _context.SaveChangesAsync();
+
+ return NoContent();
+ }
+
+ private bool SourceExists(int id)
+ {
+ return _context.Sources.Any(e => e.Id == id);
+ }
+ }
+}
diff --git a/Backend/Data/AIMeasureDbContext.cs b/Backend/Data/AIMeasureDbContext.cs
index 4b164e3..c124656 100644
--- a/Backend/Data/AIMeasureDbContext.cs
+++ b/Backend/Data/AIMeasureDbContext.cs
@@ -1,8 +1,6 @@
using AI_stats_measurement.Backend.Models;
-using AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models;
using AI_stats_measurement.Models;
using Microsoft.EntityFrameworkCore;
-using System.Collections.Generic;
namespace AI_stats_measurement.Data;
@@ -18,6 +16,8 @@ public AIMeasureDbContext(DbContextOptions options)
public DbSet ModelResponses => Set();
public DbSet ParsedModelResponses => Set();
public DbSet FactCheckResults => Set();
+ public DbSet Sources => Set();
+ public DbSet ParsedModelResponseSources => Set();
public DbSet ExportRows => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -32,6 +32,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey(d => d.PromptId)
.OnDelete(DeleteBehavior.Cascade);
+ modelBuilder.Entity()
+ .HasOne(p => p.Source)
+ .WithMany(s => s.Prompts)
+ .HasForeignKey(p => p.SourceId)
+ .OnDelete(DeleteBehavior.Restrict);
+
modelBuilder.Entity()
.HasOne(r => r.Prompt)
.WithMany(p => p.ModelResponses)
@@ -50,6 +56,33 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey(f => f.ParsedModelResponseId)
.OnDelete(DeleteBehavior.Cascade);
+ modelBuilder.Entity()
+ .HasIndex(s => new { s.Name, s.Url })
+ .IsUnique();
+
+ modelBuilder.Entity()
+ .Property(x => x.Url)
+ .HasMaxLength(2048);
+
+ modelBuilder.Entity()
+ .Property(x => x.Name)
+ .HasMaxLength(512);
+
+ modelBuilder.Entity()
+ .HasKey(ps => new { ps.ParsedModelResponseId, ps.SourceId });
+
+ modelBuilder.Entity()
+ .HasOne(ps => ps.ParsedModelResponse)
+ .WithMany(p => p.ParsedModelResponseSources)
+ .HasForeignKey(ps => ps.ParsedModelResponseId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ modelBuilder.Entity()
+ .HasOne(ps => ps.Source)
+ .WithMany(s => s.ParsedModelResponseSources)
+ .HasForeignKey(x => x.SourceId)
+ .OnDelete(DeleteBehavior.Restrict);
+
modelBuilder.Entity();
}
}
diff --git a/Backend/Dto/ChartPointDto.cs b/Backend/Dto/ChartPointDto.cs
new file mode 100644
index 0000000..ee84a72
--- /dev/null
+++ b/Backend/Dto/ChartPointDto.cs
@@ -0,0 +1,8 @@
+namespace AI_stats_measurement.Backend.Dto
+{
+ public class ChartPointDto
+ {
+ public string Label { get; set; } = string.Empty;
+ public double Value { get; set; }
+ }
+}
diff --git a/Backend/Dto/DashboardMetricsDto.cs b/Backend/Dto/DashboardMetricsDto.cs
new file mode 100644
index 0000000..2826d3a
--- /dev/null
+++ b/Backend/Dto/DashboardMetricsDto.cs
@@ -0,0 +1,10 @@
+namespace AI_stats_measurement.Backend.Dto
+{
+ public class DashboardMetricsDto
+ {
+ public double AccuracyScore { get; set; }
+ public double ConsistencyScore { get; set; }
+ public double FindabilityScore { get; set; }
+ public int TotalMeasurements { get; set; }
+ }
+}
diff --git a/Backend/Dto/MetricsFilterDto.cs b/Backend/Dto/MetricsFilterDto.cs
new file mode 100644
index 0000000..3e0c6bf
--- /dev/null
+++ b/Backend/Dto/MetricsFilterDto.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace AI_stats_measurement.Backend.Dto
+{
+ public class MetricsFilterDto
+ {
+ public string? Nsi { get; set; }
+ public string? Llm { get; set; }
+ public string? Theme { get; set; }
+ }
+}
diff --git a/Backend/Dto/PromptDto.cs b/Backend/Dto/PromptDto.cs
index 203cb47..523565d 100644
--- a/Backend/Dto/PromptDto.cs
+++ b/Backend/Dto/PromptDto.cs
@@ -2,13 +2,16 @@
{
public class PromptDto
{
+ public string Provider { get; set; } = null!;
public string Instruction { get; set; } = null!;
public string Theme { get; set; } = null!;
public DateTime Periode { get; set; }
public string Subject { get; set; } = null!;
public string Question { get; set; } = null!;
public decimal Answer { get; set; }
- public string Source { get; set; } = null!;
+ public string SourceName { get; set; } = null!;
+ public string SourceType { get; set; } = null!;
+ public string SourceUrl { get; set; } = null!;
public string AnswerLocation { get; set; } = null!;
public Dictionary Dimensions { get; set; } = new();
}
diff --git a/Backend/Migrations/20260312141833_InitialCreate1.cs b/Backend/Migrations/20260312141833_InitialCreate1.cs
deleted file mode 100644
index 78dfa4b..0000000
--- a/Backend/Migrations/20260312141833_InitialCreate1.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace AI_stats_measurement.Backend.Migrations
-{
- ///
- public partial class InitialCreate1 : Migration
- {
- ///
- protected override void Up(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.RenameColumn(
- name: "IsCorrect",
- table: "FactCheckResults",
- newName: "SourceIsCorrect");
-
- migrationBuilder.RenameColumn(
- name: "IsCorrect",
- table: "ExportRows",
- newName: "SourceIsCorrect");
-
- migrationBuilder.AddColumn(
- name: "AnswerIsCorrect",
- table: "FactCheckResults",
- type: "bit",
- nullable: false,
- defaultValue: false);
-
- migrationBuilder.AddColumn(
- name: "AverageAnswer",
- table: "FactCheckResults",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AverageAnswerCorrectness",
- table: "FactCheckResults",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AverageRelativeError",
- table: "FactCheckResults",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AverageSourceCorrectness",
- table: "FactCheckResults",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AnswerIsCorrect",
- table: "ExportRows",
- type: "bit",
- nullable: false,
- defaultValue: false);
-
- migrationBuilder.AddColumn(
- name: "AverageAnswer",
- table: "ExportRows",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AverageAnswerCorrectness",
- table: "ExportRows",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AverageRelativeError",
- table: "ExportRows",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
-
- migrationBuilder.AddColumn(
- name: "AverageSourceCorrectness",
- table: "ExportRows",
- type: "decimal(18,2)",
- nullable: false,
- defaultValue: 0m);
- }
-
- ///
- protected override void Down(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.DropColumn(
- name: "AnswerIsCorrect",
- table: "FactCheckResults");
-
- migrationBuilder.DropColumn(
- name: "AverageAnswer",
- table: "FactCheckResults");
-
- migrationBuilder.DropColumn(
- name: "AverageAnswerCorrectness",
- table: "FactCheckResults");
-
- migrationBuilder.DropColumn(
- name: "AverageRelativeError",
- table: "FactCheckResults");
-
- migrationBuilder.DropColumn(
- name: "AverageSourceCorrectness",
- table: "FactCheckResults");
-
- migrationBuilder.DropColumn(
- name: "AnswerIsCorrect",
- table: "ExportRows");
-
- migrationBuilder.DropColumn(
- name: "AverageAnswer",
- table: "ExportRows");
-
- migrationBuilder.DropColumn(
- name: "AverageAnswerCorrectness",
- table: "ExportRows");
-
- migrationBuilder.DropColumn(
- name: "AverageRelativeError",
- table: "ExportRows");
-
- migrationBuilder.DropColumn(
- name: "AverageSourceCorrectness",
- table: "ExportRows");
-
- migrationBuilder.RenameColumn(
- name: "SourceIsCorrect",
- table: "FactCheckResults",
- newName: "IsCorrect");
-
- migrationBuilder.RenameColumn(
- name: "SourceIsCorrect",
- table: "ExportRows",
- newName: "IsCorrect");
- }
- }
-}
diff --git a/Backend/Migrations/20260317110026_InitialCreate.Designer.cs b/Backend/Migrations/20260317110026_InitialCreate.Designer.cs
new file mode 100644
index 0000000..ff3aadd
--- /dev/null
+++ b/Backend/Migrations/20260317110026_InitialCreate.Designer.cs
@@ -0,0 +1,399 @@
+//
+using System;
+using AI_stats_measurement.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace AI_stats_measurement.Backend.Migrations
+{
+ [DbContext(typeof(AIMeasureDbContext))]
+ [Migration("20260317110026_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ActualAnswer")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("AnswerIsCorrect")
+ .HasColumnType("bit");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("Exception")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExpectedAnswer")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("ExpectedSource")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Provider")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Question")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RawText")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RelativeError")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("SourceIsCorrect")
+ .HasColumnType("bit");
+
+ b.Property("SquareMeanRootError")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Theme")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ExportRows");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.Property("ParsedModelResponseId")
+ .HasColumnType("int");
+
+ b.Property("SourceId")
+ .HasColumnType("int");
+
+ b.Property("ExportRowId")
+ .HasColumnType("int");
+
+ b.HasKey("ParsedModelResponseId", "SourceId");
+
+ b.HasIndex("ExportRowId");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("ParsedModelResponseSources");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("PromptId")
+ .HasColumnType("int");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PromptId", "Name")
+ .IsUnique();
+
+ b.ToTable("PromptDimensions");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Type")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Url")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name", "Url")
+ .IsUnique()
+ .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL");
+
+ b.ToTable("Sources");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("Exception")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("PromptId")
+ .HasColumnType("int");
+
+ b.Property("Provider")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RawText")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PromptId");
+
+ b.ToTable("ModelResponses");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Answer")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("ModelResponseId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ModelResponseId")
+ .IsUnique();
+
+ b.ToTable("ParsedModelResponses");
+ });
+
+ modelBuilder.Entity("FactCheckResult", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AnswerIsCorrect")
+ .HasColumnType("bit");
+
+ b.Property("ParsedModelResponseId")
+ .HasColumnType("int");
+
+ b.Property("RelativeError")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("SourceIsCorrect")
+ .HasColumnType("bit");
+
+ b.Property("SquareMeanRootError")
+ .HasColumnType("decimal(18,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParsedModelResponseId")
+ .IsUnique();
+
+ b.ToTable("FactCheckResults");
+ });
+
+ modelBuilder.Entity("Prompt", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Answer")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("AnswerLocation")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("Instruction")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Periode")
+ .HasColumnType("datetime2");
+
+ b.Property("Question")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SourceId")
+ .HasColumnType("int");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Theme")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("Prompts");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.HasOne("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", null)
+ .WithMany("ActualSource")
+ .HasForeignKey("ExportRowId");
+
+ b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse")
+ .WithMany("ParsedModelResponseSources")
+ .HasForeignKey("ParsedModelResponseId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source")
+ .WithMany("ParsedModelResponseSources")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("ParsedModelResponse");
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
+ {
+ b.HasOne("Prompt", "Prompt")
+ .WithMany("Dimensions")
+ .HasForeignKey("PromptId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Prompt");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
+ {
+ b.HasOne("Prompt", "Prompt")
+ .WithMany("ModelResponses")
+ .HasForeignKey("PromptId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Prompt");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b =>
+ {
+ b.HasOne("AI_stats_measurement.Models.ModelResponse", "ModelResponse")
+ .WithOne("ParsedResponse")
+ .HasForeignKey("AI_stats_measurement.Models.ParsedModelResponse", "ModelResponseId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ModelResponse");
+ });
+
+ modelBuilder.Entity("FactCheckResult", b =>
+ {
+ b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse")
+ .WithOne("FactCheckResult")
+ .HasForeignKey("FactCheckResult", "ParsedModelResponseId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ParsedModelResponse");
+ });
+
+ modelBuilder.Entity("Prompt", b =>
+ {
+ b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source")
+ .WithMany("Prompts")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", b =>
+ {
+ b.Navigation("ActualSource");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Navigation("ParsedModelResponseSources");
+
+ b.Navigation("Prompts");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
+ {
+ b.Navigation("ParsedResponse");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b =>
+ {
+ b.Navigation("FactCheckResult");
+
+ b.Navigation("ParsedModelResponseSources");
+ });
+
+ modelBuilder.Entity("Prompt", b =>
+ {
+ b.Navigation("Dimensions");
+
+ b.Navigation("ModelResponses");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Backend/Migrations/20260312104828_InitialCreate.cs b/Backend/Migrations/20260317110026_InitialCreate.cs
similarity index 68%
rename from Backend/Migrations/20260312104828_InitialCreate.cs
rename to Backend/Migrations/20260317110026_InitialCreate.cs
index 23caae6..b1a8e4d 100644
--- a/Backend/Migrations/20260312104828_InitialCreate.cs
+++ b/Backend/Migrations/20260317110026_InitialCreate.cs
@@ -22,13 +22,13 @@ protected override void Up(MigrationBuilder migrationBuilder)
ExpectedAnswer = table.Column(type: "decimal(18,2)", nullable: false),
ExpectedSource = table.Column(type: "nvarchar(max)", nullable: false),
ActualAnswer = table.Column(type: "decimal(18,2)", nullable: false),
- ActualSource = table.Column(type: "nvarchar(max)", nullable: false),
Provider = table.Column(type: "nvarchar(max)", nullable: false),
RawText = table.Column(type: "nvarchar(max)", nullable: true),
Exception = table.Column(type: "nvarchar(max)", nullable: true),
SquareMeanRootError = table.Column(type: "decimal(18,2)", nullable: false),
RelativeError = table.Column(type: "decimal(18,2)", nullable: false),
- IsCorrect = table.Column(type: "bit", nullable: false),
+ AnswerIsCorrect = table.Column(type: "bit", nullable: false),
+ SourceIsCorrect = table.Column(type: "bit", nullable: false),
CreatedUtc = table.Column(type: "datetime2", nullable: false)
},
constraints: table =>
@@ -36,6 +36,21 @@ protected override void Up(MigrationBuilder migrationBuilder)
table.PrimaryKey("PK_ExportRows", x => x.Id);
});
+ migrationBuilder.CreateTable(
+ name: "Sources",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(450)", nullable: true),
+ Url = table.Column(type: "nvarchar(450)", nullable: true),
+ Type = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Sources", x => x.Id);
+ });
+
migrationBuilder.CreateTable(
name: "Prompts",
columns: table => new
@@ -48,13 +63,19 @@ protected override void Up(MigrationBuilder migrationBuilder)
Subject = table.Column(type: "nvarchar(max)", nullable: false),
Question = table.Column(type: "nvarchar(max)", nullable: false),
Answer = table.Column(type: "decimal(18,2)", nullable: false),
- Source = table.Column(type: "nvarchar(max)", nullable: false),
+ SourceId = table.Column(type: "int", nullable: false),
AnswerLocation = table.Column(type: "nvarchar(max)", nullable: false),
CreatedUtc = table.Column(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Prompts", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Prompts_Sources_SourceId",
+ column: x => x.SourceId,
+ principalTable: "Sources",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
@@ -108,8 +129,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
Id = table.Column(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ModelResponseId = table.Column(type: "int", nullable: false),
- Answer = table.Column(type: "decimal(18,2)", nullable: false),
- Sources = table.Column(type: "nvarchar(max)", nullable: false)
+ Answer = table.Column(type: "decimal(18,2)", nullable: false)
},
constraints: table =>
{
@@ -131,7 +151,8 @@ protected override void Up(MigrationBuilder migrationBuilder)
ParsedModelResponseId = table.Column(type: "int", nullable: false),
SquareMeanRootError = table.Column(type: "decimal(18,2)", nullable: false),
RelativeError = table.Column(type: "decimal(18,2)", nullable: false),
- IsCorrect = table.Column(type: "bit", nullable: false)
+ AnswerIsCorrect = table.Column(type: "bit", nullable: false),
+ SourceIsCorrect = table.Column(type: "bit", nullable: false)
},
constraints: table =>
{
@@ -144,6 +165,36 @@ protected override void Up(MigrationBuilder migrationBuilder)
onDelete: ReferentialAction.Cascade);
});
+ migrationBuilder.CreateTable(
+ name: "ParsedModelResponseSources",
+ columns: table => new
+ {
+ ParsedModelResponseId = table.Column(type: "int", nullable: false),
+ SourceId = table.Column(type: "int", nullable: false),
+ ExportRowId = table.Column(type: "int", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ParsedModelResponseSources", x => new { x.ParsedModelResponseId, x.SourceId });
+ table.ForeignKey(
+ name: "FK_ParsedModelResponseSources_ExportRows_ExportRowId",
+ column: x => x.ExportRowId,
+ principalTable: "ExportRows",
+ principalColumn: "Id");
+ table.ForeignKey(
+ name: "FK_ParsedModelResponseSources_ParsedModelResponses_ParsedModelResponseId",
+ column: x => x.ParsedModelResponseId,
+ principalTable: "ParsedModelResponses",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_ParsedModelResponseSources_Sources_SourceId",
+ column: x => x.SourceId,
+ principalTable: "Sources",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
migrationBuilder.CreateIndex(
name: "IX_FactCheckResults_ParsedModelResponseId",
table: "FactCheckResults",
@@ -161,25 +212,50 @@ protected override void Up(MigrationBuilder migrationBuilder)
column: "ModelResponseId",
unique: true);
+ migrationBuilder.CreateIndex(
+ name: "IX_ParsedModelResponseSources_ExportRowId",
+ table: "ParsedModelResponseSources",
+ column: "ExportRowId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ParsedModelResponseSources_SourceId",
+ table: "ParsedModelResponseSources",
+ column: "SourceId");
+
migrationBuilder.CreateIndex(
name: "IX_PromptDimensions_PromptId_Name",
table: "PromptDimensions",
columns: new[] { "PromptId", "Name" },
unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Prompts_SourceId",
+ table: "Prompts",
+ column: "SourceId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Sources_Name_Url",
+ table: "Sources",
+ columns: new[] { "Name", "Url" },
+ unique: true,
+ filter: "[Name] IS NOT NULL AND [Url] IS NOT NULL");
}
///
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
- name: "ExportRows");
+ name: "FactCheckResults");
migrationBuilder.DropTable(
- name: "FactCheckResults");
+ name: "ParsedModelResponseSources");
migrationBuilder.DropTable(
name: "PromptDimensions");
+ migrationBuilder.DropTable(
+ name: "ExportRows");
+
migrationBuilder.DropTable(
name: "ParsedModelResponses");
@@ -188,6 +264,9 @@ protected override void Down(MigrationBuilder migrationBuilder)
migrationBuilder.DropTable(
name: "Prompts");
+
+ migrationBuilder.DropTable(
+ name: "Sources");
}
}
}
diff --git a/Backend/Migrations/20260312141833_InitialCreate1.Designer.cs b/Backend/Migrations/20260317124535_InitialCreate1.Designer.cs
similarity index 77%
rename from Backend/Migrations/20260312141833_InitialCreate1.Designer.cs
rename to Backend/Migrations/20260317124535_InitialCreate1.Designer.cs
index 49475d2..2504697 100644
--- a/Backend/Migrations/20260312141833_InitialCreate1.Designer.cs
+++ b/Backend/Migrations/20260317124535_InitialCreate1.Designer.cs
@@ -12,7 +12,7 @@
namespace AI_stats_measurement.Backend.Migrations
{
[DbContext(typeof(AIMeasureDbContext))]
- [Migration("20260312141833_InitialCreate1")]
+ [Migration("20260317124535_InitialCreate1")]
partial class InitialCreate1
{
///
@@ -43,18 +43,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("AnswerIsCorrect")
.HasColumnType("bit");
- b.Property("AverageAnswer")
- .HasColumnType("decimal(18,2)");
-
- b.Property("AverageAnswerCorrectness")
- .HasColumnType("decimal(18,2)");
-
- b.Property("AverageRelativeError")
- .HasColumnType("decimal(18,2)");
-
- b.Property("AverageSourceCorrectness")
- .HasColumnType("decimal(18,2)");
-
b.Property("CreatedUtc")
.HasColumnType("datetime2");
@@ -97,6 +85,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.ToTable("ExportRows");
});
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.Property("ParsedModelResponseId")
+ .HasColumnType("int");
+
+ b.Property("SourceId")
+ .HasColumnType("int");
+
+ b.HasKey("ParsedModelResponseId", "SourceId");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("ParsedModelResponseSources");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
{
b.Property("Id")
@@ -124,6 +127,32 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.ToTable("PromptDimensions");
});
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Type")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Url")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name", "Url")
+ .IsUnique()
+ .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL");
+
+ b.ToTable("Sources");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
{
b.Property("Id")
@@ -169,10 +198,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("ModelResponseId")
.HasColumnType("int");
- b.PrimitiveCollection("Sources")
- .IsRequired()
- .HasColumnType("nvarchar(max)");
-
b.HasKey("Id");
b.HasIndex("ModelResponseId")
@@ -192,18 +217,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("AnswerIsCorrect")
.HasColumnType("bit");
- b.Property("AverageAnswer")
- .HasColumnType("decimal(18,2)");
-
- b.Property("AverageAnswerCorrectness")
- .HasColumnType("decimal(18,2)");
-
- b.Property("AverageRelativeError")
- .HasColumnType("decimal(18,2)");
-
- b.Property("AverageSourceCorrectness")
- .HasColumnType("decimal(18,2)");
-
b.Property("ParsedModelResponseId")
.HasColumnType("int");
@@ -253,9 +266,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("nvarchar(max)");
- b.Property("Source")
- .IsRequired()
- .HasColumnType("nvarchar(max)");
+ b.Property("SourceId")
+ .HasColumnType("int");
b.Property("Subject")
.IsRequired()
@@ -267,9 +279,30 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasKey("Id");
+ b.HasIndex("SourceId");
+
b.ToTable("Prompts");
});
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse")
+ .WithMany("ParsedModelResponseSources")
+ .HasForeignKey("ParsedModelResponseId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source")
+ .WithMany("ParsedModelResponseSources")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("ParsedModelResponse");
+
+ b.Navigation("Source");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
{
b.HasOne("Prompt", "Prompt")
@@ -314,6 +347,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Navigation("ParsedModelResponse");
});
+ modelBuilder.Entity("Prompt", b =>
+ {
+ b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source")
+ .WithMany("Prompts")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Navigation("ParsedModelResponseSources");
+
+ b.Navigation("Prompts");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
{
b.Navigation("ParsedResponse");
@@ -322,6 +373,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b =>
{
b.Navigation("FactCheckResult");
+
+ b.Navigation("ParsedModelResponseSources");
});
modelBuilder.Entity("Prompt", b =>
diff --git a/Backend/Migrations/20260317124535_InitialCreate1.cs b/Backend/Migrations/20260317124535_InitialCreate1.cs
new file mode 100644
index 0000000..10f8bd4
--- /dev/null
+++ b/Backend/Migrations/20260317124535_InitialCreate1.cs
@@ -0,0 +1,59 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace AI_stats_measurement.Backend.Migrations
+{
+ ///
+ public partial class InitialCreate1 : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_ParsedModelResponseSources_ExportRows_ExportRowId",
+ table: "ParsedModelResponseSources");
+
+ migrationBuilder.DropIndex(
+ name: "IX_ParsedModelResponseSources_ExportRowId",
+ table: "ParsedModelResponseSources");
+
+ migrationBuilder.DropColumn(
+ name: "ExportRowId",
+ table: "ParsedModelResponseSources");
+
+ migrationBuilder.AddColumn(
+ name: "ActualSource",
+ table: "ExportRows",
+ type: "nvarchar(max)",
+ nullable: false,
+ defaultValue: "[]");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "ActualSource",
+ table: "ExportRows");
+
+ migrationBuilder.AddColumn(
+ name: "ExportRowId",
+ table: "ParsedModelResponseSources",
+ type: "int",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ParsedModelResponseSources_ExportRowId",
+ table: "ParsedModelResponseSources",
+ column: "ExportRowId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_ParsedModelResponseSources_ExportRows_ExportRowId",
+ table: "ParsedModelResponseSources",
+ column: "ExportRowId",
+ principalTable: "ExportRows",
+ principalColumn: "Id");
+ }
+ }
+}
diff --git a/Backend/Migrations/20260312104828_InitialCreate.Designer.cs b/Backend/Migrations/20260319103433_InitialCreate2.Designer.cs
similarity index 74%
rename from Backend/Migrations/20260312104828_InitialCreate.Designer.cs
rename to Backend/Migrations/20260319103433_InitialCreate2.Designer.cs
index a100c64..5952da3 100644
--- a/Backend/Migrations/20260312104828_InitialCreate.Designer.cs
+++ b/Backend/Migrations/20260319103433_InitialCreate2.Designer.cs
@@ -12,8 +12,8 @@
namespace AI_stats_measurement.Backend.Migrations
{
[DbContext(typeof(AIMeasureDbContext))]
- [Migration("20260312104828_InitialCreate")]
- partial class InitialCreate
+ [Migration("20260319103433_InitialCreate2")]
+ partial class InitialCreate2
{
///
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -40,6 +40,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("nvarchar(max)");
+ b.Property("AnswerIsCorrect")
+ .HasColumnType("bit");
+
b.Property("CreatedUtc")
.HasColumnType("datetime2");
@@ -53,9 +56,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("nvarchar(max)");
- b.Property("IsCorrect")
- .HasColumnType("bit");
-
b.Property("Provider")
.IsRequired()
.HasColumnType("nvarchar(max)");
@@ -70,6 +70,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("RelativeError")
.HasColumnType("decimal(18,2)");
+ b.Property("SourceIsCorrect")
+ .HasColumnType("bit");
+
b.Property("SquareMeanRootError")
.HasColumnType("decimal(18,2)");
@@ -82,6 +85,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.ToTable("ExportRows");
});
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.Property("ParsedModelResponseId")
+ .HasColumnType("int");
+
+ b.Property("SourceId")
+ .HasColumnType("int");
+
+ b.HasKey("ParsedModelResponseId", "SourceId");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("ParsedModelResponseSources");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
{
b.Property("Id")
@@ -109,6 +127,32 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.ToTable("PromptDimensions");
});
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Type")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Url")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name", "Url")
+ .IsUnique()
+ .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL");
+
+ b.ToTable("Sources");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
{
b.Property("Id")
@@ -154,10 +198,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("ModelResponseId")
.HasColumnType("int");
- b.PrimitiveCollection("Sources")
- .IsRequired()
- .HasColumnType("nvarchar(max)");
-
b.HasKey("Id");
b.HasIndex("ModelResponseId")
@@ -174,7 +214,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
- b.Property("IsCorrect")
+ b.Property("AbsoluteError")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("AnswerIsCorrect")
.HasColumnType("bit");
b.Property("ParsedModelResponseId")
@@ -183,8 +226,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("RelativeError")
.HasColumnType("decimal(18,2)");
- b.Property("SquareMeanRootError")
- .HasColumnType("decimal(18,2)");
+ b.Property("SourceIsCorrect")
+ .HasColumnType("bit");
b.HasKey("Id");
@@ -219,14 +262,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("Periode")
.HasColumnType("datetime2");
- b.Property("Question")
+ b.Property("Provider")
.IsRequired()
.HasColumnType("nvarchar(max)");
- b.Property("Source")
+ b.Property("Question")
.IsRequired()
.HasColumnType("nvarchar(max)");
+ b.Property("SourceId")
+ .HasColumnType("int");
+
b.Property("Subject")
.IsRequired()
.HasColumnType("nvarchar(max)");
@@ -237,9 +283,30 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.HasKey("Id");
+ b.HasIndex("SourceId");
+
b.ToTable("Prompts");
});
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse")
+ .WithMany("ParsedModelResponseSources")
+ .HasForeignKey("ParsedModelResponseId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source")
+ .WithMany("ParsedModelResponseSources")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("ParsedModelResponse");
+
+ b.Navigation("Source");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
{
b.HasOne("Prompt", "Prompt")
@@ -284,6 +351,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Navigation("ParsedModelResponse");
});
+ modelBuilder.Entity("Prompt", b =>
+ {
+ b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source")
+ .WithMany("Prompts")
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Navigation("ParsedModelResponseSources");
+
+ b.Navigation("Prompts");
+ });
+
modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
{
b.Navigation("ParsedResponse");
@@ -292,6 +377,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b =>
{
b.Navigation("FactCheckResult");
+
+ b.Navigation("ParsedModelResponseSources");
});
modelBuilder.Entity("Prompt", b =>
diff --git a/Backend/Migrations/20260319103433_InitialCreate2.cs b/Backend/Migrations/20260319103433_InitialCreate2.cs
new file mode 100644
index 0000000..fcf530c
--- /dev/null
+++ b/Backend/Migrations/20260319103433_InitialCreate2.cs
@@ -0,0 +1,39 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace AI_stats_measurement.Backend.Migrations
+{
+ ///
+ public partial class InitialCreate2 : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.RenameColumn(
+ name: "SquareMeanRootError",
+ table: "FactCheckResults",
+ newName: "AbsoluteError");
+
+ migrationBuilder.AddColumn(
+ name: "Provider",
+ table: "Prompts",
+ type: "nvarchar(max)",
+ nullable: false,
+ defaultValue: "");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Provider",
+ table: "Prompts");
+
+ migrationBuilder.RenameColumn(
+ name: "AbsoluteError",
+ table: "FactCheckResults",
+ newName: "SquareMeanRootError");
+ }
+ }
+}
diff --git a/Backend/Migrations/20260319135710_InitialCreate3.Designer.cs b/Backend/Migrations/20260319135710_InitialCreate3.Designer.cs
new file mode 100644
index 0000000..200326e
--- /dev/null
+++ b/Backend/Migrations/20260319135710_InitialCreate3.Designer.cs
@@ -0,0 +1,393 @@
+//
+using System;
+using AI_stats_measurement.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace AI_stats_measurement.Backend.Migrations
+{
+ [DbContext(typeof(AIMeasureDbContext))]
+ [Migration("20260319135710_InitialCreate3")]
+ partial class InitialCreate3
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ExportRow", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ActualAnswer")
+ .HasColumnType("decimal(18,2)");
+
+ b.PrimitiveCollection("ActualSource")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AnswerIsCorrect")
+ .HasColumnType("bit");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("Exception")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ExpectedAnswer")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("ExpectedSource")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Provider")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Question")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RawText")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RelativeError")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("SourceIsCorrect")
+ .HasColumnType("bit");
+
+ b.Property("SquareMeanRootError")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Theme")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ExportRows");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b =>
+ {
+ b.Property("ParsedModelResponseId")
+ .HasColumnType("int");
+
+ b.Property("SourceId")
+ .HasColumnType("int");
+
+ b.HasKey("ParsedModelResponseId", "SourceId");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("ParsedModelResponseSources");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("PromptId")
+ .HasColumnType("int");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PromptId", "Name")
+ .IsUnique();
+
+ b.ToTable("PromptDimensions");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("Type")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Url")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name", "Url")
+ .IsUnique()
+ .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL");
+
+ b.ToTable("Sources");
+ });
+
+ modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property