Rom89823974978 commited on
Commit
f16cb34
·
1 Parent(s): 4b2beb8
frontend/src/components/Dashboard.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { useState, useEffect, useRef } from "react";//React,
 
2
  import {
3
  Box,
4
  Grid,
@@ -45,12 +46,14 @@ ChartJS.register(
45
 
46
  interface ChartData { labels: string[]; values: number[]; }
47
  interface Stats { [key: string]: ChartData; }
 
48
  const FILTER_LABELS: Record<keyof FilterState, string> = {
49
  status: "Status",
50
  organization: "Organization",
51
  country: "Country",
52
  legalBasis: "Legal Basis",
53
  };
 
54
  interface DashboardProps {
55
  stats: Stats;
56
  filters: FilterState;
@@ -70,27 +73,16 @@ const Dashboard: React.FC<DashboardProps> = ({
70
  const [orgInput, setOrgInput] = useState("");
71
  const [statsData, setStatsData] = useState<Stats>(initialStats);
72
  const [loadingStats, setLoadingStats] = useState(false);
73
-
74
- // ref to hold our debounce timer
75
  const fetchTimer = useRef<number | null>(null);
76
 
 
77
  useEffect(() => {
78
- // clear any pending fetch
79
- if (fetchTimer.current) {
80
- clearTimeout(fetchTimer.current);
81
- }
82
-
83
- // schedule new fetch 300ms after last filter change
84
  fetchTimer.current = window.setTimeout(() => {
85
  const qs = new URLSearchParams();
86
- if (filters.status) qs.set("status", filters.status);
87
- if (filters.organization) qs.set("organization", filters.organization);
88
- if (filters.country) qs.set("country", filters.country);
89
- if (filters.legalBasis) qs.set("legalBasis", filters.legalBasis);
90
- qs.set("minYear", filters.minYear);
91
- qs.set("maxYear", filters.maxYear);
92
- qs.set("minFunding", filters.minFunding);
93
- qs.set("maxFunding", filters.maxFunding);
94
 
95
  setLoadingStats(true);
96
  fetch(`/api/stats?${qs.toString()}`)
@@ -100,29 +92,19 @@ const Dashboard: React.FC<DashboardProps> = ({
100
  .finally(() => setLoadingStats(false));
101
  }, 300);
102
 
103
- // cleanup on unmount or next effect-run
104
  return () => {
105
- if (fetchTimer.current) {
106
- clearTimeout(fetchTimer.current);
107
- }
108
  };
109
- }, [
110
- filters.status,
111
- filters.organization,
112
- filters.country,
113
- filters.legalBasis,
114
- filters.minYear,
115
- filters.maxYear,
116
- filters.minFunding,
117
- filters.maxFunding,
118
- ]);
119
 
120
- const updateFilter = (key: keyof FilterState) => (opt: { value: string } | null) =>
121
- setFilters(prev => ({ ...prev, [key]: opt?.value || "" }));
 
 
122
 
123
  const updateSlider = (
124
- k1: "minYear" | "minFunding",
125
- k2: "maxYear" | "maxFunding"
126
  ) => ([min, max]: number[]) =>
127
  setFilters(prev => ({
128
  ...prev,
@@ -131,13 +113,9 @@ const Dashboard: React.FC<DashboardProps> = ({
131
  }));
132
 
133
  const filterKeys: Array<keyof FilterState> = [
134
- "status",
135
- "organization",
136
- "country",
137
- "legalBasis",
138
  ];
139
 
140
- // initial blank-state spinner
141
  if (loadingStats && !Object.keys(statsData).length) {
142
  return (
143
  <Flex justify="center" mt={10}>
@@ -152,26 +130,21 @@ const Dashboard: React.FC<DashboardProps> = ({
152
  <Box borderWidth="1px" borderRadius="lg" p={4} mb={6} bg="gray.50">
153
  <Grid
154
  templateColumns={{
155
- base: "repeat(1,1fr)",
156
- sm: "repeat(2,1fr)",
157
- md: "repeat(3,1fr)",
158
- lg: "repeat(6,1fr)",
159
  }}
160
  gap={4}
161
- columnGap={6}
162
  >
163
- {filterKeys.map((key) => {
164
- const isOrg = key === "organization";
165
- const opts =
166
- availableFilters[
167
- key === "status"
168
- ? "statuses"
169
- : key === "organization"
170
- ? "organizations"
171
- : key === "country"
172
- ? "countries"
173
- : "legalBases"
174
- ] || [];
175
 
176
  return (
177
  <GridItem key={key} colSpan={1}>
@@ -180,17 +153,13 @@ const Dashboard: React.FC<DashboardProps> = ({
180
  </Text>
181
  <Select
182
  options={opts.map(v => ({ label: v, value: v }))}
183
- placeholder={`Select ${key}`}
184
  onChange={updateFilter(key)}
185
  isClearable
186
  isSearchable
187
- openMenuOnClick
188
- openMenuOnFocus
189
  {...(isOrg && {
190
- openMenuOnClick: false,
191
- openMenuOnFocus: false,
192
  menuIsOpen: orgInput.length > 0,
193
- onInputChange: (str: string) => setOrgInput(str),
194
  })}
195
  />
196
  </GridItem>
@@ -198,7 +167,7 @@ const Dashboard: React.FC<DashboardProps> = ({
198
  })}
199
 
200
  {/* Year Range */}
201
- <GridItem colSpan={{ base: 1, md: 3 }}>
202
  <Box mb={6}>
203
  <Flex justify="space-between" mb={1}>
204
  <Text fontSize="sm" fontWeight="medium">Year Range</Text>
@@ -216,7 +185,7 @@ const Dashboard: React.FC<DashboardProps> = ({
216
  size="md"
217
  >
218
  <RangeSliderTrack>
219
- <RangeSliderFilledTrack bg="brand.blue" />
220
  </RangeSliderTrack>
221
  <RangeSliderThumb index={0} boxSize={4}/>
222
  <RangeSliderThumb index={1} boxSize={4}/>
@@ -225,7 +194,7 @@ const Dashboard: React.FC<DashboardProps> = ({
225
  </GridItem>
226
 
227
  {/* Funding Range */}
228
- <GridItem colSpan={{ base: 1, md: 3 }}>
229
  <Box>
230
  <Flex justify="space-between" mb={1}>
231
  <Text fontSize="sm" fontWeight="medium">Funding (€)</Text>
@@ -236,14 +205,14 @@ const Dashboard: React.FC<DashboardProps> = ({
236
  <RangeSlider
237
  aria-label={["Min Funding","Max Funding"]}
238
  min={0}
239
- max={10_000_000}
240
- step={100_000}
241
  defaultValue={[+filters.minFunding, +filters.maxFunding]}
242
  onChange={updateSlider("minFunding","maxFunding")}
243
  size="md"
244
  >
245
  <RangeSliderTrack>
246
- <RangeSliderFilledTrack bg="brand.blue" />
247
  </RangeSliderTrack>
248
  <RangeSliderThumb index={0} boxSize={4}/>
249
  <RangeSliderThumb index={1} boxSize={4}/>
 
1
+ // src/components/Dashboard.tsx
2
+ import { useState, useEffect, useRef } from "react";
3
  import {
4
  Box,
5
  Grid,
 
46
 
47
  interface ChartData { labels: string[]; values: number[]; }
48
  interface Stats { [key: string]: ChartData; }
49
+
50
  const FILTER_LABELS: Record<keyof FilterState, string> = {
51
  status: "Status",
52
  organization: "Organization",
53
  country: "Country",
54
  legalBasis: "Legal Basis",
55
  };
56
+
57
  interface DashboardProps {
58
  stats: Stats;
59
  filters: FilterState;
 
73
  const [orgInput, setOrgInput] = useState("");
74
  const [statsData, setStatsData] = useState<Stats>(initialStats);
75
  const [loadingStats, setLoadingStats] = useState(false);
 
 
76
  const fetchTimer = useRef<number | null>(null);
77
 
78
+ // Debounced stats fetch
79
  useEffect(() => {
80
+ if (fetchTimer.current) clearTimeout(fetchTimer.current);
 
 
 
 
 
81
  fetchTimer.current = window.setTimeout(() => {
82
  const qs = new URLSearchParams();
83
+ Object.entries(filters).forEach(([key, val]) => {
84
+ if (val) qs.set(key, val);
85
+ });
 
 
 
 
 
86
 
87
  setLoadingStats(true);
88
  fetch(`/api/stats?${qs.toString()}`)
 
92
  .finally(() => setLoadingStats(false));
93
  }, 300);
94
 
 
95
  return () => {
96
+ if (fetchTimer.current) clearTimeout(fetchTimer.current);
 
 
97
  };
98
+ }, [filters]);
 
 
 
 
 
 
 
 
 
99
 
100
+ // Select & slider updaters
101
+ const updateFilter = (key: keyof FilterState) =>
102
+ (opt: { value: string } | null) =>
103
+ setFilters(prev => ({ ...prev, [key]: opt?.value || "" }));
104
 
105
  const updateSlider = (
106
+ k1: 'minYear' | 'minFunding',
107
+ k2: 'maxYear' | 'maxFunding'
108
  ) => ([min, max]: number[]) =>
109
  setFilters(prev => ({
110
  ...prev,
 
113
  }));
114
 
115
  const filterKeys: Array<keyof FilterState> = [
116
+ 'status', 'organization', 'country', 'legalBasis'
 
 
 
117
  ];
118
 
 
119
  if (loadingStats && !Object.keys(statsData).length) {
120
  return (
121
  <Flex justify="center" mt={10}>
 
130
  <Box borderWidth="1px" borderRadius="lg" p={4} mb={6} bg="gray.50">
131
  <Grid
132
  templateColumns={{
133
+ base: '1fr',
134
+ sm: 'repeat(2,1fr)',
135
+ md: 'repeat(4,1fr)',
136
+ lg: 'repeat(6,1fr)',
137
  }}
138
  gap={4}
 
139
  >
140
+ {filterKeys.map(key => {
141
+ const opts = availableFilters[
142
+ key === 'status' ? 'statuses'
143
+ : key === 'organization' ? 'organizations'
144
+ : key === 'country' ? 'countries'
145
+ : 'legalBases'
146
+ ] || [];
147
+ const isOrg = key === 'organization';
 
 
 
 
148
 
149
  return (
150
  <GridItem key={key} colSpan={1}>
 
153
  </Text>
154
  <Select
155
  options={opts.map(v => ({ label: v, value: v }))}
156
+ placeholder={FILTER_LABELS[key]}
157
  onChange={updateFilter(key)}
158
  isClearable
159
  isSearchable
 
 
160
  {...(isOrg && {
 
 
161
  menuIsOpen: orgInput.length > 0,
162
+ onInputChange: setOrgInput,
163
  })}
164
  />
165
  </GridItem>
 
167
  })}
168
 
169
  {/* Year Range */}
170
+ <GridItem colSpan={{ base: 1, md: 2 }}>
171
  <Box mb={6}>
172
  <Flex justify="space-between" mb={1}>
173
  <Text fontSize="sm" fontWeight="medium">Year Range</Text>
 
185
  size="md"
186
  >
187
  <RangeSliderTrack>
188
+ <RangeSliderFilledTrack />
189
  </RangeSliderTrack>
190
  <RangeSliderThumb index={0} boxSize={4}/>
191
  <RangeSliderThumb index={1} boxSize={4}/>
 
194
  </GridItem>
195
 
196
  {/* Funding Range */}
197
+ <GridItem colSpan={{ base: 1, md: 2 }}>
198
  <Box>
199
  <Flex justify="space-between" mb={1}>
200
  <Text fontSize="sm" fontWeight="medium">Funding (€)</Text>
 
205
  <RangeSlider
206
  aria-label={["Min Funding","Max Funding"]}
207
  min={0}
208
+ max={1e7}
209
+ step={1e5}
210
  defaultValue={[+filters.minFunding, +filters.maxFunding]}
211
  onChange={updateSlider("minFunding","maxFunding")}
212
  size="md"
213
  >
214
  <RangeSliderTrack>
215
+ <RangeSliderFilledTrack />
216
  </RangeSliderTrack>
217
  <RangeSliderThumb index={0} boxSize={4}/>
218
  <RangeSliderThumb index={1} boxSize={4}/>
frontend/src/components/ProjectDetails.tsx CHANGED
@@ -186,11 +186,11 @@ export default function ProjectDetails({
186
  </Box>
187
 
188
  {/* Right: Model Explanation */}
189
- <Box flex={{ base: '1', md: '0.6' }} bg="grey" p={4} borderRadius="md" boxShadow="sm">
190
  <Heading size="sm" mb={4}>Model Prediction & Explanation</Heading>
191
  {shapData?.length ? (
192
  <>
193
- <Text mb={2}><strong>Predicted Label:</strong> {predicted}</Text>
194
  <Text mb={4}><strong>Probability:</strong> {(probability * 100).toFixed(2)}%</Text>
195
  <ResponsiveContainer width="100%" height={300}>
196
  <BarChart data={shapData} margin={{ top: 10, right: 30, left: 0, bottom: 5 }}>
@@ -202,7 +202,7 @@ export default function ProjectDetails({
202
  {shapData.map((entry, index) => (
203
  <Cell
204
  key={`cell-${index}`}
205
- fill={entry.shap >= 0 ? "#4caf50" : "#f44336"}
206
  />
207
  ))}
208
  </Bar>
 
186
  </Box>
187
 
188
  {/* Right: Model Explanation */}
189
+ <Box flex={{ base: '1', md: '0.6' }} bg="gray.50" p={4} borderRadius="md" boxShadow="sm" height="400px">
190
  <Heading size="sm" mb={4}>Model Prediction & Explanation</Heading>
191
  {shapData?.length ? (
192
  <>
193
+ <Text mb={2}><strong>Predicted Label:</strong> {predicted === 1 ? 'Terminated' : 'Closed'}</Text>
194
  <Text mb={4}><strong>Probability:</strong> {(probability * 100).toFixed(2)}%</Text>
195
  <ResponsiveContainer width="100%" height={300}>
196
  <BarChart data={shapData} margin={{ top: 10, right: 30, left: 0, bottom: 5 }}>
 
202
  {shapData.map((entry, index) => (
203
  <Cell
204
  key={`cell-${index}`}
205
+ fill={entry.shap >= 0 ? "#003399" : "#FFCC00"}
206
  />
207
  ))}
208
  </Bar>
frontend/src/components/ProjectExplorer.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React from "react";
2
  import {
3
  Box,
4
  Button,
@@ -18,11 +18,14 @@ import {
18
  Text,
19
  Avatar,
20
  } from "@chakra-ui/react";
21
- import type {
22
- ProjectExplorerProps,
23
- Project,
24
- ChatMessage,
25
- } from "../hooks/types";
 
 
 
26
 
27
  const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
28
  projects,
@@ -30,6 +33,12 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
30
  setSearch,
31
  statusFilter,
32
  setStatusFilter,
 
 
 
 
 
 
33
  page,
34
  setPage,
35
  setSelectedProject,
@@ -38,162 +47,165 @@ const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
38
  chatHistory,
39
  askChatbot,
40
  messagesEndRef,
41
- }) => (
42
- <Flex direction={{ base: "column", md: "row" }} gap={6}>
43
- {/* Left Pane: Projects */}
44
- <Box flex={1}>
45
- <Heading size="sm" mb={2}>
46
- Projects
47
- </Heading>
48
- <Flex gap={4} mb={4}>
49
- <Input
50
- placeholder="Search by title..."
51
- value={search}
52
- onChange={(e) => {
53
- setSearch(e.target.value);
54
- setPage(0);
55
- }}
56
- />
57
- <ChakraSelect
58
- placeholder="Filter by status"
59
- value={statusFilter}
60
- onChange={(e) => {
61
- setStatusFilter(e.target.value);
62
- setPage(0);
63
- }}
64
- >
65
- <option value="Signed">Signed</option>
66
- <option value="Closed">Closed</option>
67
- <option value="Terminated">Terminated</option>
68
- </ChakraSelect>
69
- </Flex>
70
 
71
- {/* Table Container with grey background */}
72
- <Box
73
- bg="gray.50"
74
- p={4}
75
- borderRadius="md"
76
- height="500px"
77
- overflowY="auto"
78
- >
79
- {!projects.length ? (
80
- <Flex justify="center" py={10}>
81
- <Spinner />
82
- </Flex>
83
- ) : (
84
- <Table
85
- variant="simple"
86
- size="sm"
87
- width="100%"
88
- >
89
- <Thead>
90
- <Tr>
91
- <Th width="60%" whiteSpace="nowrap">Title</Th>
92
- <Th width="10%" whiteSpace="nowrap">Status</Th>
93
- <Th width="10%" whiteSpace="nowrap">ID</Th>
94
- <Th width="10%" whiteSpace="nowrap">Start Date</Th>
95
- <Th width="10%" whiteSpace="nowrap">Funding €</Th>
96
- </Tr>
97
- </Thead>
98
- <Tbody>
99
- {projects.map((p: Project) => (
100
- <Tr
101
- key={p.id}
102
- onClick={() => setSelectedProject(p)}
103
- cursor="pointer"
104
- _hover={{ bg: "gray.100" }}
105
- >
106
- <Td overflow="hidden" textOverflow="ellipsis">{p.title}</Td>
107
- <Td>{p.status}</Td>
108
- <Td>{p.id}</Td>
109
- <Td whiteSpace="nowrap">{p.startDate}</Td>
110
- <Td>{p.ecMaxContribution?.toLocaleString()}</Td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  </Tr>
112
- ))}
113
- </Tbody>
114
- </Table>
115
- )}
116
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- <Flex mt={4} gap={2} justify="center">
119
- <Button
120
- onClick={() => setPage((p) => Math.max(p - 1, 0))}
121
- isDisabled={page === 0}
122
- aria-label="Previous page"
123
- >
124
- Previous
125
- </Button>
126
- <Button onClick={() => setPage((p) => p + 1)} aria-label="Next page">
127
- Next
128
- </Button>
129
- </Flex>
130
- </Box>
131
 
132
- {/* Right Pane: Assistant */}
133
- <Box
134
- flex={{ base: 1, md: 0.4 }}
135
- bg="gray.50"
136
- p={4}
137
- borderRadius="md"
138
- height="500px"
139
- display="flex"
140
- flexDirection="column"
141
- >
142
- <Heading size="sm" mb={2}>
143
- Assistant
144
- </Heading>
145
- <Box flex={1} overflowY="auto" mb={4}>
146
- <VStack spacing={3} align="stretch">
147
- {chatHistory.map((msg: ChatMessage, i: number) => (
148
- <HStack
149
- key={i}
150
- alignSelf={msg.role === "user" ? "flex-end" : "flex-start"}
151
- maxW="90%"
152
- >
153
- {msg.role === "assistant" && (
154
- <Avatar size="sm" name="Bot" />
155
- )}
156
- <Box>
157
- <Text
158
- fontSize="sm"
159
- bg={msg.role === "user" ? "blue.100" : "gray.200"}
160
- px={3}
161
- py={2}
162
- borderRadius="md"
163
- >
164
- {msg.content}
165
- </Text>
166
- </Box>
167
- {msg.role === "user" && (
168
- <Avatar size="sm" name="You" bg="blue.300" />
169
- )}
170
- </HStack>
171
- ))}
172
- <div ref={messagesEndRef} />
173
- </VStack>
174
  </Box>
175
- <HStack>
176
- <Input
177
- placeholder="Ask something..."
178
- value={question}
179
- onChange={(e) => setQuestion(e.target.value)}
180
- onKeyDown={(e) => {
181
- if (e.key === "Enter" && !e.shiftKey) {
182
- e.preventDefault();
183
- askChatbot();
184
- }
185
- }}
186
- />
187
- <Button
188
- onClick={askChatbot}
189
- colorScheme="blue"
190
- aria-label="Ask the chatbot"
191
- >
192
- Send
193
- </Button>
194
- </HStack>
195
- </Box>
196
- </Flex>
197
- );
198
 
199
- export default ProjectExplorer;
 
1
+ import React, { useEffect, useState } from "react";
2
  import {
3
  Box,
4
  Button,
 
18
  Text,
19
  Avatar,
20
  } from "@chakra-ui/react";
21
+ import type { ProjectExplorerProps, Project, ChatMessage } from "../hooks/types";
22
+
23
+ interface FilterOptions {
24
+ statuses: string[];
25
+ legalBases: string[];
26
+ organizations: string[];
27
+ countries: string[];
28
+ }
29
 
30
  const ProjectExplorer: React.FC<ProjectExplorerProps> = ({
31
  projects,
 
33
  setSearch,
34
  statusFilter,
35
  setStatusFilter,
36
+ legalFilter,
37
+ setLegalFilter,
38
+ orgFilter,
39
+ setOrgFilter,
40
+ countryFilter,
41
+ setCountryFilter,
42
  page,
43
  setPage,
44
  setSelectedProject,
 
47
  chatHistory,
48
  askChatbot,
49
  messagesEndRef,
50
+ }) => {
51
+ const [filterOpts, setFilterOpts] = useState<FilterOptions>({
52
+ statuses: [],
53
+ legalBases: [],
54
+ organizations: [],
55
+ countries: []
56
+ });
57
+ const [loadingFilters, setLoadingFilters] = useState(false);
58
+
59
+ // Fetch dynamic filter options whenever any filter changes
60
+ useEffect(() => {
61
+ setLoadingFilters(true);
62
+ const params = new URLSearchParams();
63
+ if (statusFilter) params.set("status", statusFilter);
64
+ if (legalFilter) params.set("legalBasis", legalFilter);
65
+ if (orgFilter) params.set("organization", orgFilter);
66
+ if (countryFilter) params.set("country", countryFilter);
67
+ if (search) params.set("search", search);
68
+
69
+ fetch(`/api/filters?${params.toString()}`)
70
+ .then((res) => res.json())
71
+ .then((data: FilterOptions) => setFilterOpts(data))
72
+ .catch(console.error)
73
+ .finally(() => setLoadingFilters(false));
74
+ }, [statusFilter, legalFilter, orgFilter, countryFilter, search]);
 
 
 
 
75
 
76
+ return (
77
+ <Flex direction={{ base: "column", md: "row" }} gap={6}>
78
+ {/* Left Pane: Projects & Filters */}
79
+ <Box flex={1}>
80
+ <Heading size="sm" mb={2}>Projects</Heading>
81
+ <Flex gap={4} mb={4} flexWrap="wrap">
82
+ <Input
83
+ placeholder="Search by title..."
84
+ value={search}
85
+ onChange={(e) => { setSearch(e.target.value); setPage(0); }}
86
+ width={{ base: "100%", md: "200px" }}
87
+ />
88
+ <ChakraSelect
89
+ placeholder={loadingFilters ? "Loading..." : "Status"}
90
+ value={statusFilter}
91
+ onChange={(e) => { setStatusFilter(e.target.value); setPage(0); }}
92
+ isDisabled={loadingFilters}
93
+ width="150px"
94
+ >
95
+ {filterOpts.statuses.map((s) => <option key={s} value={s}>{s}</option>)}
96
+ </ChakraSelect>
97
+ <ChakraSelect
98
+ placeholder={loadingFilters ? "Loading..." : "Legal Basis"}
99
+ value={legalFilter}
100
+ onChange={(e) => { setLegalFilter(e.target.value); setPage(0); }}
101
+ isDisabled={loadingFilters}
102
+ width="150px"
103
+ >
104
+ {filterOpts.legalBases.map((lb) => <option key={lb} value={lb}>{lb}</option>)}
105
+ </ChakraSelect>
106
+ <ChakraSelect
107
+ placeholder={loadingFilters ? "Loading..." : "Organization"}
108
+ value={orgFilter}
109
+ onChange={(e) => { setOrgFilter(e.target.value); setPage(0); }}
110
+ isDisabled={loadingFilters}
111
+ width="180px"
112
+ >
113
+ {filterOpts.organizations.map((o) => <option key={o} value={o}>{o}</option>)}
114
+ </ChakraSelect>
115
+ <ChakraSelect
116
+ placeholder={loadingFilters ? "Loading..." : "Country"}
117
+ value={countryFilter}
118
+ onChange={(e) => { setCountryFilter(e.target.value); setPage(0); }}
119
+ isDisabled={loadingFilters}
120
+ width="150px"
121
+ >
122
+ {filterOpts.countries.map((c) => <option key={c} value={c}>{c}</option>)}
123
+ </ChakraSelect>
124
+ </Flex>
125
+
126
+ <Box bg="gray.50" p={4} borderRadius="md" height="500px" overflowY="auto">
127
+ {!projects.length ? (
128
+ <Flex justify="center" py={10}><Spinner /></Flex>
129
+ ) : (
130
+ <Table variant="simple" size="sm" width="100%">
131
+ <Thead>
132
+ <Tr>
133
+ <Th width="60%">Title</Th>
134
+ <Th>Status</Th>
135
+ <Th>ID</Th>
136
+ <Th>Start Date</Th>
137
+ <Th>Funding €</Th>
138
  </Tr>
139
+ </Thead>
140
+ <Tbody>
141
+ {projects.map((p: Project) => (
142
+ <Tr
143
+ key={p.id}
144
+ onClick={() => setSelectedProject(p)}
145
+ cursor="pointer"
146
+ _hover={{ bg: "gray.100" }}
147
+ >
148
+ <Td isTruncated>{p.title}</Td>
149
+ <Td>{p.status}</Td>
150
+ <Td>{p.id}</Td>
151
+ <Td whiteSpace="nowrap">{p.startDate}</Td>
152
+ <Td>{p.ecMaxContribution.toLocaleString()}</Td>
153
+ </Tr>
154
+ ))}
155
+ </Tbody>
156
+ </Table>
157
+ )}
158
+ </Box>
159
 
160
+ <Flex mt={4} gap={2} justify="center">
161
+ <Button onClick={() => setPage(p => Math.max(p - 1, 0))} isDisabled={page === 0}>Previous</Button>
162
+ <Button onClick={() => setPage(p => p + 1)}>Next</Button>
163
+ </Flex>
164
+ </Box>
 
 
 
 
 
 
 
 
165
 
166
+ {/* Right Pane: Assistant */}
167
+ <Box
168
+ flex={{ base: 1, md: 0.4 }}
169
+ bg="gray.50"
170
+ p={4}
171
+ borderRadius="md"
172
+ height="500px"
173
+ display="flex"
174
+ flexDirection="column"
175
+ >
176
+ <Heading size="sm" mb={2}>Assistant</Heading>
177
+ <Box flex={1} overflowY="auto" mb={4}>
178
+ <VStack spacing={3} align="stretch">
179
+ {chatHistory.map((msg: ChatMessage, i: number) => (
180
+ <HStack
181
+ key={i}
182
+ alignSelf={msg.role === "user" ? "flex-end" : "flex-start"}
183
+ maxW="90%"
184
+ >
185
+ {msg.role === "assistant" && <Avatar size="sm" name="Bot" />}
186
+ <Box>
187
+ <Text fontSize="sm" bg={msg.role === "user" ? "blue.100" : "gray.200"} px={3} py={2} borderRadius="md">
188
+ {msg.content}
189
+ </Text>
190
+ </Box>
191
+ {msg.role === "user" && <Avatar size="sm" name="You" bg="blue.300" />}
192
+ </HStack>
193
+ ))}
194
+ <div ref={messagesEndRef} />
195
+ </VStack>
196
+ </Box>
197
+ <HStack>
198
+ <Input
199
+ placeholder="Ask something..."
200
+ value={question}
201
+ onChange={(e) => setQuestion(e.target.value)}
202
+ onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); askChatbot(); } }}
203
+ />
204
+ <Button onClick={askChatbot} colorScheme="blue" aria-label="Ask the chatbot">Send</Button>
205
+ </HStack>
 
 
206
  </Box>
207
+ </Flex>
208
+ );
209
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
+ export default ProjectExplorer;
frontend/src/hooks/types.ts CHANGED
@@ -74,6 +74,12 @@ export interface ProjectExplorerProps {
74
  setSearch: (value: string) => void;
75
  statusFilter: string;
76
  setStatusFilter: (value: string) => void;
 
 
 
 
 
 
77
  page: number;
78
  setPage: React.Dispatch<React.SetStateAction<number>>;
79
  setSelectedProject: (project: Project) => void;
 
74
  setSearch: (value: string) => void;
75
  statusFilter: string;
76
  setStatusFilter: (value: string) => void;
77
+ legalFilter: string;
78
+ setLegalFilter: (value: string) => void;
79
+ orgFilter: string;
80
+ setOrgFilter: (value: string) => void;
81
+ countryFilter: string;
82
+ setCountryFilter: (value: string) => void;
83
  page: number;
84
  setPage: React.Dispatch<React.SetStateAction<number>>;
85
  setSelectedProject: (project: Project) => void;