Skip to content

Commit 2f0bfd7

Browse files
committed
wip: orcha
1 parent f18b106 commit 2f0bfd7

11 files changed

Lines changed: 1354 additions & 685 deletions

File tree

invenio_app_rdm/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,3 +1626,5 @@ def github_link_render(record):
16261626

16271627
MULTIPROFILER_PERMISSION = administration_permission.can
16281628
"""Function to check for permissions to access the profiler."""
1629+
1630+
RDM_DEPOSIT_ORCHA_ENABLED = False

invenio_app_rdm/records_ui/views/deposits.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,9 @@ def get_form_config(**kwargs):
399399
if record_quota:
400400
quota["maxStorage"] = record_quota["quota_size"]
401401
quota["quotaIncrease"] = evaluate_quota_increase(record, identity)
402+
orcha_enabled = conf.get("RDM_DEPOSIT_ORCHA_ENABLED", False)
403+
if callable(orcha_enabled):
404+
orcha_enabled = orcha_enabled()
402405

403406
return dict(
404407
vocabularies=VocabulariesOptions().dump(),
@@ -423,6 +426,7 @@ def get_form_config(**kwargs):
423426
default_transfer_type=current_transfer_registry.default_transfer_type,
424427
enabled_transfer_types=list(current_transfer_registry.get_transfer_types()),
425428
transfer_types=file_transfer_type()["transfer_types"],
429+
orcha_enabled=orcha_enabled,
426430
**kwargs,
427431
)
428432

invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/deposit/RDMDepositForm.js

Lines changed: 680 additions & 641 deletions
Large diffs are not rendered by default.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import { connect } from "react-redux";
4+
import { Button, Message } from "semantic-ui-react";
5+
import { useSuggestions } from "./Context";
6+
import { i18next } from "@translations/invenio_app_rdm/i18next";
7+
8+
const getDraftId = (record) => {
9+
const id = record?.id;
10+
return id && id !== "null" && id !== "undefined" ? id : null;
11+
};
12+
13+
export const SuggestionsButtonComponent = ({ record, currentRecord, file }) => {
14+
const suggestions = useSuggestions();
15+
16+
if (!suggestions) return null;
17+
18+
const {
19+
isLoading,
20+
status,
21+
error,
22+
activeFileKey,
23+
suggestionsCount,
24+
trigger,
25+
dismissError,
26+
} = suggestions;
27+
const draftRecord = getDraftId(currentRecord) ? currentRecord : record;
28+
const draftId = getDraftId(draftRecord);
29+
const fileKey = file?.name ?? null;
30+
const isCurrentFile = !fileKey || activeFileKey === fileKey;
31+
const isButtonLoading = isLoading && isCurrentFile;
32+
const showError = error && isCurrentFile;
33+
const showSuggestionsCount = status === "success" && isCurrentFile;
34+
let label = i18next.t("Extract metadata");
35+
if (isButtonLoading) {
36+
label = i18next.t("Extracting metadata...");
37+
} else if (status === "success" && isCurrentFile) {
38+
label = i18next.t("Re-extract metadata");
39+
}
40+
41+
return (
42+
<div className="orcha-suggestions-button">
43+
<Button
44+
type="button"
45+
primary
46+
compact
47+
size="mini"
48+
loading={isButtonLoading}
49+
disabled={isLoading || !draftId || !fileKey}
50+
icon="magic"
51+
content={label}
52+
onClick={() => trigger(draftRecord, fileKey)}
53+
/>
54+
{showSuggestionsCount && (
55+
<span className="orcha-suggestions-count ml-5">
56+
{i18next.t("Extracted {{count}} suggestions", {
57+
count: suggestionsCount,
58+
})}
59+
</span>
60+
)}
61+
{showError && (
62+
<Message negative size="mini" className="mt-5" onDismiss={dismissError}>
63+
{error}
64+
</Message>
65+
)}
66+
</div>
67+
);
68+
};
69+
70+
SuggestionsButtonComponent.propTypes = {
71+
record: PropTypes.object.isRequired,
72+
currentRecord: PropTypes.object,
73+
file: PropTypes.shape({
74+
name: PropTypes.string,
75+
}),
76+
};
77+
78+
SuggestionsButtonComponent.defaultProps = {
79+
currentRecord: null,
80+
file: null,
81+
};
82+
83+
const mapStateToProps = (state) => ({
84+
currentRecord: state.deposit?.record,
85+
});
86+
87+
export const SuggestionsButton = connect(mapStateToProps)(SuggestionsButtonComponent);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createContext, useContext } from "react";
2+
3+
const SuggestionsContext = createContext(null);
4+
5+
export const useSuggestions = () => {
6+
return useContext(SuggestionsContext);
7+
};
8+
9+
export default SuggestionsContext;
10+
export { SuggestionsContext };
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
import { connect as connectFormik } from "formik";
4+
import { Button, Icon, Message } from "semantic-ui-react";
5+
import { useSuggestions } from "./Context";
6+
import { i18next } from "@translations/invenio_app_rdm/i18next";
7+
import { makeIcon } from "../../utils";
8+
9+
// This is the mapping for suggestion feild translation
10+
const suggestedFieldLabels = {
11+
doi: "DOI",
12+
title: "Title",
13+
publication_date: "Publication date",
14+
creators: "Authors/Creators",
15+
description: "Description",
16+
};
17+
18+
const renderAuthor = (author) => (
19+
<>
20+
{author.orcid && <span>{makeIcon("orcid", author.orcid, author.name)} </span>}
21+
<strong> {author.name}</strong>
22+
{author.affiliation && <span> {author.affiliation}</span>}
23+
</>
24+
);
25+
26+
const renderSuggestionActions = (onApply, onDeny) => (
27+
<Button.Group size="mini" className="orcha-suggestion-actions">
28+
<Button
29+
type="button"
30+
positive
31+
icon="check"
32+
content={i18next.t("Apply")}
33+
onClick={onApply}
34+
/>
35+
<Button
36+
type="button"
37+
negative
38+
icon="times"
39+
aria-label={i18next.t("Deny")}
40+
onClick={onDeny}
41+
/>
42+
</Button.Group>
43+
);
44+
45+
const renderSuggestedCreators = (value, onApply, onDeny) => {
46+
return (
47+
<ul className="orcha-suggestion-list">
48+
{value.map((author, index) => (
49+
<li key={author.orcid ?? `${index}-${author.name}`}>
50+
<span className="orcha-suggestion-value">{renderAuthor(author)}</span>
51+
{renderSuggestionActions(
52+
() => onApply(index),
53+
() => onDeny(index)
54+
)}
55+
</li>
56+
))}
57+
</ul>
58+
);
59+
};
60+
61+
const renderSuggestedValue = (field, value) => {
62+
if (field === "creators" && Array.isArray(value)) {
63+
return (
64+
<ul className="orcha-suggestion-list">
65+
{value.map((author, index) => (
66+
<li key={author.orcid ?? `${index}-${author.name}`}>
67+
{renderAuthor(author)}
68+
</li>
69+
))}
70+
</ul>
71+
);
72+
}
73+
return <span>{value}</span>;
74+
};
75+
76+
const FieldSuggestionComponent = ({ field, formik }) => {
77+
const suggestions = useSuggestions();
78+
79+
if (!suggestions) return null;
80+
if (!formik) return null;
81+
82+
const { getSuggestion, apply, deny } = suggestions;
83+
const suggestion = getSuggestion(field);
84+
85+
if (!suggestion) return null;
86+
87+
const isCreatorsSuggestion = field === "creators" && Array.isArray(suggestion.value);
88+
89+
if (suggestion.status === "applied") {
90+
return null;
91+
}
92+
93+
if (suggestion.status === "denied") {
94+
return null;
95+
}
96+
97+
return (
98+
<Message size="tiny" info className="orcha-field-suggestion">
99+
<Message.Content>
100+
<Message.Header>
101+
<Icon name="lightbulb outline" />
102+
{i18next.t("Suggested {{field}}", {
103+
field: i18next.t(suggestedFieldLabels[field] ?? "Value"),
104+
})}
105+
</Message.Header>
106+
{isCreatorsSuggestion ? (
107+
renderSuggestedCreators(
108+
suggestion.value,
109+
(index) => apply(formik, field, index),
110+
(index) => deny(field, index)
111+
)
112+
) : (
113+
<div className="orcha-suggestion-row">
114+
<div className="orcha-suggestion-value">
115+
{renderSuggestedValue(field, suggestion.value)}
116+
</div>
117+
{renderSuggestionActions(
118+
() => apply(formik, field),
119+
() => deny(field)
120+
)}
121+
</div>
122+
)}
123+
</Message.Content>
124+
</Message>
125+
);
126+
};
127+
128+
FieldSuggestionComponent.propTypes = {
129+
field: PropTypes.string.isRequired,
130+
formik: PropTypes.object,
131+
};
132+
133+
FieldSuggestionComponent.defaultProps = {
134+
formik: null,
135+
};
136+
137+
export const FieldSuggestion = connectFormik(FieldSuggestionComponent);

0 commit comments

Comments
 (0)