Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Book): improved AI summary and tag generation #453

Merged
merged 1 commit into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 94 additions & 37 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Homework,
HomeworkSearchParams,
PageDetailsResponse,
PageTag,
PeerReview,
Project,
ProjectFile,
Expand All @@ -33,6 +34,9 @@ import {
Sender,
ProjectSummary,
InvitationsResponse,
PageSimpleWTags,
PageSimpleWOverview,
TableOfContentsDetailed,
} from "./types";
import {
AddableProjectTeamMember,
Expand Down Expand Up @@ -343,6 +347,15 @@ class API {
return res;
}

async getBookPagesDetails(bookID: string) {
const res = await axios.get<
{
toc: TableOfContentsDetailed;
} & ConductorBaseResponse
>(`/commons/book/${bookID}/pages-details`);
return res;
}

async getPageDetails(pageID: string, coverPageID: string) {
const res = await axios.get<PageDetailsResponse & ConductorBaseResponse>(
`/commons/pages/${pageID}?coverPageID=${coverPageID}`
Expand All @@ -369,12 +382,38 @@ class API {
}

/**
* Generates and applies an AI-generated summary to all pages in a book
* @param {string} pageID - the cover page of the book to apply the summaries to
* Generates and applies AI-generated summaries, tags, or both, to all pages in a book
* @param {string} bookID - the cover page of the book to apply the summaries to
*/
async batchApplyPageAISummary(pageID: string) {
const res = await axios.patch<ConductorBaseResponse>(
`/commons/pages/${pageID}/ai-summary/batch`
async batchGenerateAIMetadata(
bookID: string,
summaries: boolean,
tags: boolean
) {
const res = await axios.post<ConductorBaseResponse>(
`/commons/book/${bookID}/ai-metadata-batch`,
{
summaries,
tags,
}
);
return res;
}

/**
* Applies user-supplied summaries and tags to the respective pages in a book
* @param {string} bookID - the cover page of the book to apply the metadata to
* @param {Array<{ id: string; summary: string; tags: string[] }>} pages - the pages & data to update
*/
async batchUpdateBookMetadata(
bookID: string,
pages: { id: string; summary: string; tags: string[] }[]
) {
const res = await axios.post<ConductorBaseResponse>(
`/commons/book/${bookID}/update-metadata-batch`,
{
pages,
}
);
return res;
}
Expand All @@ -391,6 +430,21 @@ class API {
return res;
}

async bulkUpdatePageTags(
bookID: string,
pages: { id: string; tags: string[] }[]
) {
const res = await axios.put<
{
failed: number;
processed: number;
} & ConductorBaseResponse
>(`/commons/book/${bookID}/page-tags`, {
pages,
});
return res;
}

// Central Identity
async getCentralIdentityOrgs({
activePage,
Expand Down Expand Up @@ -1021,38 +1075,40 @@ class API {
email: string,
role: string
) {
const res = await axios.post<{
responseInvitation: BaseInvitation
} & ConductorBaseResponse>(
`/project-invitations/${projectID}`,
const res = await axios.post<
{
email,
role
}
);
responseInvitation: BaseInvitation;
} & ConductorBaseResponse
>(`/project-invitations/${projectID}`, {
email,
role,
});
return res.data;
}

async getAllProjectInvitations(
projectID: string,
page: number = 1,
projectID: string,
page: number = 1,
limit: number
) {
const res = await axios.get<{
data: InvitationsResponse;
} & ConductorBaseResponse>(`/project-invitations/project/${projectID}`, {
const res = await axios.get<
{
data: InvitationsResponse;
} & ConductorBaseResponse
>(`/project-invitations/project/${projectID}`, {
params: { page, limit },
});
return res.data;
}

async getProjectInvitation(
inviteID: string,
token: string | null
) {
const res = await axios.get<{
invitation: BaseInvitation & {sender: Sender} & {project: ProjectSummary};
} & ConductorBaseResponse>(`/project-invitations/${inviteID}`, {
async getProjectInvitation(inviteID: string, token: string | null) {
const res = await axios.get<
{
invitation: BaseInvitation & { sender: Sender } & {
project: ProjectSummary;
};
} & ConductorBaseResponse
>(`/project-invitations/${inviteID}`, {
params: { token },
});
return res.data;
Expand All @@ -1064,34 +1120,35 @@ class API {
deleted: boolean;
} & ConductorBaseResponse
>(`/project-invitations/${invitationId}`);

return res.data;
}

async updateInvitationRole(inviteID: string, role: string) {
const res = await axios.put<
{
updatedInvitation: BaseInvitation;
} & ConductorBaseResponse
>(`/project-invitations/${inviteID}/update`, { role });

return res.data;
}

async acceptProjectInvitation(inviteID: string | null, token: string | null){
const res = await axios.post<{
data: string
} & ConductorBaseResponse>(

return res.data;
}

async acceptProjectInvitation(inviteID: string | null, token: string | null) {
const res = await axios.post<
{
data: string;
} & ConductorBaseResponse
>(
`/project-invitation/${inviteID}/accept`,
{},
{
params: {token},
params: { token },
}
);

return res.data;
}

}

export default new API();
40 changes: 40 additions & 0 deletions client/src/components/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button, Modal, SemanticCOLORS } from "semantic-ui-react";

interface ConfirmModalProps {
text?: string;
onConfirm: () => void;
onCancel: () => void;
confirmText?: string;
cancelText?: string;
confirmColor?: SemanticCOLORS;
cancelColor?: SemanticCOLORS;
}

const ConfirmModal: React.FC<ConfirmModalProps> = ({
text = "Are you sure?",
onConfirm,
onCancel,
confirmText = "Confirm",
cancelText = "Cancel",
confirmColor = "green",
cancelColor = undefined,
}) => {
return (
<Modal size="large" open={true} onClose={onCancel}>
<Modal.Header>Confirm</Modal.Header>
<Modal.Content>
<p>{text}</p>
</Modal.Content>
<Modal.Actions>
<Button color={cancelColor} onClick={onCancel}>
{cancelText}
</Button>
<Button color={confirmColor} onClick={onConfirm}>
{confirmText}
</Button>
</Modal.Actions>
</Modal>
);
};

export default ConfirmModal;
39 changes: 32 additions & 7 deletions client/src/components/ControlledInputs/CtlTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { Form, FormTextAreaProps } from "semantic-ui-react";
import { ControlledInputProps } from "../../types";
import "../../styles/global.css";

interface CtlTextAreaProps extends FormTextAreaProps {
interface CtlTextAreaProps extends React.HTMLProps<HTMLTextAreaElement> {
label?: string;
required?: boolean;
maxLength?: number;
showRemaining?: boolean;
fluid?: boolean;
bordered?: boolean;
error?: string;
}

/**
Expand Down Expand Up @@ -49,24 +50,48 @@ export default function CtlTextArea<
field: { value, onChange, onBlur },
fieldState: { error },
}) => (
<div className={`${restClassName ?? ""}`}>
<div className={`flex flex-col ${restClassName ?? ""}`}>
{label && (
<label
className={`form-field-label ${required ? "form-required" : ""}`}
className={`form-field-label !font-semibold mb-1.5 ${required ? "form-required" : ""}`}
>
{label}
</label>
)}
<Form.TextArea
<textarea
value={value}
onChange={onChange}
onChange={(e) => onChange(e.target.value)}
onBlur={onBlur}
error={error?.message}
className={`!m-0 ${fluid ? "fluid-textarea" : ""} ${bordered ? 'border border-slate-400 rounded-md padded-textarea': ''} ${bordered && showRemaining && maxLength && getRemainingChars(value) < 0 ? '!border-red-500' : ''}`}
className={`!m-0 ${fluid ? "!w-full" : ""} ${
bordered
? "border border-slate-400 rounded-md p-[0.5em]"
: ""
} ${
bordered &&
showRemaining &&
maxLength &&
getRemainingChars(value) < 0
? "!border-red-500"
: ""
}`}
rows={3}
{...rest}
/>
{/* <Form.TextArea
value={value}
onChange={onChange}
onBlur={onBlur}
error={error?.message}
className={`!m-0.5 ${fluid ? "fluid-textarea" : ""} ${bordered ? 'border border-slate-400 rounded-md padded-textarea': ''} ${bordered && showRemaining && maxLength && getRemainingChars(value) < 0 ? '!border-red-500' : ''}`}
{...rest}
/> */}
{maxLength && showRemaining && typeof value === "string" && (
<span className={`font-semibold small-text ${getRemainingChars(value) < 0 ? '!text-red-500' : ''}`}>
<span
className={`font-semibold text-xs mt-0.5 ${
getRemainingChars(value) < 0 ? "!text-red-500" : ""
}`}
>
Characters remaining: {getRemainingChars(value)}
</span>
)}
Expand Down
8 changes: 6 additions & 2 deletions client/src/components/LoadingSpinner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const LoadingSpinner: React.FC = () => {
interface LoadingSpinnerProps {
text?: string;
}

const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ text }) => {
return (
<div>
<div className="ui active inverted dimmer">
<div className="ui text loader">
<span>Loading</span>
<span>{text || "Loading"}</span>
</div>
</div>
</div>
Expand Down
Loading