Skip to content

Commit

Permalink
Initial release; the Recommender widget was build by Tim, with some m… (
Browse files Browse the repository at this point in the history
#2072)

* Initial release; the Recommender widget was build by Tim, with some modications by me

* When user logs out, save the '' username - and switch to search help tab
  • Loading branch information
romanchyla committed Oct 2, 2020
1 parent 4182455 commit 60df2e1
Show file tree
Hide file tree
Showing 15 changed files with 788 additions and 89 deletions.
8 changes: 6 additions & 2 deletions src/config/discovery.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ require.config({
SearchWidget: 'js/widgets/search_bar/search_bar_widget',
PaperSearchForm: 'js/widgets/paper_search_form/widget',
Results: 'js/widgets/results/widget',
MyAdsFreeform: 'reactify!js/react/BumblebeeWidget?MyAdsFreeform',
QueryInfo: 'js/widgets/query_info/query_info_widget',
QueryDebugInfo: 'js/widgets/api_query/widget',
ExportWidget: 'es6!js/widgets/export/widget.jsx',
Expand All @@ -100,7 +99,6 @@ require.config({
PaperNetwork: 'js/wraps/paper_network',
ConceptCloud: 'js/widgets/wordcloud/widget',
BubbleChart: 'js/widgets/bubble_chart/widget',
MyAdsDashboard: 'reactify!js/react/BumblebeeWidget?MyAdsDashboard',
AuthorAffiliationTool:
'es6!js/widgets/author_affiliation_tool/widget.jsx',

Expand Down Expand Up @@ -146,6 +144,12 @@ require.config({
LibraryActionsWidget: 'es6!js/widgets/library_actions/widget.jsx',
AllLibrariesWidget: 'js/widgets/libraries_all/widget',
LibraryListWidget: 'js/widgets/library_list/widget',

// react widgets

MyAdsFreeform: 'reactify!js/react/BumblebeeWidget?MyAdsFreeform',
MyAdsDashboard: 'reactify!js/react/BumblebeeWidget?MyAdsDashboard',
RecommenderWidget: 'reactify!js/react/BumblebeeWidget?Recommender',
},
plugins: {},
},
Expand Down
11 changes: 7 additions & 4 deletions src/js/apps/discovery/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ define(['config/discovery.config', 'module'], function(config, module) {
updateProgress(100);

app.onBootstrap(data);

var dynConf = app.getObject('DynamicConfig');
if (dynConf && dynConf.debugExportBBB) {
window.bbb = app;
}

pubsub.publish(pubsub.getCurrentPubSubKey(), pubsub.APP_BOOTSTRAPPED);

pubsub.publish(pubsub.getCurrentPubSubKey(), pubsub.APP_STARTING);
Expand Down Expand Up @@ -155,10 +161,7 @@ define(['config/discovery.config', 'module'], function(config, module) {
return false;
});

var dynConf = app.getObject('DynamicConfig');
if (dynConf && dynConf.debugExportBBB) {
window.bbb = app;
}


// app is loaded, send timing event

Expand Down
2 changes: 1 addition & 1 deletion src/js/apps/discovery/navigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ define([
var defer = $.Deferred();
app
.getObject('MasterPageManager')
.show('LandingPage', ['SearchWidget'])
.show('LandingPage', ['SearchWidget', 'RecommenderWidget'])
.then(function() {
return app.getWidget('LandingPage').then(function(widget) {
if (data && data.origin === 'SearchWidget') {
Expand Down
67 changes: 67 additions & 0 deletions src/js/react/Recommender/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
define([], function() {
const actions = {
GET_RECOMMENDATIONS: 'GET_RECOMMENDATIONS',
GET_DOCS: 'GET_DOCS',
SET_DOCS: 'SET_DOCS',
SET_QUERY: 'SET_QUERY',
UPDATE_SEARCH_BAR: 'UPDATE_SEARCH_BAR',
GET_FULL_LIST: 'GET_FULL_LIST',
EMIT_ANALYTICS: 'EMIT_ANALYTICS',
SET_TAB: 'SET_TAB',
SET_ORACLE_TARGET: 'SET_ORACLE_TARGET',
SET_QUERY_PARAMS: 'SET_QUERY_PARAMS',
UPDATE_USERNAME: 'UPDATE_USERNAME'
};

const actionCreators = {
getRecommendations: () => ({
type: actions.GET_RECOMMENDATIONS,
}),
getDocs: (query) => ({
type: 'API_REQUEST',
scope: actions.GET_DOCS,
options: {
type: 'GET',
target: 'search/query',
query,
},
}),
setDocs: (docs) => ({
type: actions.SET_DOCS,
payload: docs,
}),
setQuery: (query) => ({
type: actions.SET_QUERY,
payload: query,
}),
setQueryParams: (payload) => ({
type: actions.SET_QUERY_PARAMS,
payload,
}),
updateSearchBar: (text) => ({
type: actions.UPDATE_SEARCH_BAR,
payload: text,
}),
updateUserName: (text) => ({
type: actions.UPDATE_USERNAME,
payload: text,
}),
getFullList: () => ({
type: actions.GET_FULL_LIST,
}),
emitAnalytics: (payload) => ({
type: actions.EMIT_ANALYTICS,
payload,
}),
setTab: (tab) => ({
type: actions.SET_TAB,
payload: tab,
}),
setOracleTarget: (target) => ({
type: actions.SET_ORACLE_TARGET,
payload: target,
}),
};

return { ...actions, ...actionCreators };
});
60 changes: 60 additions & 0 deletions src/js/react/Recommender/components/App.jsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
define([
'react',
'react-bootstrap',
'react-prop-types',
'react-redux',
'../actions',
'es6!./RecommendedList.jsx',
'es6!./SearchExamples.jsx',
], function(
React,
{ Nav, NavItem },
PropTypes,
{ useDispatch, useSelector },
{ setTab, emitAnalytics },
RecommendedList,
SearchExamples
) {
const selector = (state) => ({
tab: state.tab,
});

const App = () => {
const dispatch = useDispatch();
const { tab } = useSelector(selector);
const onSelected = (key) => {
dispatch(setTab(key));
dispatch(
emitAnalytics([
'send',
'event',
'interaction.main-page',
key === 1 ? 'recommender' : 'help',
])
);
};

return (
<div>
<Nav
bsStyle="tabs"
justified
activeKey={tab}
onSelect={(key) => onSelected(key)}
>
<NavItem eventKey={1} href="javascript:void(0);">
Recommendations
</NavItem>
<NavItem eventKey={2} href="javascript:void(0);">
Search examples
</NavItem>
</Nav>
<div style={{ minHeight: 200, padding: '1rem 0' }}>
{tab === 1 ? <RecommendedList /> : <SearchExamples />}
</div>
</div>
);
};

return App;
});
200 changes: 200 additions & 0 deletions src/js/react/Recommender/components/RecommendedList.jsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
define([
'react',
'react-prop-types',
'react-redux',
'react-bootstrap',
'../actions',
], function(
React,
PropTypes,
{ useSelector, useDispatch },
{ Button },
{ getRecommendations, getFullList, emitAnalytics }
) {
const Paper = ({ title, bibcode, author, totalAuthors, onClick }) => {
const el = React.useRef(null);
React.useEffect(() => {
if (el.current) {
el.current.addEventListener('click', onClick);
}
return () => {
if (el.current) {
el.current.removeEventListener('click', onClick);
}
};
}, []);

return (
<li style={{ marginTop: '1rem' }}>
<a href={`#/abs/${bibcode}/abstract`} ref={el}>
{title}
</a>
<ul className="list-inline">
{author.map((entry, i) => (
<li key={entry}>{`${entry}${i < 2 ? ';' : ''}`}</li>
))}
{totalAuthors > 3 && <li>...</li>}
</ul>
</li>
);
};
Paper.defaultProps = {
title: '',
bibcode: '',
author: [],
totalAuthors: 0,
onClick: () => {},
};

Paper.propTypes = {
title: PropTypes.string,
bibcode: PropTypes.string,
author: PropTypes.arrayOf(PropTypes.string),
totalAuthors: PropTypes.number,
onClick: PropTypes.func,
};

const Message = ({ children }) => (
<div
style={{
display: 'flex',
justifyContent: 'center',
padding: '2rem 0',
}}
>
{children}
</div>
);
Message.propTypes = {
children: PropTypes.element.isRequired,
};

var executed = 0;
var userName = null;
const selector = (state) => {
// reset if it is a different user
if (state.userName !== userName) {
executed = 0;
userName = state.userName;
}

return {
getRecommendationsRequest: state.requests.GET_RECOMMENDATIONS,
getDocsRequest: state.requests.GET_DOCS,
docs: state.docs,
queryParams: state.queryParams,
executed: executed
};
};

const RecommendedList = () => {
const dispatch = useDispatch();
const onGetMore = () => {
dispatch(getFullList());
};
const {
getRecommendationsRequest,
getDocsRequest,
docs,
queryParams,
userName
} = useSelector(selector);

React.useEffect(() => {
if ((executed + 12*60*60*1000) < Date.now()) {
// the hook gets called too many times even with [docs] in the args to useEffect
// (and oracle returns 404 when nothing is found; which is IMHO wrong) but we can't
// rely on status.failure for that reason
executed = Date.now();
dispatch(getRecommendations());
}
else {
if (executed && getDocsRequest && getDocsRequest.status && docs.length === 0) {
// we are rendered (send the signal everytime -- even if it was sent already)
dispatch(
emitAnalytics([
'send',
'event',
'interaction.recommendation', // category
'no-useful-recommendations', // action
'', // label,
0, // value
])
);
}
}
});


const onPaperSelect = ({ bibcode }, index) => {
dispatch(
emitAnalytics([
'send',
'event',
'interaction.recommendation', // category
queryParams.function, // action
bibcode, // label,
index, // value
])
);
};

if (
getRecommendationsRequest.status === 'pending' ||
getDocsRequest.status === 'pending'
) {
return (
<Message>
<span>
<i className="fa fa-spinner fa-spin" aria-hidden="true" />{' '}
Loading...
</span>
</Message>
);
}

if (
getRecommendationsRequest.status === 'failure' ||
getDocsRequest.status === 'failure'
) {
return (
<Message>
<span>
<i
className="fa fa-exclamation-triangle text-danger"
aria-hidden="true"
/>{' '}
{getRecommendationsRequest.error || getDocsRequest.error}
</span>
</Message>
);
}

if (docs.length === 0) {
return (
<Message>
Sorry, we don't have any recommendations for you just yet! ADS provides users recommendations based on their reading history, and we suggest that you create an ADS account to take advantage of this feature. If you already have an account, then be sure you are logged in while searching and reading papers. In due time we will be able to provide you with suggestions based on your inferred interests.
</Message>
);
}

return (
<div>
<ul className="list-unstyled">
{docs.map(({ title, bibcode, author, totalAuthors }, index) => (
<Paper
key={bibcode}
title={title}
bibcode={bibcode}
author={author}
totalAuthors={totalAuthors}
onClick={() => onPaperSelect(docs[index], index)}
/>
))}
</ul>
</div>
);
};

return RecommendedList;
});
Loading

0 comments on commit 60df2e1

Please sign in to comment.