@@ -21,7 +21,7 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
2121 < span id ="status-text " class ="text-slate-500 "> Verifica MCP...</ span >
2222 </ div >
2323 </ div >
24- < div class ="grid gap-4 md:grid-cols-2 ">
24+ < div class ="grid gap-4 md:grid-cols-3 ">
2525 < label class ="flex flex-col gap-2 text-sm font-medium ">
2626 Endpoint MCP
2727 < input
@@ -38,6 +38,15 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
3838 placeholder ="https://www.dati.gov.it/opendata "
3939 />
4040 </ label >
41+ < label class ="flex flex-col gap-2 text-sm font-medium ">
42+ API key Gemini
43+ < input
44+ id ="gemini-key "
45+ type ="password "
46+ class ="rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none "
47+ placeholder ="AIza... "
48+ />
49+ </ label >
4150 </ div >
4251 < div class ="flex flex-wrap items-center justify-between gap-4 text-sm ">
4352 < button
@@ -47,7 +56,7 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
4756 Applica impostazioni
4857 </ button >
4958 < p class ="text-slate-500 ">
50- Nessuna integrazione Gemini: solo MCP JSON-RPC .
59+ Query in linguaggio naturale via Gemini 2.5 Flash .
5160 </ p >
5261 </ div >
5362 </ header >
@@ -87,6 +96,7 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
8796 const elements = {
8897 endpoint : document . getElementById ( "mcp-endpoint" ) ,
8998 ckan : document . getElementById ( "ckan-server" ) ,
99+ geminiKey : document . getElementById ( "gemini-key" ) ,
90100 save : document . getElementById ( "save-settings" ) ,
91101 statusDot : document . getElementById ( "status-dot" ) ,
92102 statusText : document . getElementById ( "status-text" ) ,
@@ -99,11 +109,13 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
99109 const state = {
100110 endpoint : localStorage . getItem ( "mcpEndpoint" ) || DEFAULT_ENDPOINT ,
101111 ckanServer : localStorage . getItem ( "ckanServer" ) || DEFAULT_CKAN ,
112+ geminiKey : localStorage . getItem ( "geminiApiKey" ) || "" ,
102113 busy : false
103114 } ;
104115
105116 elements . endpoint . value = state . endpoint ;
106117 elements . ckan . value = state . ckanServer ;
118+ elements . geminiKey . value = state . geminiKey ;
107119
108120 const setStatus = ( status , text ) => {
109121 elements . statusText . textContent = text ;
@@ -223,13 +235,102 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
223235 }
224236 } ;
225237
238+ const stopwords = new Set ( [
239+ "a" ,
240+ "al" ,
241+ "allo" ,
242+ "alla" ,
243+ "alle" ,
244+ "ai" ,
245+ "agli" ,
246+ "da" ,
247+ "dal" ,
248+ "dallo" ,
249+ "dalla" ,
250+ "delle" ,
251+ "dei" ,
252+ "del" ,
253+ "della" ,
254+ "di" ,
255+ "e" ,
256+ "ed" ,
257+ "il" ,
258+ "lo" ,
259+ "la" ,
260+ "le" ,
261+ "i" ,
262+ "gli" ,
263+ "in" ,
264+ "su" ,
265+ "per" ,
266+ "con" ,
267+ "che" ,
268+ "quali" ,
269+ "quale" ,
270+ "quanti" ,
271+ "quanto" ,
272+ "tema" ,
273+ "dataset"
274+ ] ) ;
275+
276+ const normalizeQuery = ( query ) => {
277+ if ( / [ " * : ] | \b A N D \b | \b O R \b | \b N O T \b / i. test ( query ) ) {
278+ return query ;
279+ }
280+ const tokens = query
281+ . toLowerCase ( )
282+ . match ( / [ \p{ L} \p{ N} ] + / gu)
283+ ?. filter ( ( token ) => token . length > 1 && ! stopwords . has ( token ) ) ;
284+ if ( ! tokens || tokens . length === 0 ) return "*:*" ;
285+ return tokens . join ( " " ) ;
286+ } ;
287+
288+ const callGemini = async ( userQuery ) => {
289+ if ( ! state . geminiKey ) {
290+ throw new Error ( "API key Gemini mancante" ) ;
291+ }
292+ const prompt = `Converti la richiesta in una query Solr per CKAN. Rispondi solo con JSON: {"q":"..."}.
293+ Richiesta: ${ userQuery } ` ;
294+ const response = await fetch (
295+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ state . geminiKey } ` ,
296+ {
297+ method : "POST" ,
298+ headers : {
299+ "Content-Type" : "application/json" ,
300+ Accept : "application/json"
301+ } ,
302+ body : JSON . stringify ( {
303+ contents : [ { role : "user" , parts : [ { text : prompt } ] } ] ,
304+ generationConfig : { temperature : 0.2 , maxOutputTokens : 200 }
305+ } )
306+ }
307+ ) ;
308+ if ( ! response . ok ) {
309+ throw new Error ( `Gemini HTTP ${ response . status } ` ) ;
310+ }
311+ const data = await response . json ( ) ;
312+ const text = data . candidates ?. [ 0 ] ?. content ?. parts ?. [ 0 ] ?. text || "" ;
313+ const jsonStart = text . indexOf ( "{" ) ;
314+ const jsonEnd = text . lastIndexOf ( "}" ) ;
315+ if ( jsonStart === - 1 || jsonEnd === - 1 ) {
316+ throw new Error ( "Risposta Gemini non valida" ) ;
317+ }
318+ const parsed = JSON . parse ( text . slice ( jsonStart , jsonEnd + 1 ) ) ;
319+ if ( ! parsed . q ) {
320+ throw new Error ( "Gemini non ha restituito q" ) ;
321+ }
322+ return parsed . q ;
323+ } ;
324+
226325 const runSearch = async ( query ) => {
326+ const normalizedQuery = normalizeQuery ( query ) ;
327+ const solrQuery = await callGemini ( query ) . catch ( ( ) => normalizedQuery ) ;
227328 setToolStatus ( "Uso tool: ckan_package_search" ) ;
228329 const result = await callMcp ( "tools/call" , {
229330 name : "ckan_package_search" ,
230331 arguments : {
231332 server_url : state . ckanServer ,
232- q : query ,
333+ q : solrQuery ,
233334 rows : 5 ,
234335 response_format : "json"
235336 }
@@ -278,8 +379,10 @@ <h1 class="text-3xl font-semibold">CKAN Open Data Explorer</h1>
278379 elements . save . addEventListener ( "click" , async ( ) => {
279380 state . endpoint = elements . endpoint . value . trim ( ) || DEFAULT_ENDPOINT ;
280381 state . ckanServer = elements . ckan . value . trim ( ) || DEFAULT_CKAN ;
382+ state . geminiKey = elements . geminiKey . value . trim ( ) ;
281383 localStorage . setItem ( "mcpEndpoint" , state . endpoint ) ;
282384 localStorage . setItem ( "ckanServer" , state . ckanServer ) ;
385+ localStorage . setItem ( "geminiApiKey" , state . geminiKey ) ;
283386 await checkMcp ( ) ;
284387 } ) ;
285388
0 commit comments