@enhearten/knowledge-base-module 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/backend/.env CHANGED
@@ -1,3 +1,3 @@
1
1
  COMPOSIO_API_KEY=ak_98QH038j63Jg1gVRIK_5
2
- GOOGLE_API_KEY=AIzaSyDkELFOfve9WPbT7wDiOqwULHmrMdbS1zw
2
+ GOOGLE_API_KEY=AIzaSyBQVW8kgEo8E5JLD9qCgh0JNbzela20Keo
3
3
  PORT=3000
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface SettingsModalProps {
3
+ open: boolean;
4
+ onClose: () => void;
5
+ }
6
+ export declare const SettingsModal: React.FC<SettingsModalProps>;
7
+ export {};
8
+ //# sourceMappingURL=SettingsModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SettingsModal.d.ts","sourceRoot":"","sources":["../../src/components/SettingsModal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAMnD,UAAU,kBAAkB;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA+ItD,CAAC"}
@@ -0,0 +1,84 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Button, Input, Label, Sheet, Text, XStack, YStack, Theme, Spinner } from 'tamagui';
3
+ import { MaterialCommunityIcons } from '@expo/vector-icons';
4
+ import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import { configure } from '../services/api';
6
+ export const SettingsModal = ({ open, onClose }) => {
7
+ const [backendUrl, setBackendUrl] = useState('');
8
+ const [composioKey, setComposioKey] = useState('');
9
+ const [googleKey, setGoogleKey] = useState('');
10
+ const [loading, setLoading] = useState(false);
11
+ const [saving, setSaving] = useState(false);
12
+ useEffect(() => {
13
+ if (open) {
14
+ loadSettings();
15
+ }
16
+ }, [open]);
17
+ const loadSettings = async () => {
18
+ setLoading(true);
19
+ try {
20
+ const storedUrl = await AsyncStorage.getItem('kb_backend_url');
21
+ const storedComposioKey = await AsyncStorage.getItem('kb_composio_key');
22
+ const storedGoogleKey = await AsyncStorage.getItem('kb_google_key');
23
+ if (storedUrl)
24
+ setBackendUrl(storedUrl);
25
+ if (storedComposioKey)
26
+ setComposioKey(storedComposioKey);
27
+ if (storedGoogleKey)
28
+ setGoogleKey(storedGoogleKey);
29
+ }
30
+ catch (error) {
31
+ console.error('Failed to load settings', error);
32
+ }
33
+ finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+ const handleSave = async () => {
38
+ setSaving(true);
39
+ try {
40
+ // Remove trailing slash from URL if present
41
+ const cleanUrl = backendUrl.replace(/\/$/, '');
42
+ await AsyncStorage.setItem('kb_backend_url', cleanUrl);
43
+ await AsyncStorage.setItem('kb_composio_key', composioKey);
44
+ await AsyncStorage.setItem('kb_google_key', googleKey);
45
+ configure({
46
+ backendUrl: cleanUrl,
47
+ headers: {
48
+ 'x-composio-key': composioKey,
49
+ 'x-google-key': googleKey
50
+ }
51
+ });
52
+ onClose();
53
+ }
54
+ catch (error) {
55
+ console.error('Failed to save settings', error);
56
+ }
57
+ finally {
58
+ setSaving(false);
59
+ }
60
+ };
61
+ return (React.createElement(Theme, { name: "light" },
62
+ React.createElement(Sheet, { modal: true, open: open, onOpenChange: (isOpen) => !isOpen && onClose(), snapPoints: [85], dismissOnSnapToBottom: true },
63
+ React.createElement(Sheet.Overlay, null),
64
+ React.createElement(Sheet.Frame, { padding: "$4", backgroundColor: "white" },
65
+ React.createElement(Sheet.Handle, null),
66
+ React.createElement(YStack, { space: "$4", paddingTop: "$4" },
67
+ React.createElement(XStack, { alignItems: "center", space: "$2" },
68
+ React.createElement(MaterialCommunityIcons, { name: "cog", size: 28, color: "#333" }),
69
+ React.createElement(Text, { fontSize: "$6", fontWeight: "bold" }, "Settings")),
70
+ React.createElement(Text, { color: "$gray11" }, "Configure your Cloudflare Worker URL and API keys."),
71
+ loading ? (React.createElement(Spinner, { size: "large", color: "$blue10" })) : (React.createElement(YStack, { space: "$4" },
72
+ React.createElement(YStack, { space: "$2" },
73
+ React.createElement(Label, { htmlFor: "backendUrl", fontWeight: "600" }, "Backend URL (Cloudflare Worker)"),
74
+ React.createElement(Input, { id: "backendUrl", placeholder: "https://knowledge-base-worker.your-subdomain.workers.dev", value: backendUrl, onChangeText: setBackendUrl, autoCapitalize: "none", autoCorrect: false })),
75
+ React.createElement(YStack, { space: "$2" },
76
+ React.createElement(Label, { htmlFor: "composioKey", fontWeight: "600" }, "Composio API Key"),
77
+ React.createElement(Input, { id: "composioKey", placeholder: "Enter your Composio API Key", value: composioKey, onChangeText: setComposioKey, secureTextEntry: true })),
78
+ React.createElement(YStack, { space: "$2" },
79
+ React.createElement(Label, { htmlFor: "googleKey", fontWeight: "600" }, "Google Gemini API Key"),
80
+ React.createElement(Input, { id: "googleKey", placeholder: "Enter your Google Gemini API Key", value: googleKey, onChangeText: setGoogleKey, secureTextEntry: true })),
81
+ React.createElement(XStack, { gap: "$3", justifyContent: "flex-end", marginTop: "$4" },
82
+ React.createElement(Button, { size: "$4", variant: "outlined", onPress: onClose, disabled: saving }, "Cancel"),
83
+ React.createElement(Button, { size: "$4", themeInverse: true, onPress: handleSave, disabled: saving, icon: saving ? React.createElement(Spinner, { size: "small", color: "white" }) : undefined }, saving ? 'Saving...' : 'Save Settings')))))))));
84
+ };
@@ -2,6 +2,7 @@ export declare const useServiceConnection: (appName: string, entityId: string, s
2
2
  connected: boolean;
3
3
  loading: boolean;
4
4
  disconnecting: boolean;
5
+ error: string | null;
5
6
  handleConnect: () => Promise<void>;
6
7
  handleCancel: () => void;
7
8
  handleDisconnect: () => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"useServiceConnection.d.ts","sourceRoot":"","sources":["../../src/hooks/useServiceConnection.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,EAAE,cAAa,MAAM,EAAc;;;;;;;CAgFxG,CAAC"}
1
+ {"version":3,"file":"useServiceConnection.d.ts","sourceRoot":"","sources":["../../src/hooks/useServiceConnection.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,EAAE,cAAa,MAAM,EAAc;;;;;;;;CAgHxG,CAAC"}
@@ -1,12 +1,15 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { Linking } from 'react-native';
3
- import { startConnection, getConnectionStatus, disconnectApp } from '../services/api';
2
+ import { Linking, Alert } from 'react-native';
3
+ import { startConnection, getConnectionStatus, disconnectApp, isConfigured } from '../services/api';
4
4
  export const useServiceConnection = (appName, entityId, searchTerms = [appName]) => {
5
5
  const [loading, setLoading] = useState(false);
6
6
  const [disconnecting, setDisconnecting] = useState(false);
7
7
  const [connected, setConnected] = useState(false);
8
+ const [error, setError] = useState(null);
8
9
  const pollingInterval = useRef(null);
9
10
  const checkStatus = async () => {
11
+ if (!isConfigured())
12
+ return false;
10
13
  try {
11
14
  const data = await getConnectionStatus(entityId);
12
15
  const isConnected = data.connectedApps && data.connectedApps.some((a) => searchTerms.some(term => a.toLowerCase().includes(term.toLowerCase())));
@@ -28,11 +31,34 @@ export const useServiceConnection = (appName, entityId, searchTerms = [appName])
28
31
  };
29
32
  }, [entityId]);
30
33
  const handleConnect = async () => {
34
+ if (!isConfigured()) {
35
+ setError('Configuration missing. Please set up the API URL and keys.');
36
+ Alert.alert('Configuration Missing', 'Please configure the backend URL and API keys in settings.');
37
+ return;
38
+ }
39
+ // SAFARI FIX: Open window immediately while still in user gesture context
40
+ // This must happen BEFORE any async operations to avoid popup blocker
41
+ let popup = null;
42
+ if (typeof window !== 'undefined') {
43
+ popup = window.open('about:blank', '_blank');
44
+ }
31
45
  try {
32
46
  setLoading(true);
47
+ setError(null);
33
48
  const data = await startConnection(appName, entityId);
34
- if (data.url)
35
- await Linking.openURL(data.url);
49
+ if (data.url) {
50
+ // Redirect the pre-opened popup, or fallback to Linking
51
+ if (popup) {
52
+ popup.location.href = data.url;
53
+ }
54
+ else {
55
+ await Linking.openURL(data.url);
56
+ }
57
+ }
58
+ else if (popup) {
59
+ // Close popup if no URL was returned
60
+ popup.close();
61
+ }
36
62
  // Start polling for connection status
37
63
  let attempts = 0;
38
64
  const maxAttempts = 60; // 2 minutes approx (2s interval)
@@ -52,8 +78,12 @@ export const useServiceConnection = (appName, entityId, searchTerms = [appName])
52
78
  }, 2000);
53
79
  }
54
80
  catch (error) {
81
+ // Close the pre-opened popup on error
82
+ if (popup)
83
+ popup.close();
55
84
  console.error(error);
56
85
  console.error(`Failed to connect ${appName}`);
86
+ setError(`Failed to connect ${appName}`);
57
87
  setLoading(false);
58
88
  }
59
89
  };
@@ -65,6 +95,8 @@ export const useServiceConnection = (appName, entityId, searchTerms = [appName])
65
95
  setLoading(false);
66
96
  };
67
97
  const handleDisconnect = async () => {
98
+ if (!isConfigured())
99
+ return;
68
100
  try {
69
101
  setDisconnecting(true);
70
102
  await disconnectApp(appName, entityId);
@@ -72,10 +104,11 @@ export const useServiceConnection = (appName, entityId, searchTerms = [appName])
72
104
  }
73
105
  catch (error) {
74
106
  console.error(`Failed to disconnect ${appName}:`, error);
107
+ setError(`Failed to disconnect ${appName}`);
75
108
  }
76
109
  finally {
77
110
  setDisconnecting(false);
78
111
  }
79
112
  };
80
- return { connected, loading, disconnecting, handleConnect, handleCancel, handleDisconnect };
113
+ return { connected, loading, disconnecting, error, handleConnect, handleCancel, handleDisconnect };
81
114
  };
package/dist/index.d.mts CHANGED
@@ -9,6 +9,7 @@ export { ConnectInstagram } from './components/ConnectInstagram';
9
9
  export { ConnectLinkedIn } from './components/ConnectLinkedIn';
10
10
  export { ConnectWhatsApp } from './components/ConnectWhatsApp';
11
11
  export { DataUploadModal } from './components/DataUploadModal';
12
+ export { SettingsModal } from './components/SettingsModal';
12
13
  export { useServiceConnection } from './hooks/useServiceConnection';
13
14
  export { configure, startConnection, executeAction, getConnectionStatus, disconnectApp, uploadUserData, getUserDataStatus, deleteUserData } from './services/api';
14
15
  export { searchPerson, getGiftIdeas, draftEmail, summarizeRecent } from './services/api';
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { ConnectInstagram } from './components/ConnectInstagram';
9
9
  export { ConnectLinkedIn } from './components/ConnectLinkedIn';
10
10
  export { ConnectWhatsApp } from './components/ConnectWhatsApp';
11
11
  export { DataUploadModal } from './components/DataUploadModal';
12
+ export { SettingsModal } from './components/SettingsModal';
12
13
  export { useServiceConnection } from './hooks/useServiceConnection';
13
14
  export { configure, startConnection, executeAction, getConnectionStatus, disconnectApp, uploadUserData, getUserDataStatus, deleteUserData } from './services/api';
14
15
  export { searchPerson, getGiftIdeas, draftEmail, summarizeRecent } from './services/api';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EACH,SAAS,EACT,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,cAAc,EACjB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACH,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,eAAe,EAClB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACR,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACjB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAG3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,EACH,SAAS,EACT,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,cAAc,EACjB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACH,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,eAAe,EAClB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACR,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACjB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { ConnectInstagram } from './components/ConnectInstagram';
10
10
  export { ConnectLinkedIn } from './components/ConnectLinkedIn';
11
11
  export { ConnectWhatsApp } from './components/ConnectWhatsApp';
12
12
  export { DataUploadModal } from './components/DataUploadModal';
13
+ export { SettingsModal } from './components/SettingsModal';
13
14
  // Hooks
14
15
  export { useServiceConnection } from './hooks/useServiceConnection';
15
16
  // Core API functions
package/dist/index.mjs CHANGED
@@ -10,6 +10,7 @@ export { ConnectInstagram } from './components/ConnectInstagram';
10
10
  export { ConnectLinkedIn } from './components/ConnectLinkedIn';
11
11
  export { ConnectWhatsApp } from './components/ConnectWhatsApp';
12
12
  export { DataUploadModal } from './components/DataUploadModal';
13
+ export { SettingsModal } from './components/SettingsModal';
13
14
  // Hooks
14
15
  export { useServiceConnection } from './hooks/useServiceConnection';
15
16
  // Core API functions
@@ -1,11 +1,17 @@
1
1
  /**
2
- * Configure the backend URL for the knowledge base API.
3
- * Call this before using any other functions if your backend is not at localhost:3000.
4
- * @param url - The base URL of the backend (e.g., 'https://api.example.com')
2
+ * Configure the backend URL and optional headers for the knowledge base API.
3
+ * Call this before using any other functions if your backend is not at localhost:3000
4
+ * or if you need to pass specific API keys.
5
+ * @param options - The configuration options
5
6
  */
6
7
  export declare const configure: (options: {
7
8
  backendUrl: string;
9
+ headers?: Record<string, string>;
8
10
  }) => void;
11
+ /**
12
+ * Check if the API is configured (has a backend URL set).
13
+ */
14
+ export declare const isConfigured: () => boolean;
9
15
  export declare const startConnection: (appName: string, entityId: string) => Promise<any>;
10
16
  export declare const executeAction: (prompt: string, entityId: string) => Promise<any>;
11
17
  export declare const getConnectionStatus: (entityId: string) => Promise<any>;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/services/api.ts"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,SAExD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,SAAS,MAAM,EAAE,UAAU,MAAM,iBAetE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,QAAQ,MAAM,EAAE,UAAU,MAAM,iBAenE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAU,UAAU,MAAM,iBAUzD,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,SAAS,MAAM,EAAE,UAAU,MAAM,iBAepE,CAAC;AAEF,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,GACvB,aAAa,WAAW,GAAG,UAAU,EACrC,UAAU,MAAM,EAChB,MAAM,cAAc,iBAgCvB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,UAAU,MAAM,EAAE,aAAa,MAAM,iBAY5E,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,aAAa,MAAM,EAAE,UAAU,MAAM,iBAezE,CAAC;AAMF,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,mBAAmB,iBAM9D,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,gBAAgB,iBAO3D,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAU,SAAS,iBAAiB,iBAM1D,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAU,SAAS,sBAAsB,iBAOpE,CAAC"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/services/api.ts"],"names":[],"mappings":"AAaA;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,SAK1F,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,eAExB,CAAC;AASF,eAAO,MAAM,eAAe,GAAU,SAAS,MAAM,EAAE,UAAU,MAAM,iBAatE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,QAAQ,MAAM,EAAE,UAAU,MAAM,iBAanE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAU,UAAU,MAAM,iBAYzD,CAAC;AAEF,eAAO,MAAM,aAAa,GAAU,SAAS,MAAM,EAAE,UAAU,MAAM,iBAapE,CAAC;AAEF,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,GACvB,aAAa,WAAW,GAAG,UAAU,EACrC,UAAU,MAAM,EAChB,MAAM,cAAc,iBAgCvB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,UAAU,MAAM,EAAE,aAAa,MAAM,iBAY5E,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,aAAa,MAAM,EAAE,UAAU,MAAM,iBAezE,CAAC;AAMF,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,mBAAmB,iBAM9D,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,SAAS,gBAAgB,iBAO3D,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAU,SAAS,iBAAiB,iBAM1D,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAU,SAAS,sBAAsB,iBAOpE,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Platform } from 'react-native';
2
2
  let _backendUrl = null;
3
+ let _headers = {};
3
4
  const getBackendUrl = () => {
4
5
  if (_backendUrl)
5
6
  return _backendUrl;
@@ -9,20 +10,34 @@ const getBackendUrl = () => {
9
10
  });
10
11
  };
11
12
  /**
12
- * Configure the backend URL for the knowledge base API.
13
- * Call this before using any other functions if your backend is not at localhost:3000.
14
- * @param url - The base URL of the backend (e.g., 'https://api.example.com')
13
+ * Configure the backend URL and optional headers for the knowledge base API.
14
+ * Call this before using any other functions if your backend is not at localhost:3000
15
+ * or if you need to pass specific API keys.
16
+ * @param options - The configuration options
15
17
  */
16
18
  export const configure = (options) => {
17
19
  _backendUrl = options.backendUrl;
20
+ if (options.headers) {
21
+ _headers = { ..._headers, ...options.headers };
22
+ }
23
+ };
24
+ /**
25
+ * Check if the API is configured (has a backend URL set).
26
+ */
27
+ export const isConfigured = () => {
28
+ return _backendUrl !== null;
29
+ };
30
+ const getHeaders = () => {
31
+ return {
32
+ 'Content-Type': 'application/json',
33
+ ..._headers
34
+ };
18
35
  };
19
36
  export const startConnection = async (appName, entityId) => {
20
37
  try {
21
38
  const response = await fetch(`${getBackendUrl()}/start-connection`, {
22
39
  method: 'POST',
23
- headers: {
24
- 'Content-Type': 'application/json',
25
- },
40
+ headers: getHeaders(),
26
41
  body: JSON.stringify({ appName, entityId }),
27
42
  });
28
43
  const data = await response.json();
@@ -37,9 +52,7 @@ export const executeAction = async (prompt, entityId) => {
37
52
  try {
38
53
  const response = await fetch(`${getBackendUrl()}/execute`, {
39
54
  method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json',
42
- },
55
+ headers: getHeaders(),
43
56
  body: JSON.stringify({ prompt, entityId }),
44
57
  });
45
58
  const data = await response.json();
@@ -52,7 +65,9 @@ export const executeAction = async (prompt, entityId) => {
52
65
  };
53
66
  export const getConnectionStatus = async (entityId) => {
54
67
  try {
55
- const response = await fetch(`${getBackendUrl()}/connection-status?entityId=${entityId}`);
68
+ const response = await fetch(`${getBackendUrl()}/connection-status?entityId=${entityId}`, {
69
+ headers: getHeaders()
70
+ });
56
71
  if (!response.ok)
57
72
  throw new Error('Failed to fetch status');
58
73
  const data = await response.json();
@@ -67,9 +82,7 @@ export const disconnectApp = async (appName, entityId) => {
67
82
  try {
68
83
  const response = await fetch(`${getBackendUrl()}/disconnect`, {
69
84
  method: 'POST',
70
- headers: {
71
- 'Content-Type': 'application/json',
72
- },
85
+ headers: getHeaders(),
73
86
  body: JSON.stringify({ appName, entityId }),
74
87
  });
75
88
  const data = await response.json();
@@ -163,7 +176,7 @@ export const getGiftIdeas = async (options) => {
163
176
  prompt += ` The occasion is: ${occasion}.`;
164
177
  if (budget)
165
178
  prompt += ` Budget: ${budget}.`;
166
- prompt += ` Look for their interests, hobbies, recent conversations, and anything that might hint at what they would appreciate.`;
179
+ prompt += `.`;
167
180
  return executeAction(prompt, entityId);
168
181
  };
169
182
  /**
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@enhearten/knowledge-base-module",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Connect accounts (Gmail, GitHub, Discord, etc.) and search across them with AI to execute actions",
5
5
  "main": "index.ts",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/index.d.ts",
10
11
  "import": "./dist/index.mjs",
11
- "require": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
12
+ "require": "./dist/index.js"
13
13
  },
14
14
  "./backend": {
15
15
  "require": "./backend/app.js"
@@ -0,0 +1,155 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Button, Card, Input, Label, Sheet, Text, XStack, YStack, Theme, Spinner } from 'tamagui';
3
+ import { MaterialCommunityIcons } from '@expo/vector-icons';
4
+ import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import { configure } from '../services/api';
6
+
7
+ interface SettingsModalProps {
8
+ open: boolean;
9
+ onClose: () => void;
10
+ }
11
+
12
+ export const SettingsModal: React.FC<SettingsModalProps> = ({ open, onClose }) => {
13
+ const [backendUrl, setBackendUrl] = useState('');
14
+ const [composioKey, setComposioKey] = useState('');
15
+ const [googleKey, setGoogleKey] = useState('');
16
+ const [loading, setLoading] = useState(false);
17
+ const [saving, setSaving] = useState(false);
18
+
19
+ useEffect(() => {
20
+ if (open) {
21
+ loadSettings();
22
+ }
23
+ }, [open]);
24
+
25
+ const loadSettings = async () => {
26
+ setLoading(true);
27
+ try {
28
+ const storedUrl = await AsyncStorage.getItem('kb_backend_url');
29
+ const storedComposioKey = await AsyncStorage.getItem('kb_composio_key');
30
+ const storedGoogleKey = await AsyncStorage.getItem('kb_google_key');
31
+
32
+ if (storedUrl) setBackendUrl(storedUrl);
33
+ if (storedComposioKey) setComposioKey(storedComposioKey);
34
+ if (storedGoogleKey) setGoogleKey(storedGoogleKey);
35
+ } catch (error) {
36
+ console.error('Failed to load settings', error);
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+
42
+ const handleSave = async () => {
43
+ setSaving(true);
44
+ try {
45
+ // Remove trailing slash from URL if present
46
+ const cleanUrl = backendUrl.replace(/\/$/, '');
47
+
48
+ await AsyncStorage.setItem('kb_backend_url', cleanUrl);
49
+ await AsyncStorage.setItem('kb_composio_key', composioKey);
50
+ await AsyncStorage.setItem('kb_google_key', googleKey);
51
+
52
+ configure({
53
+ backendUrl: cleanUrl,
54
+ headers: {
55
+ 'x-composio-key': composioKey,
56
+ 'x-google-key': googleKey
57
+ }
58
+ });
59
+
60
+ onClose();
61
+ } catch (error) {
62
+ console.error('Failed to save settings', error);
63
+ } finally {
64
+ setSaving(false);
65
+ }
66
+ };
67
+
68
+ return (
69
+ <Theme name="light">
70
+ <Sheet
71
+ modal
72
+ open={open}
73
+ onOpenChange={(isOpen: boolean) => !isOpen && onClose()}
74
+ snapPoints={[85]}
75
+ dismissOnSnapToBottom
76
+ >
77
+ <Sheet.Overlay />
78
+ <Sheet.Frame padding="$4" backgroundColor="white">
79
+ <Sheet.Handle />
80
+
81
+ <YStack space="$4" paddingTop="$4">
82
+ <XStack alignItems="center" space="$2">
83
+ <MaterialCommunityIcons name="cog" size={28} color="#333" />
84
+ <Text fontSize="$6" fontWeight="bold">Settings</Text>
85
+ </XStack>
86
+
87
+ <Text color="$gray11">
88
+ Configure your Cloudflare Worker URL and API keys.
89
+ </Text>
90
+
91
+ {loading ? (
92
+ <Spinner size="large" color="$blue10" />
93
+ ) : (
94
+ <YStack space="$4">
95
+ <YStack space="$2">
96
+ <Label htmlFor="backendUrl" fontWeight="600">Backend URL (Cloudflare Worker)</Label>
97
+ <Input
98
+ id="backendUrl"
99
+ placeholder="https://knowledge-base-worker.your-subdomain.workers.dev"
100
+ value={backendUrl}
101
+ onChangeText={setBackendUrl}
102
+ autoCapitalize="none"
103
+ autoCorrect={false}
104
+ />
105
+ </YStack>
106
+
107
+ <YStack space="$2">
108
+ <Label htmlFor="composioKey" fontWeight="600">Composio API Key</Label>
109
+ <Input
110
+ id="composioKey"
111
+ placeholder="Enter your Composio API Key"
112
+ value={composioKey}
113
+ onChangeText={setComposioKey}
114
+ secureTextEntry
115
+ />
116
+ </YStack>
117
+
118
+ <YStack space="$2">
119
+ <Label htmlFor="googleKey" fontWeight="600">Google Gemini API Key</Label>
120
+ <Input
121
+ id="googleKey"
122
+ placeholder="Enter your Google Gemini API Key"
123
+ value={googleKey}
124
+ onChangeText={setGoogleKey}
125
+ secureTextEntry
126
+ />
127
+ </YStack>
128
+
129
+ <XStack gap="$3" justifyContent="flex-end" marginTop="$4">
130
+ <Button
131
+ size="$4"
132
+ variant="outlined"
133
+ onPress={onClose}
134
+ disabled={saving}
135
+ >
136
+ Cancel
137
+ </Button>
138
+ <Button
139
+ size="$4"
140
+ themeInverse
141
+ onPress={handleSave}
142
+ disabled={saving}
143
+ icon={saving ? <Spinner size="small" color="white" /> : undefined}
144
+ >
145
+ {saving ? 'Saving...' : 'Save Settings'}
146
+ </Button>
147
+ </XStack>
148
+ </YStack>
149
+ )}
150
+ </YStack>
151
+ </Sheet.Frame>
152
+ </Sheet>
153
+ </Theme>
154
+ );
155
+ };
@@ -1,14 +1,16 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { Linking } from 'react-native';
3
- import { startConnection, getConnectionStatus, disconnectApp } from '../services/api';
2
+ import { Linking, Alert } from 'react-native';
3
+ import { startConnection, getConnectionStatus, disconnectApp, isConfigured } from '../services/api';
4
4
 
5
5
  export const useServiceConnection = (appName: string, entityId: string, searchTerms: string[] = [appName]) => {
6
6
  const [loading, setLoading] = useState(false);
7
7
  const [disconnecting, setDisconnecting] = useState(false);
8
8
  const [connected, setConnected] = useState(false);
9
+ const [error, setError] = useState<string | null>(null);
9
10
  const pollingInterval = useRef<NodeJS.Timeout | null>(null);
10
11
 
11
12
  const checkStatus = async () => {
13
+ if (!isConfigured()) return false;
12
14
  try {
13
15
  const data = await getConnectionStatus(entityId);
14
16
  const isConnected = data.connectedApps && data.connectedApps.some((a: string) =>
@@ -32,10 +34,35 @@ export const useServiceConnection = (appName: string, entityId: string, searchTe
32
34
  }, [entityId]);
33
35
 
34
36
  const handleConnect = async () => {
37
+ if (!isConfigured()) {
38
+ setError('Configuration missing. Please set up the API URL and keys.');
39
+ Alert.alert('Configuration Missing', 'Please configure the backend URL and API keys in settings.');
40
+ return;
41
+ }
42
+
43
+ // SAFARI FIX: Open window immediately while still in user gesture context
44
+ // This must happen BEFORE any async operations to avoid popup blocker
45
+ let popup: Window | null = null;
46
+ if (typeof window !== 'undefined') {
47
+ popup = window.open('about:blank', '_blank');
48
+ }
49
+
35
50
  try {
36
51
  setLoading(true);
52
+ setError(null);
37
53
  const data = await startConnection(appName, entityId);
38
- if (data.url) await Linking.openURL(data.url);
54
+
55
+ if (data.url) {
56
+ // Redirect the pre-opened popup, or fallback to Linking
57
+ if (popup) {
58
+ popup.location.href = data.url;
59
+ } else {
60
+ await Linking.openURL(data.url);
61
+ }
62
+ } else if (popup) {
63
+ // Close popup if no URL was returned
64
+ popup.close();
65
+ }
39
66
 
40
67
  // Start polling for connection status
41
68
  let attempts = 0;
@@ -55,8 +82,11 @@ export const useServiceConnection = (appName: string, entityId: string, searchTe
55
82
  }, 2000);
56
83
 
57
84
  } catch (error) {
85
+ // Close the pre-opened popup on error
86
+ if (popup) popup.close();
58
87
  console.error(error);
59
88
  console.error(`Failed to connect ${appName}`);
89
+ setError(`Failed to connect ${appName}`);
60
90
  setLoading(false);
61
91
  }
62
92
  };
@@ -70,16 +100,18 @@ export const useServiceConnection = (appName: string, entityId: string, searchTe
70
100
  };
71
101
 
72
102
  const handleDisconnect = async () => {
103
+ if (!isConfigured()) return;
73
104
  try {
74
105
  setDisconnecting(true);
75
106
  await disconnectApp(appName, entityId);
76
107
  setConnected(false);
77
108
  } catch (error) {
78
109
  console.error(`Failed to disconnect ${appName}:`, error);
110
+ setError(`Failed to disconnect ${appName}`);
79
111
  } finally {
80
112
  setDisconnecting(false);
81
113
  }
82
114
  };
83
115
 
84
- return { connected, loading, disconnecting, handleConnect, handleCancel, handleDisconnect };
116
+ return { connected, loading, disconnecting, error, handleConnect, handleCancel, handleDisconnect };
85
117
  };
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export { ConnectInstagram } from './components/ConnectInstagram';
10
10
  export { ConnectLinkedIn } from './components/ConnectLinkedIn';
11
11
  export { ConnectWhatsApp } from './components/ConnectWhatsApp';
12
12
  export { DataUploadModal } from './components/DataUploadModal';
13
+ export { SettingsModal } from './components/SettingsModal';
13
14
 
14
15
  // Hooks
15
16
  export { useServiceConnection } from './hooks/useServiceConnection';
@@ -1,6 +1,7 @@
1
1
  import { Platform } from 'react-native';
2
2
 
3
3
  let _backendUrl: string | null = null;
4
+ let _headers: Record<string, string> = {};
4
5
 
5
6
  const getBackendUrl = () => {
6
7
  if (_backendUrl) return _backendUrl;
@@ -11,21 +12,37 @@ const getBackendUrl = () => {
11
12
  };
12
13
 
13
14
  /**
14
- * Configure the backend URL for the knowledge base API.
15
- * Call this before using any other functions if your backend is not at localhost:3000.
16
- * @param url - The base URL of the backend (e.g., 'https://api.example.com')
15
+ * Configure the backend URL and optional headers for the knowledge base API.
16
+ * Call this before using any other functions if your backend is not at localhost:3000
17
+ * or if you need to pass specific API keys.
18
+ * @param options - The configuration options
17
19
  */
18
- export const configure = (options: { backendUrl: string }) => {
20
+ export const configure = (options: { backendUrl: string; headers?: Record<string, string> }) => {
19
21
  _backendUrl = options.backendUrl;
22
+ if (options.headers) {
23
+ _headers = { ..._headers, ...options.headers };
24
+ }
25
+ };
26
+
27
+ /**
28
+ * Check if the API is configured (has a backend URL set).
29
+ */
30
+ export const isConfigured = () => {
31
+ return _backendUrl !== null;
32
+ };
33
+
34
+ const getHeaders = () => {
35
+ return {
36
+ 'Content-Type': 'application/json',
37
+ ..._headers
38
+ };
20
39
  };
21
40
 
22
41
  export const startConnection = async (appName: string, entityId: string) => {
23
42
  try {
24
43
  const response = await fetch(`${getBackendUrl()}/start-connection`, {
25
44
  method: 'POST',
26
- headers: {
27
- 'Content-Type': 'application/json',
28
- },
45
+ headers: getHeaders(),
29
46
  body: JSON.stringify({ appName, entityId }),
30
47
  });
31
48
  const data = await response.json();
@@ -40,9 +57,7 @@ export const executeAction = async (prompt: string, entityId: string) => {
40
57
  try {
41
58
  const response = await fetch(`${getBackendUrl()}/execute`, {
42
59
  method: 'POST',
43
- headers: {
44
- 'Content-Type': 'application/json',
45
- },
60
+ headers: getHeaders(),
46
61
  body: JSON.stringify({ prompt, entityId }),
47
62
  });
48
63
  const data = await response.json();
@@ -55,7 +70,9 @@ export const executeAction = async (prompt: string, entityId: string) => {
55
70
 
56
71
  export const getConnectionStatus = async (entityId: string) => {
57
72
  try {
58
- const response = await fetch(`${getBackendUrl()}/connection-status?entityId=${entityId}`);
73
+ const response = await fetch(`${getBackendUrl()}/connection-status?entityId=${entityId}`, {
74
+ headers: getHeaders()
75
+ });
59
76
  if (!response.ok) throw new Error('Failed to fetch status');
60
77
  const data = await response.json();
61
78
  return data;
@@ -69,9 +86,7 @@ export const disconnectApp = async (appName: string, entityId: string) => {
69
86
  try {
70
87
  const response = await fetch(`${getBackendUrl()}/disconnect`, {
71
88
  method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- },
89
+ headers: getHeaders(),
75
90
  body: JSON.stringify({ appName, entityId }),
76
91
  });
77
92
  const data = await response.json();
@@ -197,7 +212,7 @@ export const getGiftIdeas = async (options: GiftIdeasOptions) => {
197
212
  let prompt = `Based on my connected accounts, suggest gift ideas for ${personName}.`;
198
213
  if (occasion) prompt += ` The occasion is: ${occasion}.`;
199
214
  if (budget) prompt += ` Budget: ${budget}.`;
200
- prompt += ` Look for their interests, hobbies, recent conversations, and anything that might hint at what they would appreciate.`;
215
+ prompt += `.`;
201
216
  return executeAction(prompt, entityId);
202
217
  };
203
218