Yassine Mhirsi commited on
Commit
75137c7
·
1 Parent(s): c04cfa4

Add analysis charts and utilities

Browse files

- Introduced `StanceDistributionChart`, `TimeSeriesChart`, and `TopicFrequencyChart` components for visualizing analysis results.
- Implemented utility functions for calculating stance statistics, topic frequency, and time-based statistics.
- Updated `AnalysisPage` to include new charts and display user analysis statistics.
- Added `recharts` library for charting capabilities and updated dependencies in `package.json` and `package-lock.json`.

package-lock.json CHANGED
@@ -17,6 +17,7 @@
17
  "react-dom": "^19.1.0",
18
  "react-router-dom": "^7.10.1",
19
  "react-scripts": "5.0.1",
 
20
  "web-vitals": "^2.1.4"
21
  },
22
  "devDependencies": {
@@ -3005,6 +3006,42 @@
3005
  }
3006
  }
3007
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3008
  "node_modules/@rollup/plugin-babel": {
3009
  "version": "5.3.1",
3010
  "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3120,6 +3157,18 @@
3120
  "@sinonjs/commons": "^1.7.0"
3121
  }
3122
  },
 
 
 
 
 
 
 
 
 
 
 
 
3123
  "node_modules/@surma/rollup-plugin-off-main-thread": {
3124
  "version": "2.2.3",
3125
  "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -3575,6 +3624,69 @@
3575
  "@types/node": "*"
3576
  }
3577
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3578
  "node_modules/@types/eslint": {
3579
  "version": "8.56.12",
3580
  "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
@@ -3838,6 +3950,12 @@
3838
  "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
3839
  "license": "MIT"
3840
  },
 
 
 
 
 
 
3841
  "node_modules/@types/ws": {
3842
  "version": "8.18.1",
3843
  "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -5616,6 +5734,15 @@
5616
  "wrap-ansi": "^7.0.0"
5617
  }
5618
  },
 
 
 
 
 
 
 
 
 
5619
  "node_modules/co": {
5620
  "version": "4.6.0",
5621
  "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6354,6 +6481,127 @@
6354
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6355
  "license": "MIT"
6356
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6357
  "node_modules/damerau-levenshtein": {
6358
  "version": "1.0.8",
6359
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6448,6 +6696,12 @@
6448
  "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
6449
  "license": "MIT"
6450
  },
 
 
 
 
 
 
6451
  "node_modules/dedent": {
6452
  "version": "0.7.0",
6453
  "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -7097,6 +7351,16 @@
7097
  "url": "https://github.com/sponsors/ljharb"
7098
  }
7099
  },
 
 
 
 
 
 
 
 
 
 
7100
  "node_modules/escalade": {
7101
  "version": "3.2.0",
7102
  "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -9240,6 +9504,15 @@
9240
  "node": ">= 0.4"
9241
  }
9242
  },
 
 
 
 
 
 
 
 
 
9243
  "node_modules/ipaddr.js": {
9244
  "version": "2.3.0",
9245
  "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
@@ -13790,6 +14063,29 @@
13790
  "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
13791
  "license": "MIT"
13792
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13793
  "node_modules/react-refresh": {
13794
  "version": "0.11.0",
13795
  "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -13958,6 +14254,52 @@
13958
  "node": ">=8.10.0"
13959
  }
13960
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13961
  "node_modules/recursive-readdir": {
13962
  "version": "2.2.3",
13963
  "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -13983,6 +14325,21 @@
13983
  "node": ">=8"
13984
  }
13985
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13986
  "node_modules/reflect.getprototypeof": {
13987
  "version": "1.0.10",
13988
  "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -14136,6 +14493,12 @@
14136
  "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
14137
  "license": "MIT"
14138
  },
 
 
 
 
 
 
14139
  "node_modules/resolve": {
14140
  "version": "1.22.11",
14141
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -16080,6 +16443,12 @@
16080
  "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
16081
  "license": "MIT"
16082
  },
 
 
 
 
 
 
16083
  "node_modules/tinyglobby": {
16084
  "version": "0.2.15",
16085
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -16635,6 +17004,15 @@
16635
  "requires-port": "^1.0.0"
16636
  }
16637
  },
 
 
 
 
 
 
 
 
 
16638
  "node_modules/util-deprecate": {
16639
  "version": "1.0.2",
16640
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -16717,6 +17095,28 @@
16717
  "node": ">= 0.8"
16718
  }
16719
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16720
  "node_modules/w3c-hr-time": {
16721
  "version": "1.0.2",
16722
  "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
 
17
  "react-dom": "^19.1.0",
18
  "react-router-dom": "^7.10.1",
19
  "react-scripts": "5.0.1",
20
+ "recharts": "^3.5.1",
21
  "web-vitals": "^2.1.4"
22
  },
23
  "devDependencies": {
 
3006
  }
3007
  }
3008
  },
3009
+ "node_modules/@reduxjs/toolkit": {
3010
+ "version": "2.11.1",
3011
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz",
3012
+ "integrity": "sha512-HjhlEREguAyBTGNzRlGNiDHGQ2EjLSPWwdhhpoEqHYy8hWak3Dp6/fU72OfqVsiMb8S6rbfPsWUF24fxpilrVA==",
3013
+ "license": "MIT",
3014
+ "dependencies": {
3015
+ "@standard-schema/spec": "^1.0.0",
3016
+ "@standard-schema/utils": "^0.3.0",
3017
+ "immer": "^11.0.0",
3018
+ "redux": "^5.0.1",
3019
+ "redux-thunk": "^3.1.0",
3020
+ "reselect": "^5.1.0"
3021
+ },
3022
+ "peerDependencies": {
3023
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
3024
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
3025
+ },
3026
+ "peerDependenciesMeta": {
3027
+ "react": {
3028
+ "optional": true
3029
+ },
3030
+ "react-redux": {
3031
+ "optional": true
3032
+ }
3033
+ }
3034
+ },
3035
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
3036
+ "version": "11.0.1",
3037
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz",
3038
+ "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==",
3039
+ "license": "MIT",
3040
+ "funding": {
3041
+ "type": "opencollective",
3042
+ "url": "https://opencollective.com/immer"
3043
+ }
3044
+ },
3045
  "node_modules/@rollup/plugin-babel": {
3046
  "version": "5.3.1",
3047
  "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
 
3157
  "@sinonjs/commons": "^1.7.0"
3158
  }
3159
  },
3160
+ "node_modules/@standard-schema/spec": {
3161
+ "version": "1.0.0",
3162
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
3163
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
3164
+ "license": "MIT"
3165
+ },
3166
+ "node_modules/@standard-schema/utils": {
3167
+ "version": "0.3.0",
3168
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
3169
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
3170
+ "license": "MIT"
3171
+ },
3172
  "node_modules/@surma/rollup-plugin-off-main-thread": {
3173
  "version": "2.2.3",
3174
  "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
 
3624
  "@types/node": "*"
3625
  }
3626
  },
3627
+ "node_modules/@types/d3-array": {
3628
+ "version": "3.2.2",
3629
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
3630
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
3631
+ "license": "MIT"
3632
+ },
3633
+ "node_modules/@types/d3-color": {
3634
+ "version": "3.1.3",
3635
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
3636
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
3637
+ "license": "MIT"
3638
+ },
3639
+ "node_modules/@types/d3-ease": {
3640
+ "version": "3.0.2",
3641
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
3642
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
3643
+ "license": "MIT"
3644
+ },
3645
+ "node_modules/@types/d3-interpolate": {
3646
+ "version": "3.0.4",
3647
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
3648
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
3649
+ "license": "MIT",
3650
+ "dependencies": {
3651
+ "@types/d3-color": "*"
3652
+ }
3653
+ },
3654
+ "node_modules/@types/d3-path": {
3655
+ "version": "3.1.1",
3656
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
3657
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
3658
+ "license": "MIT"
3659
+ },
3660
+ "node_modules/@types/d3-scale": {
3661
+ "version": "4.0.9",
3662
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
3663
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
3664
+ "license": "MIT",
3665
+ "dependencies": {
3666
+ "@types/d3-time": "*"
3667
+ }
3668
+ },
3669
+ "node_modules/@types/d3-shape": {
3670
+ "version": "3.1.7",
3671
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
3672
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
3673
+ "license": "MIT",
3674
+ "dependencies": {
3675
+ "@types/d3-path": "*"
3676
+ }
3677
+ },
3678
+ "node_modules/@types/d3-time": {
3679
+ "version": "3.0.4",
3680
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
3681
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
3682
+ "license": "MIT"
3683
+ },
3684
+ "node_modules/@types/d3-timer": {
3685
+ "version": "3.0.2",
3686
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
3687
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
3688
+ "license": "MIT"
3689
+ },
3690
  "node_modules/@types/eslint": {
3691
  "version": "8.56.12",
3692
  "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
 
3950
  "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
3951
  "license": "MIT"
3952
  },
3953
+ "node_modules/@types/use-sync-external-store": {
3954
+ "version": "0.0.6",
3955
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
3956
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
3957
+ "license": "MIT"
3958
+ },
3959
  "node_modules/@types/ws": {
3960
  "version": "8.18.1",
3961
  "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
 
5734
  "wrap-ansi": "^7.0.0"
5735
  }
5736
  },
5737
+ "node_modules/clsx": {
5738
+ "version": "2.1.1",
5739
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
5740
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
5741
+ "license": "MIT",
5742
+ "engines": {
5743
+ "node": ">=6"
5744
+ }
5745
+ },
5746
  "node_modules/co": {
5747
  "version": "4.6.0",
5748
  "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
 
6481
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6482
  "license": "MIT"
6483
  },
6484
+ "node_modules/d3-array": {
6485
+ "version": "3.2.4",
6486
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
6487
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
6488
+ "license": "ISC",
6489
+ "dependencies": {
6490
+ "internmap": "1 - 2"
6491
+ },
6492
+ "engines": {
6493
+ "node": ">=12"
6494
+ }
6495
+ },
6496
+ "node_modules/d3-color": {
6497
+ "version": "3.1.0",
6498
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
6499
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
6500
+ "license": "ISC",
6501
+ "engines": {
6502
+ "node": ">=12"
6503
+ }
6504
+ },
6505
+ "node_modules/d3-ease": {
6506
+ "version": "3.0.1",
6507
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
6508
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
6509
+ "license": "BSD-3-Clause",
6510
+ "engines": {
6511
+ "node": ">=12"
6512
+ }
6513
+ },
6514
+ "node_modules/d3-format": {
6515
+ "version": "3.1.0",
6516
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
6517
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
6518
+ "license": "ISC",
6519
+ "engines": {
6520
+ "node": ">=12"
6521
+ }
6522
+ },
6523
+ "node_modules/d3-interpolate": {
6524
+ "version": "3.0.1",
6525
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
6526
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
6527
+ "license": "ISC",
6528
+ "dependencies": {
6529
+ "d3-color": "1 - 3"
6530
+ },
6531
+ "engines": {
6532
+ "node": ">=12"
6533
+ }
6534
+ },
6535
+ "node_modules/d3-path": {
6536
+ "version": "3.1.0",
6537
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
6538
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
6539
+ "license": "ISC",
6540
+ "engines": {
6541
+ "node": ">=12"
6542
+ }
6543
+ },
6544
+ "node_modules/d3-scale": {
6545
+ "version": "4.0.2",
6546
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
6547
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
6548
+ "license": "ISC",
6549
+ "dependencies": {
6550
+ "d3-array": "2.10.0 - 3",
6551
+ "d3-format": "1 - 3",
6552
+ "d3-interpolate": "1.2.0 - 3",
6553
+ "d3-time": "2.1.1 - 3",
6554
+ "d3-time-format": "2 - 4"
6555
+ },
6556
+ "engines": {
6557
+ "node": ">=12"
6558
+ }
6559
+ },
6560
+ "node_modules/d3-shape": {
6561
+ "version": "3.2.0",
6562
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
6563
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
6564
+ "license": "ISC",
6565
+ "dependencies": {
6566
+ "d3-path": "^3.1.0"
6567
+ },
6568
+ "engines": {
6569
+ "node": ">=12"
6570
+ }
6571
+ },
6572
+ "node_modules/d3-time": {
6573
+ "version": "3.1.0",
6574
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
6575
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
6576
+ "license": "ISC",
6577
+ "dependencies": {
6578
+ "d3-array": "2 - 3"
6579
+ },
6580
+ "engines": {
6581
+ "node": ">=12"
6582
+ }
6583
+ },
6584
+ "node_modules/d3-time-format": {
6585
+ "version": "4.1.0",
6586
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
6587
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
6588
+ "license": "ISC",
6589
+ "dependencies": {
6590
+ "d3-time": "1 - 3"
6591
+ },
6592
+ "engines": {
6593
+ "node": ">=12"
6594
+ }
6595
+ },
6596
+ "node_modules/d3-timer": {
6597
+ "version": "3.0.1",
6598
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
6599
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
6600
+ "license": "ISC",
6601
+ "engines": {
6602
+ "node": ">=12"
6603
+ }
6604
+ },
6605
  "node_modules/damerau-levenshtein": {
6606
  "version": "1.0.8",
6607
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
 
6696
  "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
6697
  "license": "MIT"
6698
  },
6699
+ "node_modules/decimal.js-light": {
6700
+ "version": "2.5.1",
6701
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
6702
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
6703
+ "license": "MIT"
6704
+ },
6705
  "node_modules/dedent": {
6706
  "version": "0.7.0",
6707
  "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
 
7351
  "url": "https://github.com/sponsors/ljharb"
7352
  }
7353
  },
7354
+ "node_modules/es-toolkit": {
7355
+ "version": "1.43.0",
7356
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
7357
+ "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
7358
+ "license": "MIT",
7359
+ "workspaces": [
7360
+ "docs",
7361
+ "benchmarks"
7362
+ ]
7363
+ },
7364
  "node_modules/escalade": {
7365
  "version": "3.2.0",
7366
  "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
 
9504
  "node": ">= 0.4"
9505
  }
9506
  },
9507
+ "node_modules/internmap": {
9508
+ "version": "2.0.3",
9509
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
9510
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
9511
+ "license": "ISC",
9512
+ "engines": {
9513
+ "node": ">=12"
9514
+ }
9515
+ },
9516
  "node_modules/ipaddr.js": {
9517
  "version": "2.3.0",
9518
  "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
 
14063
  "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
14064
  "license": "MIT"
14065
  },
14066
+ "node_modules/react-redux": {
14067
+ "version": "9.2.0",
14068
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
14069
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
14070
+ "license": "MIT",
14071
+ "dependencies": {
14072
+ "@types/use-sync-external-store": "^0.0.6",
14073
+ "use-sync-external-store": "^1.4.0"
14074
+ },
14075
+ "peerDependencies": {
14076
+ "@types/react": "^18.2.25 || ^19",
14077
+ "react": "^18.0 || ^19",
14078
+ "redux": "^5.0.0"
14079
+ },
14080
+ "peerDependenciesMeta": {
14081
+ "@types/react": {
14082
+ "optional": true
14083
+ },
14084
+ "redux": {
14085
+ "optional": true
14086
+ }
14087
+ }
14088
+ },
14089
  "node_modules/react-refresh": {
14090
  "version": "0.11.0",
14091
  "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
 
14254
  "node": ">=8.10.0"
14255
  }
14256
  },
14257
+ "node_modules/recharts": {
14258
+ "version": "3.5.1",
14259
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.1.tgz",
14260
+ "integrity": "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA==",
14261
+ "license": "MIT",
14262
+ "workspaces": [
14263
+ "www"
14264
+ ],
14265
+ "dependencies": {
14266
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
14267
+ "clsx": "^2.1.1",
14268
+ "decimal.js-light": "^2.5.1",
14269
+ "es-toolkit": "^1.39.3",
14270
+ "eventemitter3": "^5.0.1",
14271
+ "immer": "^10.1.1",
14272
+ "react-redux": "8.x.x || 9.x.x",
14273
+ "reselect": "5.1.1",
14274
+ "tiny-invariant": "^1.3.3",
14275
+ "use-sync-external-store": "^1.2.2",
14276
+ "victory-vendor": "^37.0.2"
14277
+ },
14278
+ "engines": {
14279
+ "node": ">=18"
14280
+ },
14281
+ "peerDependencies": {
14282
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
14283
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
14284
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
14285
+ }
14286
+ },
14287
+ "node_modules/recharts/node_modules/eventemitter3": {
14288
+ "version": "5.0.1",
14289
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
14290
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
14291
+ "license": "MIT"
14292
+ },
14293
+ "node_modules/recharts/node_modules/immer": {
14294
+ "version": "10.2.0",
14295
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
14296
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
14297
+ "license": "MIT",
14298
+ "funding": {
14299
+ "type": "opencollective",
14300
+ "url": "https://opencollective.com/immer"
14301
+ }
14302
+ },
14303
  "node_modules/recursive-readdir": {
14304
  "version": "2.2.3",
14305
  "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
 
14325
  "node": ">=8"
14326
  }
14327
  },
14328
+ "node_modules/redux": {
14329
+ "version": "5.0.1",
14330
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
14331
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
14332
+ "license": "MIT"
14333
+ },
14334
+ "node_modules/redux-thunk": {
14335
+ "version": "3.1.0",
14336
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
14337
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
14338
+ "license": "MIT",
14339
+ "peerDependencies": {
14340
+ "redux": "^5.0.0"
14341
+ }
14342
+ },
14343
  "node_modules/reflect.getprototypeof": {
14344
  "version": "1.0.10",
14345
  "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
 
14493
  "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
14494
  "license": "MIT"
14495
  },
14496
+ "node_modules/reselect": {
14497
+ "version": "5.1.1",
14498
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
14499
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
14500
+ "license": "MIT"
14501
+ },
14502
  "node_modules/resolve": {
14503
  "version": "1.22.11",
14504
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
 
16443
  "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
16444
  "license": "MIT"
16445
  },
16446
+ "node_modules/tiny-invariant": {
16447
+ "version": "1.3.3",
16448
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
16449
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
16450
+ "license": "MIT"
16451
+ },
16452
  "node_modules/tinyglobby": {
16453
  "version": "0.2.15",
16454
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
 
17004
  "requires-port": "^1.0.0"
17005
  }
17006
  },
17007
+ "node_modules/use-sync-external-store": {
17008
+ "version": "1.6.0",
17009
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
17010
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
17011
+ "license": "MIT",
17012
+ "peerDependencies": {
17013
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
17014
+ }
17015
+ },
17016
  "node_modules/util-deprecate": {
17017
  "version": "1.0.2",
17018
  "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
 
17095
  "node": ">= 0.8"
17096
  }
17097
  },
17098
+ "node_modules/victory-vendor": {
17099
+ "version": "37.3.6",
17100
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
17101
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
17102
+ "license": "MIT AND ISC",
17103
+ "dependencies": {
17104
+ "@types/d3-array": "^3.0.3",
17105
+ "@types/d3-ease": "^3.0.0",
17106
+ "@types/d3-interpolate": "^3.0.1",
17107
+ "@types/d3-scale": "^4.0.2",
17108
+ "@types/d3-shape": "^3.1.0",
17109
+ "@types/d3-time": "^3.0.0",
17110
+ "@types/d3-timer": "^3.0.0",
17111
+ "d3-array": "^3.1.6",
17112
+ "d3-ease": "^3.0.1",
17113
+ "d3-interpolate": "^3.0.1",
17114
+ "d3-scale": "^4.0.2",
17115
+ "d3-shape": "^3.1.0",
17116
+ "d3-time": "^3.0.0",
17117
+ "d3-timer": "^3.0.1"
17118
+ }
17119
+ },
17120
  "node_modules/w3c-hr-time": {
17121
  "version": "1.0.2",
17122
  "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
package.json CHANGED
@@ -12,6 +12,7 @@
12
  "react-dom": "^19.1.0",
13
  "react-router-dom": "^7.10.1",
14
  "react-scripts": "5.0.1",
 
15
  "web-vitals": "^2.1.4"
16
  },
17
  "devDependencies": {
 
12
  "react-dom": "^19.1.0",
13
  "react-router-dom": "^7.10.1",
14
  "react-scripts": "5.0.1",
15
+ "recharts": "^3.5.1",
16
  "web-vitals": "^2.1.4"
17
  },
18
  "devDependencies": {
src/app/components/analysis/StanceDistributionChart.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
3
+ import type { StanceStats } from '../../utils/analysis.utils.ts';
4
+
5
+ type StanceDistributionChartProps = {
6
+ stats: StanceStats;
7
+ };
8
+
9
+ const COLORS = {
10
+ PRO: '#10b981', // green-500
11
+ CON: '#ef4444', // red-500
12
+ };
13
+
14
+ const StanceDistributionChart: React.FC<StanceDistributionChartProps> = ({ stats }) => {
15
+ const data = [
16
+ { name: 'PRO', value: stats.pro, percentage: stats.proPercentage },
17
+ { name: 'CON', value: stats.con, percentage: stats.conPercentage },
18
+ ];
19
+
20
+ return (
21
+ <div className="h-64 w-full">
22
+ <ResponsiveContainer width="100%" height="100%">
23
+ <PieChart>
24
+ <Pie
25
+ data={data}
26
+ cx="50%"
27
+ cy="50%"
28
+ labelLine={false}
29
+ label={({ name, percentage }) => `${name}: ${percentage.toFixed(1)}%`}
30
+ outerRadius={80}
31
+ fill="#8884d8"
32
+ dataKey="value"
33
+ >
34
+ {data.map((entry, index) => (
35
+ <Cell
36
+ key={`cell-${index}`}
37
+ fill={entry.name === 'PRO' ? COLORS.PRO : COLORS.CON}
38
+ />
39
+ ))}
40
+ </Pie>
41
+ <Tooltip
42
+ formatter={(value: number, name: string, props: any) => [
43
+ `${value} (${props.payload.percentage.toFixed(1)}%)`,
44
+ name,
45
+ ]}
46
+ />
47
+ <Legend />
48
+ </PieChart>
49
+ </ResponsiveContainer>
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export default StanceDistributionChart;
55
+
src/app/components/analysis/TimeSeriesChart.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
3
+ import type { TimeStats } from '../../utils/analysis.utils.ts';
4
+
5
+ type TimeSeriesChartProps = {
6
+ data: TimeStats[];
7
+ };
8
+
9
+ const TimeSeriesChart: React.FC<TimeSeriesChartProps> = ({ data }) => {
10
+ // Format date for display
11
+ const chartData = data.map((item) => ({
12
+ ...item,
13
+ dateDisplay: new Date(item.date).toLocaleDateString('en-US', {
14
+ month: 'short',
15
+ day: 'numeric',
16
+ }),
17
+ }));
18
+
19
+ return (
20
+ <div className="h-64 w-full">
21
+ <ResponsiveContainer width="100%" height="100%">
22
+ <LineChart
23
+ data={chartData}
24
+ margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
25
+ >
26
+ <CartesianGrid strokeDasharray="3 3" />
27
+ <XAxis dataKey="dateDisplay" />
28
+ <YAxis />
29
+ <Tooltip />
30
+ <Legend />
31
+ <Line
32
+ type="monotone"
33
+ dataKey="count"
34
+ stroke="#3b82f6"
35
+ strokeWidth={2}
36
+ name="Arguments Analyzed"
37
+ />
38
+ </LineChart>
39
+ </ResponsiveContainer>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default TimeSeriesChart;
45
+
src/app/components/analysis/TopicFrequencyChart.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
3
+ import type { TopicFrequency } from '../../utils/analysis.utils.ts';
4
+
5
+ type TopicFrequencyChartProps = {
6
+ data: TopicFrequency[];
7
+ };
8
+
9
+ const TopicFrequencyChart: React.FC<TopicFrequencyChartProps> = ({ data }) => {
10
+ // Truncate long topic names for display
11
+ const chartData = data.map((item) => ({
12
+ ...item,
13
+ topicDisplay: item.topic.length > 40 ? `${item.topic.substring(0, 40)}...` : item.topic,
14
+ }));
15
+
16
+ return (
17
+ <div className="h-96 w-full">
18
+ <ResponsiveContainer width="100%" height="100%">
19
+ <BarChart
20
+ layout="vertical"
21
+ data={chartData}
22
+ margin={{ top: 5, right: 30, left: 150, bottom: 5 }}
23
+ >
24
+ <CartesianGrid strokeDasharray="3 3" />
25
+ <XAxis type="number" />
26
+ <YAxis
27
+ type="category"
28
+ dataKey="topicDisplay"
29
+ width={140}
30
+ style={{ fontSize: '12px' }}
31
+ />
32
+ <Tooltip
33
+ formatter={(value: number, name: string) => [value, name === 'proCount' ? 'PRO' : name === 'conCount' ? 'CON' : 'Total']}
34
+ contentStyle={{ fontSize: '12px' }}
35
+ />
36
+ <Legend
37
+ formatter={(value) => (value === 'proCount' ? 'PRO' : value === 'conCount' ? 'CON' : 'Total')}
38
+ />
39
+ <Bar dataKey="proCount" fill="#10b981" name="proCount" />
40
+ <Bar dataKey="conCount" fill="#ef4444" name="conCount" />
41
+ </BarChart>
42
+ </ResponsiveContainer>
43
+ </div>
44
+ );
45
+ };
46
+
47
+ export default TopicFrequencyChart;
48
+
src/app/pages/AnalysisPage.tsx CHANGED
@@ -1,8 +1,21 @@
1
- import React, { useMemo, useState } from 'react';
2
  import { parseCsv, formatError } from '../utils/index.ts';
3
  import { CsvRow } from '../types/index.ts';
4
  import { AnalysisResult } from '../types/analysis.types.ts';
5
- import { analyzeArgumentsFromCsv } from '../services/analysis.service.ts';
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  const AnalysisPage: React.FC = () => {
8
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
@@ -11,12 +24,65 @@ const AnalysisPage: React.FC = () => {
11
  const [error, setError] = useState<string | null>(null);
12
  const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
13
  const [analysisResults, setAnalysisResults] = useState<AnalysisResult[]>([]);
 
 
 
 
 
14
 
15
  const fileName = useMemo(
16
  () => selectedFile?.name ?? 'Select a CSV file with arguments',
17
  [selectedFile]
18
  );
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
21
  const file = event.target.files?.[0] ?? null;
22
  setSelectedFile(file);
@@ -62,6 +128,9 @@ const AnalysisPage: React.FC = () => {
62
  try {
63
  const response = await analyzeArgumentsFromCsv(selectedFile);
64
  setAnalysisResults(response.results);
 
 
 
65
  } catch (err) {
66
  setError(formatError(err));
67
  } finally {
@@ -71,17 +140,121 @@ const AnalysisPage: React.FC = () => {
71
 
72
  return (
73
  <div className="min-h-screen bg-slate-50 px-4 py-10">
74
- <div className="mx-auto max-w-4xl rounded-lg border border-slate-200 bg-white shadow-sm">
75
- <div className="border-b border-slate-200 px-6 py-4">
76
- <h1 className="text-lg font-semibold text-slate-800">
77
- Analysis page (open static)
78
- </h1>
79
- <p className="text-sm text-slate-500">
80
- Upload a CSV file containing arguments to preview its contents.
81
- </p>
82
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- <div className="space-y-6 p-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  <div className="flex flex-col gap-3">
86
  <label className="text-sm font-medium text-slate-700">
87
  Upload a CSV file
@@ -219,6 +392,7 @@ const AnalysisPage: React.FC = () => {
219
  </div>
220
  </div>
221
  )}
 
222
  </div>
223
  </div>
224
  </div>
 
1
+ import React, { useMemo, useState, useEffect } from 'react';
2
  import { parseCsv, formatError } from '../utils/index.ts';
3
  import { CsvRow } from '../types/index.ts';
4
  import { AnalysisResult } from '../types/analysis.types.ts';
5
+ import { analyzeArgumentsFromCsv, getAnalysisResults } from '../services/analysis.service.ts';
6
+ import {
7
+ calculateStanceStats,
8
+ calculateTopicFrequency,
9
+ calculateTimeStats,
10
+ getUniqueTopicsCount,
11
+ getAverageArgumentLength,
12
+ getMostRecentAnalysisDate,
13
+ getOldestAnalysisDate,
14
+ } from '../utils/analysis.utils.ts';
15
+ import StanceDistributionChart from '../components/analysis/StanceDistributionChart.tsx';
16
+ import TopicFrequencyChart from '../components/analysis/TopicFrequencyChart.tsx';
17
+ import TimeSeriesChart from '../components/analysis/TimeSeriesChart.tsx';
18
+ import Loading from '../components/common/Loading.tsx';
19
 
20
  const AnalysisPage: React.FC = () => {
21
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
 
24
  const [error, setError] = useState<string | null>(null);
25
  const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
26
  const [analysisResults, setAnalysisResults] = useState<AnalysisResult[]>([]);
27
+
28
+ // User's historical analysis data
29
+ const [userAnalysisData, setUserAnalysisData] = useState<AnalysisResult[]>([]);
30
+ const [isLoadingStats, setIsLoadingStats] = useState<boolean>(true);
31
+ const [statsError, setStatsError] = useState<string | null>(null);
32
 
33
  const fileName = useMemo(
34
  () => selectedFile?.name ?? 'Select a CSV file with arguments',
35
  [selectedFile]
36
  );
37
 
38
+ // Fetch user's analysis data on mount
39
+ useEffect(() => {
40
+ const fetchUserAnalysis = async () => {
41
+ setIsLoadingStats(true);
42
+ setStatsError(null);
43
+ try {
44
+ const response = await getAnalysisResults(1000, 0);
45
+ setUserAnalysisData(response.results);
46
+ } catch (err) {
47
+ setStatsError(formatError(err));
48
+ } finally {
49
+ setIsLoadingStats(false);
50
+ }
51
+ };
52
+
53
+ fetchUserAnalysis();
54
+ }, []);
55
+
56
+ // Calculate statistics from user's analysis data
57
+ const stanceStats = useMemo(
58
+ () => calculateStanceStats(userAnalysisData),
59
+ [userAnalysisData]
60
+ );
61
+ const topicFrequency = useMemo(
62
+ () => calculateTopicFrequency(userAnalysisData, 10),
63
+ [userAnalysisData]
64
+ );
65
+ const timeStats = useMemo(
66
+ () => calculateTimeStats(userAnalysisData),
67
+ [userAnalysisData]
68
+ );
69
+ const uniqueTopicsCount = useMemo(
70
+ () => getUniqueTopicsCount(userAnalysisData),
71
+ [userAnalysisData]
72
+ );
73
+ const avgArgumentLength = useMemo(
74
+ () => getAverageArgumentLength(userAnalysisData),
75
+ [userAnalysisData]
76
+ );
77
+ const mostRecentDate = useMemo(
78
+ () => getMostRecentAnalysisDate(userAnalysisData),
79
+ [userAnalysisData]
80
+ );
81
+ const oldestDate = useMemo(
82
+ () => getOldestAnalysisDate(userAnalysisData),
83
+ [userAnalysisData]
84
+ );
85
+
86
  const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
87
  const file = event.target.files?.[0] ?? null;
88
  setSelectedFile(file);
 
128
  try {
129
  const response = await analyzeArgumentsFromCsv(selectedFile);
130
  setAnalysisResults(response.results);
131
+ // Refresh user analysis data after successful analysis
132
+ const updatedResponse = await getAnalysisResults(1000, 0);
133
+ setUserAnalysisData(updatedResponse.results);
134
  } catch (err) {
135
  setError(formatError(err));
136
  } finally {
 
140
 
141
  return (
142
  <div className="min-h-screen bg-slate-50 px-4 py-10">
143
+ <div className="mx-auto max-w-7xl space-y-6">
144
+ {/* Statistics Section */}
145
+ {!isLoadingStats && userAnalysisData.length > 0 && (
146
+ <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
147
+ <div className="border-b border-slate-200 px-6 py-4">
148
+ <h2 className="text-lg font-semibold text-slate-800">
149
+ Your Analysis Statistics
150
+ </h2>
151
+ <p className="text-sm text-slate-500">
152
+ Overview of all your analyzed arguments
153
+ </p>
154
+ </div>
155
+
156
+ {isLoadingStats ? (
157
+ <div className="flex items-center justify-center py-12">
158
+ <Loading />
159
+ </div>
160
+ ) : statsError ? (
161
+ <div className="px-6 py-4">
162
+ <p className="text-sm text-red-600">{statsError}</p>
163
+ </div>
164
+ ) : (
165
+ <div className="p-6 space-y-8">
166
+ {/* Summary Statistics */}
167
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
168
+ <div className="bg-slate-50 rounded-lg p-4">
169
+ <div className="text-2xl font-bold text-slate-800">
170
+ {stanceStats.total}
171
+ </div>
172
+ <div className="text-sm text-slate-600">Total Arguments</div>
173
+ </div>
174
+ <div className="bg-slate-50 rounded-lg p-4">
175
+ <div className="text-2xl font-bold text-slate-800">
176
+ {uniqueTopicsCount}
177
+ </div>
178
+ <div className="text-sm text-slate-600">Unique Topics</div>
179
+ </div>
180
+ <div className="bg-slate-50 rounded-lg p-4">
181
+ <div className="text-2xl font-bold text-slate-800">
182
+ {avgArgumentLength}
183
+ </div>
184
+ <div className="text-sm text-slate-600">Avg Length (chars)</div>
185
+ </div>
186
+ <div className="bg-slate-50 rounded-lg p-4">
187
+ <div className="text-2xl font-bold text-slate-800">
188
+ {stanceStats.pro}
189
+ </div>
190
+ <div className="text-sm text-slate-600">PRO Arguments</div>
191
+ </div>
192
+ </div>
193
+
194
+ {/* Charts Grid */}
195
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
196
+ {/* Stance Distribution */}
197
+ <div className="rounded-lg border border-slate-200 p-4">
198
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">
199
+ Stance Distribution
200
+ </h3>
201
+ <StanceDistributionChart stats={stanceStats} />
202
+ <div className="mt-4 flex justify-center gap-6 text-sm">
203
+ <div>
204
+ <span className="font-semibold text-green-700">
205
+ PRO: {stanceStats.pro} ({stanceStats.proPercentage.toFixed(1)}%)
206
+ </span>
207
+ </div>
208
+ <div>
209
+ <span className="font-semibold text-red-700">
210
+ CON: {stanceStats.con} ({stanceStats.conPercentage.toFixed(1)}%)
211
+ </span>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ {/* Time Series */}
217
+ {timeStats.length > 0 && (
218
+ <div className="rounded-lg border border-slate-200 p-4">
219
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">
220
+ Analysis Timeline
221
+ </h3>
222
+ <TimeSeriesChart data={timeStats} />
223
+ {oldestDate && mostRecentDate && (
224
+ <div className="mt-4 text-center text-xs text-slate-500">
225
+ From {oldestDate} to {mostRecentDate}
226
+ </div>
227
+ )}
228
+ </div>
229
+ )}
230
+ </div>
231
 
232
+ {/* Topic Frequency */}
233
+ {topicFrequency.length > 0 && (
234
+ <div className="rounded-lg border border-slate-200 p-4">
235
+ <h3 className="text-sm font-semibold text-slate-700 mb-4">
236
+ Most Discussed Topics (Top 10)
237
+ </h3>
238
+ <TopicFrequencyChart data={topicFrequency} />
239
+ </div>
240
+ )}
241
+ </div>
242
+ )}
243
+ </div>
244
+ )}
245
+
246
+ {/* CSV Upload and Analysis Section */}
247
+ <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
248
+ <div className="border-b border-slate-200 px-6 py-4">
249
+ <h1 className="text-lg font-semibold text-slate-800">
250
+ Analyze New Arguments
251
+ </h1>
252
+ <p className="text-sm text-slate-500">
253
+ Upload a CSV file containing arguments to analyze them.
254
+ </p>
255
+ </div>
256
+
257
+ <div className="space-y-6 p-6">
258
  <div className="flex flex-col gap-3">
259
  <label className="text-sm font-medium text-slate-700">
260
  Upload a CSV file
 
392
  </div>
393
  </div>
394
  )}
395
+ </div>
396
  </div>
397
  </div>
398
  </div>
src/app/utils/analysis.utils.ts ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Utility functions for analyzing analysis results and calculating statistics
3
+ */
4
+
5
+ import type { AnalysisResult } from '../types/analysis.types.ts';
6
+
7
+ /**
8
+ * Statistics about stance distribution
9
+ */
10
+ export type StanceStats = {
11
+ total: number;
12
+ pro: number;
13
+ con: number;
14
+ proPercentage: number;
15
+ conPercentage: number;
16
+ };
17
+
18
+ /**
19
+ * Topic frequency data
20
+ */
21
+ export type TopicFrequency = {
22
+ topic: string;
23
+ count: number;
24
+ proCount: number;
25
+ conCount: number;
26
+ };
27
+
28
+ /**
29
+ * Time-based statistics
30
+ */
31
+ export type TimeStats = {
32
+ date: string;
33
+ count: number;
34
+ };
35
+
36
+ /**
37
+ * Calculate stance distribution statistics
38
+ */
39
+ export function calculateStanceStats(results: AnalysisResult[]): StanceStats {
40
+ const total = results.length;
41
+ const pro = results.filter((r) => r.predicted_stance === 'PRO').length;
42
+ const con = results.filter((r) => r.predicted_stance === 'CON').length;
43
+
44
+ return {
45
+ total,
46
+ pro,
47
+ con,
48
+ proPercentage: total > 0 ? (pro / total) * 100 : 0,
49
+ conPercentage: total > 0 ? (con / total) * 100 : 0,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Calculate topic frequency statistics
55
+ */
56
+ export function calculateTopicFrequency(
57
+ results: AnalysisResult[],
58
+ limit: number = 10
59
+ ): TopicFrequency[] {
60
+ const topicMap = new Map<string, { count: number; proCount: number; conCount: number }>();
61
+
62
+ results.forEach((result) => {
63
+ const topic = result.topic.trim();
64
+ if (!topic) return;
65
+
66
+ const existing = topicMap.get(topic) || { count: 0, proCount: 0, conCount: 0 };
67
+ existing.count += 1;
68
+ if (result.predicted_stance === 'PRO') {
69
+ existing.proCount += 1;
70
+ } else {
71
+ existing.conCount += 1;
72
+ }
73
+ topicMap.set(topic, existing);
74
+ });
75
+
76
+ return Array.from(topicMap.entries())
77
+ .map(([topic, stats]) => ({
78
+ topic,
79
+ count: stats.count,
80
+ proCount: stats.proCount,
81
+ conCount: stats.conCount,
82
+ }))
83
+ .sort((a, b) => b.count - a.count)
84
+ .slice(0, limit);
85
+ }
86
+
87
+ /**
88
+ * Calculate time-based statistics (grouped by date)
89
+ */
90
+ export function calculateTimeStats(results: AnalysisResult[]): TimeStats[] {
91
+ const dateMap = new Map<string, number>();
92
+
93
+ results.forEach((result) => {
94
+ const date = new Date(result.created_at).toISOString().split('T')[0];
95
+ dateMap.set(date, (dateMap.get(date) || 0) + 1);
96
+ });
97
+
98
+ return Array.from(dateMap.entries())
99
+ .map(([date, count]) => ({ date, count }))
100
+ .sort((a, b) => a.date.localeCompare(b.date));
101
+ }
102
+
103
+ /**
104
+ * Get unique topics count
105
+ */
106
+ export function getUniqueTopicsCount(results: AnalysisResult[]): number {
107
+ const uniqueTopics = new Set(results.map((r) => r.topic.trim()).filter((t) => t));
108
+ return uniqueTopics.size;
109
+ }
110
+
111
+ /**
112
+ * Get average argument length (in characters)
113
+ */
114
+ export function getAverageArgumentLength(results: AnalysisResult[]): number {
115
+ if (results.length === 0) return 0;
116
+ const totalLength = results.reduce((sum, r) => sum + r.argument.length, 0);
117
+ return Math.round(totalLength / results.length);
118
+ }
119
+
120
+ /**
121
+ * Get most recent analysis date
122
+ */
123
+ export function getMostRecentAnalysisDate(results: AnalysisResult[]): string | null {
124
+ if (results.length === 0) return null;
125
+ const dates = results.map((r) => new Date(r.created_at).getTime());
126
+ const mostRecent = new Date(Math.max(...dates));
127
+ return mostRecent.toISOString().split('T')[0];
128
+ }
129
+
130
+ /**
131
+ * Get oldest analysis date
132
+ */
133
+ export function getOldestAnalysisDate(results: AnalysisResult[]): string | null {
134
+ if (results.length === 0) return null;
135
+ const dates = results.map((r) => new Date(r.created_at).getTime());
136
+ const oldest = new Date(Math.min(...dates));
137
+ return oldest.toISOString().split('T')[0];
138
+ }
139
+
src/app/utils/index.ts CHANGED
@@ -7,6 +7,9 @@ import { CsvParseResult, CsvRow } from '../types/index.ts';
7
  // Export user utilities
8
  export * from './user.utils.ts';
9
 
 
 
 
10
  /**
11
  * Debounce function - delays execution until after wait time
12
  */
 
7
  // Export user utilities
8
  export * from './user.utils.ts';
9
 
10
+ // Export analysis utilities
11
+ export * from './analysis.utils.ts';
12
+
13
  /**
14
  * Debounce function - delays execution until after wait time
15
  */