From b2e3edab9d391b8b1f624df7cef45600f3456036 Mon Sep 17 00:00:00 2001 From: Edward Chernenko Date: Sat, 7 Dec 2024 09:17:40 +0300 Subject: [PATCH] Add "Add to AI chat" links to search results (Special:Search) --- extension.json | 25 +++++++++++++ i18n/en.json | 5 +++ i18n/qqq.json | 5 +++ includes/Hooks.php | 44 +++++++++++++++++++++++ modules/ext.askai.search.js | 70 +++++++++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 includes/Hooks.php create mode 100644 modules/ext.askai.search.js diff --git a/extension.json b/extension.json index f9c397b..ed39f2a 100644 --- a/extension.json +++ b/extension.json @@ -18,12 +18,21 @@ } }, "AutoloadClasses": { + "MediaWiki\\AskAI\\Hooks": "includes/Hooks.php", "MediaWiki\\AskAI\\SpecialAI": "includes/SpecialAI.php", "MediaWiki\\AskAI\\Service\\IExternalService": "includes/Service/IExternalService.php", "MediaWiki\\AskAI\\Service\\DebugService": "includes/Service/DebugService.php", "MediaWiki\\AskAI\\Service\\OpenAI": "includes/Service/OpenAI.php", "MediaWiki\\AskAI\\Service\\ServiceFactory": "includes/Service/ServiceFactory.php" }, + "HookHandlers": { + "main": { + "class": "MediaWiki\\AskAI\\Hooks" + } + }, + "Hooks": { + "SpecialSearchResultsPrepend": "main" + }, "ExtensionMessagesFiles": { "AIAlias": "AskAI.alias.php" }, @@ -62,6 +71,22 @@ "dependencies": [ "mediawiki.Title" ] + }, + "ext.askai.search": { + "scripts": [ + "ext.askai.search.js" + ], + "targets": [ + "desktop", + "mobile" + ], + "messages": [ + "askai-search-add", + "askai-search-adding", + "askai-search-add-failed", + "askai-search-add-not-found", + "askai-search-view" + ] } }, "ResourceFileModulePaths": { diff --git a/i18n/en.json b/i18n/en.json index 0049cae..fa5abc5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -17,6 +17,11 @@ "askai-field-response": "Response from the AI:", "askai-openai-failed": "Failed to contact OpenAI: $1", "askai-openai-not-configured": "Error: OpenAI not configured: apiKey, apiUrl or model are not set.", + "askai-search-add": "Add to AI chat", + "askai-search-adding": "Adding...", + "askai-search-add-failed": "Failed to load the page.", + "askai-search-add-not-found": "Failed to locate the paragraphs that are referenced in this search result.", + "askai-search-view": "View AI chat", "askai-source": "(Source #$1) [$2]", "askai-submit-failed": "HTTP error when submitting the form: $1", "askai-unknown-service": "Not configured: incorrect value of $wgAskAIServiceClass.", diff --git a/i18n/qqq.json b/i18n/qqq.json index 6cd09ff..a7f5d4c 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -17,6 +17,11 @@ "askai-field-response": "Label of readonly field that will show the response from the AI.", "askai-openai-failed": "Error message if HTTP request to OpenAI resulted in an error.", "askai-openai-not-configured": "Error message if OpenAI API key, etc. are not configured.", + "askai-search-add": "Link on Special:Search that adds this result to AI chat.", + "askai-search-adding": "Temporarily shown while waiting for result to be added to AI chat.", + "askai-search-add-failed": "Error when we couldn't load the page from the search results.", + "askai-search-add-not-found": "Error when the text from search results wasn't found anywhere in the article.", + "askai-search-view": "Link on Special:Search that redirects user to Special:AI.", "askai-source": "Name of source document (will be supplied to AI as part of instructions). $1 - index of document (1, 2, 3, etc.), $2 - page name.", "askai-submit-failed": "Error message if HTTP POST request to Special:AI resulted in an error.", "askai-unknown-service": "Error message if failed to create an AI service.", diff --git a/includes/Hooks.php b/includes/Hooks.php new file mode 100644 index 0000000..3c5e61c --- /dev/null +++ b/includes/Hooks.php @@ -0,0 +1,44 @@ +addModules( 'ext.askai.search' ); + } +} diff --git a/modules/ext.askai.search.js b/modules/ext.askai.search.js new file mode 100644 index 0000000..965d12d --- /dev/null +++ b/modules/ext.askai.search.js @@ -0,0 +1,70 @@ +/* Shows "Add to AI chat" link near every search result on Special:Search. */ + +$( function () { + const $results = $( '.mw-search-result' ); + if ( !$results.length ) { + return; + } + + const addedSources = []; + + /* Redirect to Special:AI, providing it with the previously added sources */ + function goToSpecial() { + const title = new mw.Title( 'Special:AI' ); + window.location.href = title.getUrl( { + wpPages: addedSources.join( '\n' ) + } ); + } + + /* Handler of "Add to AI chat" link */ + function addToAI() { + const $addLink = $( this ), + $res = $addLink.parents( '.mw-search-result' ), + pageLink = $res.find( 'a' )[ 0 ], + matchedSentences = $res.find( '.searchresult' ).text().trim().split( /\.\s/ ); + + const $loading = $( '' ) + .attr( 'class', 'mw-askai-search-adding' ) + .append( mw.msg( 'askai-search-adding' ) ); + + $addLink.replaceWith( $loading ); + + // Download the article and find the paragraph(s) that contain "matchedText". + $.get( pageLink.href ).done( ( html ) => { + const parNumbers = []; + + $( '
' ).append( html ).find( '.mw-parser-output > p' ).each( ( idx, p ) => { + for ( const sentence of matchedSentences ) { + if ( p.innerText.indexOf( sentence ) !== -1 ) { + parNumbers.push( idx ); + return; + } + } + } ); + + if ( !parNumbers.length ) { + $loading.replaceWith( mw.msg( 'askai-search-add-not-found' ) ); + return; + } + + addedSources.push( pageLink.title + '#p' + parNumbers.join( ',' ) ); + + $loading.replaceWith( $( '' ) + .attr( 'class', 'mw-askai-search-view' ) + .append( mw.msg( 'askai-search-view' ) ) + .click( goToSpecial ) + ); + } ).fail( () => { + $loading.replaceWith( mw.msg( 'askai-search-add-failed' ) ); + } ); + } + + // Every result should have "Add to AI chat" link + $results.each( ( idx, result ) => { + $( result ).find( '.mw-search-result-data' ).append( ' ', $( '' ) + .attr( 'class', 'mw-askai-search-add' ) + .append( mw.msg( 'askai-search-add' ) ) + .click( addToAI ) + ); + } ); +} );