Skip to content

Commit

Permalink
Community hub integration (#2555)
Browse files Browse the repository at this point in the history
* wip hub connection page fe + backend

* lint

* implement backend for local hub items + placeholder endpoints to fetch hub app data

* fix hebrew translations

* revamp community integration flow

* change sidebar

* Auto import if id in URL param
remove preview in card screen and instead go to import flow

* get user's items + team items from hub + ui improvements to hub settings

* lint

* fix merge conflict

* refresh hook for community items

* add fallback for user items

* Disable bundle items by default on all instances

* remove translations (will complete later)

* loading skeleton

* Make community hub endpoints admin only
show visibility on items
combine import/apply for items to they are event logged for review

* improve middleware and import flow

* community hub ui updates

* Adjust importing process

* community hub to dev

* Add webscraper preload into imported plugins

* add runtime property to plugins

* Fix button status on imported skill change
show alert on skill change
Update markdown type and theme on import of agent skill

* update documentaion paths

* remove unused import

* linting

* review loading state

---------

Co-authored-by: Timothy Carambat <[email protected]>
  • Loading branch information
shatfield4 and timothycarambat authored Nov 26, 2024
1 parent 8c9e9f2 commit 05c5302
Show file tree
Hide file tree
Showing 40 changed files with 2,449 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ concurrency:

on:
push:
branches: ['2670-feat-can-the-font-size-of-the-chat-input-box-be-increased'] # put your current branch to create a build. Core team only.
branches: ['2545-feat-community-hub-integration'] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ const LiveDocumentSyncManage = lazy(
);
const FineTuningWalkthrough = lazy(() => import("@/pages/FineTuning"));

const CommunityHubTrending = lazy(
() => import("@/pages/GeneralSettings/CommunityHub/Trending")
);
const CommunityHubAuthentication = lazy(
() => import("@/pages/GeneralSettings/CommunityHub/Authentication")
);
const CommunityHubImportItem = lazy(
() => import("@/pages/GeneralSettings/CommunityHub/ImportItem")
);

export default function App() {
return (
<ThemeProvider>
Expand Down Expand Up @@ -207,6 +217,21 @@ export default function App() {
path="/fine-tuning"
element={<AdminRoute Component={FineTuningWalkthrough} />}
/>

<Route
path="/settings/community-hub/trending"
element={<AdminRoute Component={CommunityHubTrending} />}
/>
<Route
path="/settings/community-hub/authentication"
element={
<AdminRoute Component={CommunityHubAuthentication} />
}
/>
<Route
path="/settings/community-hub/import-item"
element={<AdminRoute Component={CommunityHubImportItem} />}
/>
</Routes>
<ToastContainer />
</I18nextProvider>
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/SettingsSidebar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PencilSimpleLine,
Nut,
Toolbox,
Globe,
} from "@phosphor-icons/react";
import useUser from "@/hooks/useUser";
import { isMobile } from "react-device-detect";
Expand Down Expand Up @@ -291,6 +292,30 @@ const SidebarOptions = ({ user = null, t }) => (
flex={true}
roles={["admin"]}
/>
<Option
btnText="Community Hub"
icon={<Globe className="h-5 w-5 flex-shrink-0" />}
childOptions={[
{
btnText: "Explore Trending",
href: paths.communityHub.trending(),
flex: true,
roles: ["admin"],
},
{
btnText: "Your Account",
href: paths.communityHub.authentication(),
flex: true,
roles: ["admin"],
},
{
btnText: "Import Item",
href: paths.communityHub.importItem(),
flex: true,
roles: ["admin"],
},
]}
/>
<Option
btnText={t("settings.customization")}
icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/WorkspaceChat/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function copyCodeSnippet(uuid) {
}

// Listens and hunts for all data-code-snippet clicks.
function setEventDelegatorForCodeSnippets() {
export function setEventDelegatorForCodeSnippets() {
document?.addEventListener("click", function (e) {
const target = e.target.closest("[data-code-snippet]");
const uuidCode = target?.dataset?.code;
Expand Down
158 changes: 158 additions & 0 deletions frontend/src/models/communityHub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";

const CommunityHub = {
/**
* Get an item from the community hub by its import ID.
* @param {string} importId - The import ID of the item.
* @returns {Promise<{error: string | null, item: object | null}>}
*/
getItemFromImportId: async (importId) => {
return await fetch(`${API_BASE}/community-hub/item`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ importId }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return {
error: e.message,
item: null,
};
});
},

/**
* Apply an item to the AnythingLLM instance. Used for simple items like slash commands and system prompts.
* @param {string} importId - The import ID of the item.
* @param {object} options - Additional options for applying the item for whatever the item type requires.
* @returns {Promise<{success: boolean, error: string | null}>}
*/
applyItem: async (importId, options = {}) => {
return await fetch(`${API_BASE}/community-hub/apply`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ importId, options }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return {
success: false,
error: e.message,
};
});
},

/**
* Import a bundle item from the community hub.
* @param {string} importId - The import ID of the item.
* @returns {Promise<{error: string | null, item: object | null}>}
*/
importBundleItem: async (importId) => {
return await fetch(`${API_BASE}/community-hub/import`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ importId }),
})
.then(async (res) => {
const response = await res.json();
if (!res.ok) throw new Error(response?.error ?? res.statusText);
return response;
})
.catch((e) => {
return {
error: e.message,
item: null,
};
});
},

/**
* Update the hub settings (API key, etc.)
* @param {Object} data - The data to update.
* @returns {Promise<{success: boolean, error: string | null}>}
*/
updateSettings: async (data) => {
return await fetch(`${API_BASE}/community-hub/settings`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify(data),
})
.then(async (res) => {
const response = await res.json();
if (!res.ok)
throw new Error(response.error || "Failed to update settings");
return { success: true, error: null };
})
.catch((e) => ({
success: false,
error: e.message,
}));
},

/**
* Get the hub settings (API key, etc.)
* @returns {Promise<{connectionKey: string | null, error: string | null}>}
*/
getSettings: async () => {
return await fetch(`${API_BASE}/community-hub/settings`, {
method: "GET",
headers: baseHeaders(),
})
.then(async (res) => {
const response = await res.json();
if (!res.ok)
throw new Error(response.error || "Failed to fetch settings");
return { connectionKey: response.connectionKey, error: null };
})
.catch((e) => ({
connectionKey: null,
error: e.message,
}));
},

/**
* Fetch the explore items from the community hub that are publicly available.
* @returns {Promise<{agentSkills: {items: [], hasMore: boolean, totalCount: number}, systemPrompts: {items: [], hasMore: boolean, totalCount: number}, slashCommands: {items: [], hasMore: boolean, totalCount: number}}>}
*/
fetchExploreItems: async () => {
return await fetch(`${API_BASE}/community-hub/explore`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return {
success: false,
error: e.message,
result: null,
};
});
},

/**
* Fetch the user items from the community hub.
* @returns {Promise<{success: boolean, error: string | null, createdByMe: object, teamItems: object[]}>}
*/
fetchUserItems: async () => {
return await fetch(`${API_BASE}/community-hub/items`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return {
success: false,
error: e.message,
createdByMe: {},
teamItems: [],
};
});
},
};

export default CommunityHub;
14 changes: 14 additions & 0 deletions frontend/src/models/experimental/agentPlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ const AgentPlugins = {
return false;
});
},
deletePlugin: async function (hubId) {
return await fetch(`${API_BASE}/experimental/agent-plugins/${hubId}`, {
method: "DELETE",
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Could not delete agent plugin config.");
return true;
})
.catch((e) => {
console.error(e);
return false;
});
},
};

export default AgentPlugins;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import System from "@/models/system";
import showToast from "@/utils/toast";
import { Plug } from "@phosphor-icons/react";
import { useEffect, useState } from "react";
import { Gear, Plug } from "@phosphor-icons/react";
import { useEffect, useState, useRef } from "react";
import { sentenceCase } from "text-case";

/**
Expand Down Expand Up @@ -55,6 +55,11 @@ export default function ImportedSkillConfig({
prev.map((s) => (s.hubId === config.hubId ? updatedConfig : s))
);
setConfig(updatedConfig);
showToast(
`Skill ${updatedConfig.active ? "activated" : "deactivated"}.`,
"success",
{ clear: true }
);
}

async function handleSubmit(e) {
Expand Down Expand Up @@ -91,6 +96,7 @@ export default function ImportedSkillConfig({
)
);
showToast("Skill config updated successfully.", "success");
setHasChanges(false);
}

useEffect(() => {
Expand Down Expand Up @@ -119,6 +125,10 @@ export default function ImportedSkillConfig({
<div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-[#CFCFD0] after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border-none after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-[#32D583] peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-transparent"></div>
<span className="ml-3 text-sm font-medium"></span>
</label>
<ManageSkillMenu
config={config}
setImportedSkills={setImportedSkills}
/>
</div>
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
{config.description} by{" "}
Expand Down Expand Up @@ -178,3 +188,64 @@ export default function ImportedSkillConfig({
</>
);
}

function ManageSkillMenu({ config, setImportedSkills }) {
const [open, setOpen] = useState(false);
const menuRef = useRef(null);

async function deleteSkill() {
if (
!window.confirm(
"Are you sure you want to delete this skill? This action cannot be undone."
)
)
return;
const success = await System.experimentalFeatures.agentPlugins.deletePlugin(
config.hubId
);
if (success) {
setImportedSkills((prev) => prev.filter((s) => s.hubId !== config.hubId));
showToast("Skill deleted successfully.", "success");
setOpen(false);
} else {
showToast("Failed to delete skill.", "error");
}
}

useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setOpen(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

if (!config.hubId) return null;
return (
<div className="relative" ref={menuRef}>
<button
type="button"
onClick={() => setOpen(!open)}
className={`border-none transition duration-200 hover:rotate-90 outline-none ring-none ${open ? "rotate-90" : ""}`}
>
<Gear size={24} weight="bold" />
</button>
{open && (
<div className="absolute w-[100px] -top-1 left-7 mt-1 border-[1.5px] border-white/40 rounded-lg bg-theme-action-menu-bg flex flex-col shadow-[0_4px_14px_rgba(0,0,0,0.25)] text-white z-99 md:z-10">
<button
type="button"
onClick={deleteSkill}
className="border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left"
>
<span className="text-sm">Delete Skill</span>
</button>
</div>
)}
</div>
);
}
Loading

0 comments on commit 05c5302

Please sign in to comment.