Skip to content

Commit 0fec3f8

Browse files
committed
Add onedrive integration
Signed-off-by: Daishan Peng <[email protected]>
1 parent c3edb5b commit 0fec3f8

File tree

11 files changed

+464
-85
lines changed

11 files changed

+464
-85
lines changed

actions/knowledge/filehelper.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,9 @@ export async function getFileOrFolderSizeInKB(
2929
return 0;
3030
}
3131

32-
export async function getBasename(filePath: string): Promise<string> {
33-
return path.basename(filePath);
34-
}
35-
3632
export async function importFiles(
3733
files: string[],
38-
type: 'local' | 'notion'
34+
type: 'local' | 'notion' | 'onedrive'
3935
): Promise<Map<string, FileDetail>> {
4036
const result: Map<string, FileDetail> = new Map();
4137

actions/knowledge/knowledge.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function ensureFiles(
6565
if (!fs.existsSync(filePath)) {
6666
if (file[1].type === 'local') {
6767
await fs.promises.copyFile(file[0], filePath);
68-
} else if (file[1].type === 'notion') {
68+
} else if (file[1].type === 'notion' || file[1].type === 'onedrive') {
6969
if (
7070
fs.existsSync(filePath) &&
7171
fs.lstatSync(filePath).isSymbolicLink()
@@ -78,7 +78,7 @@ export async function ensureFiles(
7878
}
7979

8080
if (!updateOnly) {
81-
for (const type of ['local', 'notion']) {
81+
for (const type of ['local', 'notion', 'onedrive']) {
8282
if (!fs.existsSync(path.join(dir, type))) {
8383
continue;
8484
}
@@ -124,7 +124,7 @@ export async function getFiles(
124124
if (!fs.existsSync(dir)) {
125125
return result;
126126
}
127-
for (const type of ['local', 'notion']) {
127+
for (const type of ['local', 'notion', 'onedrive']) {
128128
if (!fs.existsSync(path.join(dir, type))) {
129129
continue;
130130
}
@@ -135,7 +135,7 @@ export async function getFiles(
135135
filePath = await fs.promises.readlink(filePath);
136136
}
137137
result.set(filePath, {
138-
type: type as 'local' | 'notion',
138+
type: type as any,
139139
fileName: file,
140140
size: await getFileOrFolderSizeInKB(path.join(dir, type, file)),
141141
});

actions/knowledge/notion.ts

+4-54
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
import fs from 'fs';
44
import path from 'path';
55
import { WORKSPACE_DIR } from '@/config/env';
6-
import {
7-
GPTScript,
8-
PromptFrame,
9-
Run,
10-
RunEventType,
11-
} from '@gptscript-ai/gptscript';
6+
import { runSyncTool } from '@/actions/knowledge/tool';
127

138
export async function isNotionConfigured() {
149
return fs.existsSync(
@@ -22,37 +17,15 @@ export async function isNotionConfigured() {
2217
);
2318
}
2419

25-
function readFilesRecursive(dir: string): string[] {
26-
let results: string[] = [];
27-
28-
const list = fs.readdirSync(dir);
29-
list.forEach((file) => {
30-
if (file === 'metadata.json') return;
31-
const filePath = path.join(dir, file);
32-
const stat = fs.statSync(filePath);
33-
34-
if (stat && stat.isDirectory()) {
35-
// Recursively read the directory
36-
results = results.concat(readFilesRecursive(filePath));
37-
} else {
38-
// Add the file path to the results
39-
results.push(filePath);
40-
}
41-
});
42-
43-
return results;
44-
}
45-
4620
export async function getNotionFiles(): Promise<
4721
Map<string, { url: string; fileName: string }>
4822
> {
4923
const dir = path.join(WORKSPACE_DIR(), 'knowledge', 'integrations', 'notion');
50-
const filePaths = readFilesRecursive(dir);
5124
const metadataFromFiles = fs.readFileSync(path.join(dir, 'metadata.json'));
5225
const metadata = JSON.parse(metadataFromFiles.toString());
5326
const result = new Map<string, { url: string; fileName: string }>();
54-
for (const filePath of filePaths) {
55-
const pageID = path.basename(path.dirname(filePath));
27+
for (const pageID in metadata) {
28+
const filePath = path.join(dir, pageID, metadata[pageID].filename);
5629
result.set(filePath, {
5730
url: metadata[pageID].url,
5831
fileName: path.basename(filePath),
@@ -63,28 +36,5 @@ export async function getNotionFiles(): Promise<
6336
}
6437

6538
export async function runNotionSync(authed: boolean): Promise<void> {
66-
const gptscript = new GPTScript({
67-
DefaultModelProvider: 'github.com/gptscript-ai/gateway-provider',
68-
});
69-
70-
const runningTool = await gptscript.run(
71-
'github.com/gptscript-ai/knowledge-notion-integration',
72-
{
73-
prompt: true,
74-
}
75-
);
76-
if (!authed) {
77-
const handlePromptEvent = (runningTool: Run) => {
78-
return new Promise<string>((resolve) => {
79-
runningTool.on(RunEventType.Prompt, (data: PromptFrame) => {
80-
resolve(data.id);
81-
});
82-
});
83-
};
84-
85-
const id = await handlePromptEvent(runningTool);
86-
await gptscript.promptResponse({ id, responses: {} });
87-
}
88-
await runningTool.text();
89-
return;
39+
return runSyncTool(authed, 'notion');
9040
}

actions/knowledge/onedrive.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use server';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { WORKSPACE_DIR } from '@/config/env';
5+
import { runSyncTool } from '@/actions/knowledge/tool';
6+
7+
export async function isOneDriveConfigured() {
8+
return fs.existsSync(
9+
path.join(
10+
WORKSPACE_DIR(),
11+
'knowledge',
12+
'integrations',
13+
'onedrive',
14+
'metadata.json'
15+
)
16+
);
17+
}
18+
19+
export async function getOneDriveFiles(): Promise<
20+
Map<string, { url: string; fileName: string }>
21+
> {
22+
const dir = path.join(
23+
WORKSPACE_DIR(),
24+
'knowledge',
25+
'integrations',
26+
'onedrive'
27+
);
28+
const metadataFromFiles = fs.readFileSync(path.join(dir, 'metadata.json'));
29+
const metadata = JSON.parse(metadataFromFiles.toString());
30+
const result = new Map<string, { url: string; fileName: string }>();
31+
for (const documentID in metadata) {
32+
result.set(path.join(dir, documentID, metadata[documentID].fileName), {
33+
url: metadata[documentID].url,
34+
fileName: metadata[documentID].fileName,
35+
});
36+
}
37+
return result;
38+
}
39+
40+
// syncFiles syncs all files only when they are selected
41+
// todo: we can stop syncing once file is no longer used by any other script
42+
43+
export async function runOneDriveSync(authed: boolean): Promise<void> {
44+
return runSyncTool(authed, 'onedrive');
45+
}

actions/knowledge/tool.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use server';
2+
3+
import {
4+
GPTScript,
5+
PromptFrame,
6+
Run,
7+
RunEventType,
8+
} from '@gptscript-ai/gptscript';
9+
import path from 'path';
10+
import { WORKSPACE_DIR } from '@/config/env';
11+
import fs from 'fs';
12+
13+
export async function runSyncTool(
14+
authed: boolean,
15+
tool: 'notion' | 'onedrive'
16+
): Promise<void> {
17+
const gptscript = new GPTScript({
18+
DefaultModelProvider: 'github.com/gptscript-ai/gateway-provider',
19+
});
20+
21+
let toolUrl = '';
22+
if (tool === 'notion') {
23+
toolUrl = 'github.com/gptscript-ai/knowledge-notion-integration@46a273e';
24+
} else if (tool === 'onedrive') {
25+
toolUrl = 'github.com/gptscript-ai/knowledge-onedrive-integration@a85a498';
26+
}
27+
const runningTool = await gptscript.run(toolUrl, {
28+
prompt: true,
29+
});
30+
if (!authed) {
31+
const handlePromptEvent = (runningTool: Run) => {
32+
return new Promise<string>((resolve) => {
33+
runningTool.on(RunEventType.Prompt, (data: PromptFrame) => {
34+
resolve(data.id);
35+
});
36+
});
37+
};
38+
39+
const id = await handlePromptEvent(runningTool);
40+
await gptscript.promptResponse({ id, responses: {} });
41+
}
42+
await runningTool.text();
43+
return;
44+
}
45+
46+
export async function syncFiles(
47+
selectedFiles: string[],
48+
type: 'notion' | 'onedrive'
49+
): Promise<void> {
50+
const dir = path.join(WORKSPACE_DIR(), 'knowledge', 'integrations', type);
51+
const metadataFromFiles = fs.readFileSync(path.join(dir, 'metadata.json'));
52+
const metadata = JSON.parse(metadataFromFiles.toString());
53+
for (const file of selectedFiles) {
54+
const documentID = path.basename(path.dirname(file));
55+
const detail = metadata[documentID];
56+
detail.sync = true;
57+
metadata[documentID] = detail;
58+
}
59+
fs.writeFileSync(path.join(dir, 'metadata.json'), JSON.stringify(metadata));
60+
await runSyncTool(true, type);
61+
return;
62+
}

actions/knowledge/util.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export interface FileDetail {
22
fileName: string;
33
size: number;
4-
type: 'local' | 'notion';
4+
type: 'local' | 'notion' | 'onedrive';
55
}
66

77
export function gatewayTool(): string {

components/edit/configure.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { RiFoldersLine } from 'react-icons/ri';
3131
import FileModal from '@/components/knowledge/FileModal';
3232
import { gatewayTool } from '@/actions/knowledge/util';
3333
import { importFiles } from '@/actions/knowledge/filehelper';
34+
import { DiOnedrive } from 'react-icons/di';
3435

3536
interface ConfigureProps {
3637
collapsed?: boolean;
@@ -191,14 +192,17 @@ const Configure: React.FC<ConfigureProps> = ({ collapsed }) => {
191192
([key, fileDetail], _) => (
192193
<div key={key} className="flex space-x-2">
193194
<div className="flex flex-row w-full border-2 justify-between truncate dark:border-zinc-700 text-sm pl-2 rounded-lg">
194-
<div className="flex items-center">
195+
<div className="flex items-center overflow-auto">
195196
{fileDetail.type === 'local' && (
196197
<RiFileSearchLine className="justify-start mr-2" />
197198
)}
198199
{fileDetail.type === 'notion' && (
199200
<RiNotionFill className="justify-start mr-2" />
200201
)}
201-
<div className="flex flex-row justify-start overflow-x-auto">
202+
{fileDetail.type === 'onedrive' && (
203+
<DiOnedrive className="justify-start mr-2" />
204+
)}
205+
<div className="flex flex-row justify-start overflow-auto">
202206
<p className="capitalize text-left">
203207
{fileDetail.fileName}
204208
</p>

components/knowledge/FileModal.tsx

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import {
2+
Avatar,
23
Button,
34
Modal,
45
ModalBody,
56
ModalContent,
67
useDisclosure,
78
} from '@nextui-org/react';
8-
import { Image } from '@nextui-org/image';
99
import { BiPlus } from 'react-icons/bi';
1010
import { NotionFileModal } from '@/components/knowledge/Notion';
1111
import { isNotionConfigured, runNotionSync } from '@/actions/knowledge/notion';
1212
import { useState } from 'react';
13+
import { OnedriveFileModal } from '@/components/knowledge/OneDrive';
14+
import {
15+
isOneDriveConfigured,
16+
runOneDriveSync,
17+
} from '@/actions/knowledge/onedrive';
1318

1419
interface FileModalProps {
1520
isOpen: boolean;
@@ -19,9 +24,12 @@ interface FileModalProps {
1924

2025
const FileModal = ({ isOpen, onClose, handleAddFile }: FileModalProps) => {
2126
const notionModal = useDisclosure();
27+
const onedriveModal = useDisclosure();
2228
const [isSyncing, setIsSyncing] = useState(false);
2329
const [notionConfigured, setNotionConfigured] = useState(false);
2430
const [notionSyncError, setNotionSyncError] = useState('');
31+
const [oneDriveConfigured, setOneDriveConfigured] = useState(false);
32+
const [oneDriveSyncError, setOneDriveSyncError] = useState('');
2533

2634
const onClickNotion = async () => {
2735
onClose();
@@ -40,6 +48,23 @@ const FileModal = ({ isOpen, onClose, handleAddFile }: FileModalProps) => {
4048
}
4149
};
4250

51+
const onClickOnedrive = async () => {
52+
onClose();
53+
onedriveModal.onOpen();
54+
const isConfigured = await isOneDriveConfigured();
55+
if (!isConfigured) {
56+
setIsSyncing(true);
57+
try {
58+
await runOneDriveSync(false);
59+
setOneDriveConfigured(true);
60+
} catch (e) {
61+
setOneDriveSyncError((e as Error).toString());
62+
} finally {
63+
setIsSyncing(false);
64+
}
65+
}
66+
};
67+
4368
return (
4469
<>
4570
<Modal
@@ -68,11 +93,30 @@ const FileModal = ({ isOpen, onClose, handleAddFile }: FileModalProps) => {
6893
onClick={onClickNotion}
6994
className="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent hover:cursor-pointer"
7095
>
71-
<Image className="h-5 w-5" src="notion.svg" alt="Notion Icon" />
96+
<Avatar
97+
size="sm"
98+
src="notion.svg"
99+
alt="Notion Icon"
100+
classNames={{ base: 'p-1.5 bg-white' }}
101+
/>
72102
<span className="text-sm font-semibold leading-6">
73103
Sync From Notion
74104
</span>
75105
</Button>
106+
<Button
107+
onClick={onClickOnedrive}
108+
className="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent hover:cursor-pointer"
109+
>
110+
<Avatar
111+
size="sm"
112+
src="onedrive.svg"
113+
alt="OneDrive Icon"
114+
classNames={{ base: 'p-1.5 bg-white' }}
115+
/>
116+
<span className="text-sm font-semibold leading-6">
117+
Sync From OneDrive
118+
</span>
119+
</Button>
76120
</ModalBody>
77121
</ModalContent>
78122
</Modal>
@@ -86,6 +130,16 @@ const FileModal = ({ isOpen, onClose, handleAddFile }: FileModalProps) => {
86130
syncError={notionSyncError}
87131
setSyncError={setNotionSyncError}
88132
/>
133+
<OnedriveFileModal
134+
isOpen={onedriveModal.isOpen}
135+
onClose={onedriveModal.onClose}
136+
isSyncing={isSyncing}
137+
setIsSyncing={setIsSyncing}
138+
onedriveConfigured={oneDriveConfigured}
139+
setOnedriveConfigured={setOneDriveConfigured}
140+
syncError={oneDriveSyncError}
141+
setSyncError={setOneDriveSyncError}
142+
/>
89143
</>
90144
);
91145
};

0 commit comments

Comments
 (0)