📁 feat: Integrate SharePoint File Picker and Download Workflow (#8651)

* feat(sharepoint): integrate SharePoint file picker and download workflow
Introduces end‑to‑end SharePoint import support:
* Token exchange with Microsoft Graph and scope management (`useSharePointToken`)
* Re‑usable hooks: `useSharePointPicker`, `useSharePointDownload`,
  `useSharePointFileHandling`
* FileSearch dropdown now offers **From Local Machine** / **From SharePoint**
  sources and gracefully falls back when SharePoint is disabled
* Agent upload model, `AttachFileMenu`, and `DropdownPopup` extended for
  SharePoint files and sub‑menus
* Blurry overlay with progress indicator and `maxSelectionCount` limit during
  downloads
* Cache‑flush utility (`config/flush-cache.js`) supporting Redis & filesystem,
  with dry‑run and npm script
* Updated `SharePointIcon` (uses `currentColor`) and new i18n keys
* Bug fixes: placeholder syntax in progress message, picker event‑listener
  cleanup
* Misc style and performance optimizations

* Fix ESLint warnings

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
This commit is contained in:
Danny Avila 2025-07-25 00:03:23 -04:00
parent b6413b06bc
commit a955097faf
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
40 changed files with 2500 additions and 123 deletions

View file

@ -29,7 +29,8 @@ interface DropdownProps {
type MenuProps = Omit<
DropdownProps,
'trigger' | 'isOpen' | 'setIsOpen' | 'focusLoop' | 'mountByState'
>;
> &
Ariakit.MenuProps;
const DropdownPopup: React.FC<DropdownProps> = ({
trigger,
@ -70,7 +71,9 @@ const Menu: React.FC<MenuProps> = ({
finalFocus,
unmountOnHide,
preserveTabOrder,
...props
}) => {
const menuStore = Ariakit.useMenuStore();
const menu = Ariakit.useMenuContext();
return (
<Ariakit.Menu
@ -83,13 +86,53 @@ const Menu: React.FC<MenuProps> = ({
unmountOnHide={unmountOnHide}
preserveTabOrder={preserveTabOrder}
className={cn('popover-ui z-50', className)}
{...props}
>
{items
.filter((item) => item.show !== false)
.map((item, index) => {
const { subItems } = item;
if (item.separate === true) {
return <Ariakit.MenuSeparator key={index} className="my-1 h-px bg-white/10" />;
}
if (subItems && subItems.length > 0) {
return (
<Ariakit.MenuProvider
store={menuStore}
key={`${keyPrefix ?? ''}${index}-${item.id ?? ''}-provider`}
>
<Ariakit.MenuButton
className={cn(
'group flex w-full cursor-pointer items-center justify-between gap-2 rounded-lg px-3 py-3.5 text-sm text-text-primary outline-none transition-colors duration-200 hover:bg-surface-hover focus:bg-surface-hover md:px-2.5 md:py-2',
itemClassName,
)}
disabled={item.disabled}
id={item.id}
render={item.render}
ref={item.ref}
// hideOnClick={item.hideOnClick}
>
<span className="flex items-center gap-2">
{item.icon != null && (
<span className={cn('mr-2 size-4', iconClassName)} aria-hidden="true">
{item.icon}
</span>
)}
{item.label}
</span>
<Ariakit.MenuButtonArrow className="stroke-1 text-base opacity-75" />
</Ariakit.MenuButton>
<Menu
items={subItems}
menuId={`${menuId}-${index}`}
key={`${keyPrefix ?? ''}${index}-${item.id ?? ''}`}
gutter={12}
portal={true}
/>
</Ariakit.MenuProvider>
);
}
return (
<Ariakit.MenuItem
key={`${keyPrefix ?? ''}${index}-${item.id ?? ''}`}

View file

@ -0,0 +1,20 @@
export default function CodePaths() {
return (
<>
<path
d="M21.333 23L26.333 18L21.333 13"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.667 13L9.66699 18L14.667 23"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
}

View file

@ -0,0 +1,28 @@
import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
export default function FileIcon({
file,
fileType,
}: {
file?: Partial<ExtendedFile | TFile>;
fileType: {
fill: string;
paths: React.FC;
title: string;
};
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 36 36"
fill="none"
className="h-10 w-10 flex-shrink-0"
width="36"
height="36"
>
<rect width="36" height="36" rx="6" fill={fileType.fill} />
{(file?.['progress'] ?? 1) >= 1 && <>{<fileType.paths />}</>}
</svg>
);
}

View file

@ -0,0 +1,20 @@
export default function FilePaths() {
return (
<>
<path
d="M18.833 9.66663H12.9997C12.5576 9.66663 12.1337 9.84222 11.8212 10.1548C11.5086 10.4673 11.333 10.8913 11.333 11.3333V24.6666C11.333 25.1087 11.5086 25.5326 11.8212 25.8451C12.1337 26.1577 12.5576 26.3333 12.9997 26.3333H22.9997C23.4417 26.3333 23.8656 26.1577 24.1782 25.8451C24.4907 25.5326 24.6663 25.1087 24.6663 24.6666V15.5L18.833 9.66663Z"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18.833 9.66663V15.5H24.6663"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
}

View file

@ -0,0 +1,8 @@
import React from 'react';
export default function SharePointIcon({ className = '' }) {
return (
<svg fill="currentColor" width="24" height="24" viewBox="0 0 24 24" className={className}>
<path d="M24 13.5q0 1.242-.475 2.332-.474 1.09-1.289 1.904-.814.815-1.904 1.29-1.09.474-2.332.474-.762 0-1.523-.2-.106.997-.557 1.858-.451.862-1.154 1.494-.704.633-1.606.99-.902.358-1.91.358-1.09 0-2.045-.416-.955-.416-1.664-1.125-.709-.709-1.125-1.664Q6 19.84 6 18.75q0-.188.018-.375.017-.188.04-.375H.997q-.41 0-.703-.293T0 17.004V6.996q0-.41.293-.703T.996 6h3.54q.14-1.277.726-2.373.586-1.096 1.488-1.904Q7.652.914 8.807.457 9.96 0 11.25 0q1.395 0 2.625.533T16.02 1.98q.914.915 1.447 2.145T18 6.75q0 .188-.012.375-.011.188-.035.375 1.242 0 2.344.469 1.101.468 1.928 1.277.826.809 1.3 1.904Q24 12.246 24 13.5zm-12.75-12q-.973 0-1.857.34-.885.34-1.577.943-.691.604-1.154 1.43Q6.2 5.039 6.06 6h4.945q.41 0 .703.293t.293.703v4.945l.21-.035q.212-.75.61-1.424.399-.673.944-1.218.545-.545 1.213-.944.668-.398 1.43-.61.093-.503.093-.96 0-1.09-.416-2.045-.416-.955-1.125-1.664-.709-.709-1.664-1.125Q12.34 1.5 11.25 1.5zM6.117 15.902q.54 0 1.06-.111.522-.111.932-.37.41-.257.662-.679.252-.422.252-1.055 0-.632-.263-1.054-.264-.422-.662-.703-.399-.282-.856-.463l-.855-.34q-.399-.158-.662-.334-.264-.176-.264-.445 0-.2.14-.323.141-.123.335-.193.193-.07.404-.094.21-.023.351-.023.598 0 1.055.152.457.153.95.457V8.543q-.282-.082-.522-.14-.24-.06-.475-.1-.234-.041-.486-.059-.252-.017-.557-.017-.515 0-1.054.117-.54.117-.979.375-.44.258-.715.68-.275.421-.275 1.03 0 .598.263.997.264.398.663.68.398.28.855.474l.856.363q.398.17.662.358.263.187.263.457 0 .222-.123.351-.123.13-.31.2-.188.07-.393.087-.205.018-.369.018-.703 0-1.248-.234-.545-.235-1.107-.621v1.875q1.195.468 2.472.468zM11.25 22.5q.773 0 1.453-.293t1.19-.803q.51-.51.808-1.195.299-.686.299-1.459 0-.668-.223-1.277-.222-.61-.62-1.096-.4-.486-.95-.826-.55-.34-1.207-.48v1.933q0 .41-.293.703t-.703.293H7.57q-.07.375-.07.75 0 .773.293 1.459t.803 1.195q.51.51 1.195.803.686.293 1.459.293zM18 18q.926 0 1.746-.352.82-.351 1.436-.966.615-.616.966-1.43.352-.815.352-1.752 0-.926-.352-1.746-.351-.82-.966-1.436-.616-.615-1.436-.966Q18.926 9 18 9t-1.74.357q-.815.358-1.43.973t-.973 1.43q-.357.814-.357 1.74 0 .129.006.258t.017.258q.551.27 1.02.65t.838.855q.369.475.627 1.026.258.55.387 1.148Q17.18 18 18 18Z" />
</svg>
);
}

View file

@ -0,0 +1,13 @@
export default function SheetPaths() {
return (
<>
<path
d="M15.5 10.5H12.1667C11.2462 10.5 10.5 11.2462 10.5 12.1667V13.5V18M15.5 10.5H23.8333C24.7538 10.5 25.5 11.2462 25.5 12.1667V13.5V18M15.5 10.5V25.5M15.5 25.5H18H23.8333C24.7538 25.5 25.5 24.7538 25.5 23.8333V18M15.5 25.5H12.1667C11.2462 25.5 10.5 24.7538 10.5 23.8333V18M10.5 18H25.5"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
}

View file

@ -0,0 +1,41 @@
export default function TextPaths() {
return (
<>
<path
d="M19.6663 9.66663H12.9997C12.5576 9.66663 12.1337 9.84222 11.8212 10.1548C11.5086 10.4673 11.333 10.8913 11.333 11.3333V24.6666C11.333 25.1087 11.5086 25.5326 11.8212 25.8451C12.1337 26.1577 12.5576 26.3333 12.9997 26.3333H22.9997C23.4417 26.3333 23.8656 26.1577 24.1782 25.8451C24.4907 25.5326 24.6663 25.1087 24.6663 24.6666V14.6666L19.6663 9.66663Z"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M19.667 9.66663V14.6666H24.667"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.3337 18.8334H14.667"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.3337 22.1666H14.667"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.3337 15.5H15.5003H14.667"
stroke="white"
strokeWidth="1.66667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
}

View file

@ -65,3 +65,9 @@ export { default as PersonalizationIcon } from './PersonalizationIcon';
export { default as MCPIcon } from './MCPIcon';
export { default as VectorIcon } from './VectorIcon';
export { default as SquirclePlusIcon } from './SquirclePlusIcon';
export { default as CodePaths } from './CodePaths';
export { default as FileIcon } from './FileIcon';
export { default as FilePaths } from './FilePaths';
export { default as SheetPaths } from './SheetPaths';
export { default as TextPaths } from './TextPaths';
export { default as SharePointIcon } from './SharePointIcon';

View file

@ -305,3 +305,7 @@ export const verifyTwoFactorTemp = () => '/api/auth/2fa/verify-temp';
export const memories = () => '/api/memories';
export const memory = (key: string) => `${memories()}/${encodeURIComponent(key)}`;
export const memoryPreferences = () => `${memories()}/preferences`;
// SharePoint Graph API Token
export const graphToken = (scopes: string) =>
`/api/auth/graph-token?scopes=${encodeURIComponent(scopes)}`;

View file

@ -597,6 +597,11 @@ export type TStartupConfig = {
instanceProjectId: string;
bundlerURL?: string;
staticBundlerURL?: string;
sharePointFilePickerEnabled?: boolean;
sharePointBaseUrl?: string;
sharePointPickerGraphScope?: string;
sharePointPickerSharePointScope?: string;
openidReuseTokens?: boolean;
webSearch?: {
searchProvider?: SearchProviders;
scraperType?: ScraperTypes;

View file

@ -858,3 +858,8 @@ export const createMemory = (data: {
}): Promise<{ created: boolean; memory: q.TUserMemory }> => {
return request.post(endpoints.memories(), data);
};
// SharePoint Graph API Token
export function getGraphApiToken(params: q.GraphTokenParams): Promise<q.GraphTokenResponse> {
return request.get(endpoints.graphToken(params.scopes));
}

View file

@ -50,6 +50,7 @@ export enum QueryKeys {
banner = 'banner',
/* Memories */
memories = 'memories',
graphToken = 'graphToken',
}
// Dynamic query keys that require parameters

View file

@ -147,3 +147,15 @@ export interface MCPAuthValuesResponse {
serverName: string;
authValueFlags: Record<string, boolean>;
}
/* SharePoint Graph API Token */
export type GraphTokenParams = {
scopes: string;
};
export type GraphTokenResponse = {
access_token: string;
token_type: string;
expires_in: number;
scope: string;
};