Publish system prompts to hub (#3976)

* implement ui for publish system prompt to hub

* rework ui + add backend to upload to hub

* add success modal view + publish menu item + translations

* normalize translations

* refactor PublishEntityModal + add hook
for hub auth

* normalize translations

* fix ui for success screen

* refactor, auth checks, UI/UX, and naming conventions to be more clear

* move components to CommunityHub folder + small ui tweak

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2025-06-16 09:59:38 -07:00 committed by GitHub
parent 7cef25822c
commit 96b532a0f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1090 additions and 18 deletions

View File

@ -0,0 +1,242 @@
import { useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import CommunityHub from "@/models/communityHub";
import showToast from "@/utils/toast";
import paths from "@/utils/paths";
import { X } from "@phosphor-icons/react/dist/ssr";
export default function SystemPrompts({ entity }) {
const { t } = useTranslation();
const formRef = useRef(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [tags, setTags] = useState([]);
const [tagInput, setTagInput] = useState("");
const [visibility, setVisibility] = useState("public");
const [isSuccess, setIsSuccess] = useState(false);
const [itemId, setItemId] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
setIsSubmitting(true);
try {
const form = new FormData(formRef.current);
const data = {
name: form.get("name"),
description: form.get("description"),
prompt: form.get("prompt"),
tags: tags,
visibility: visibility,
};
const { success, error, itemId } =
await CommunityHub.createSystemPrompt(data);
if (!success) throw new Error(error);
setItemId(itemId);
setIsSuccess(true);
} catch (error) {
console.error("Failed to publish prompt:", error);
showToast(`Failed to publish prompt: ${error.message}`, "error", {
clear: true,
});
} finally {
setIsSubmitting(false);
}
};
const handleKeyDown = (e) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
const value = tagInput.trim();
if (value.length > 20) return;
if (value && !tags.includes(value)) {
setTags((prevTags) => [...prevTags, value].slice(0, 5)); // Limit to 5 tags
setTagInput("");
}
}
};
const removeTag = (tagToRemove) => {
setTags(tags.filter((tag) => tag !== tagToRemove));
};
if (isSuccess) {
return (
<div className="p-6 -mt-12 w-[400px]">
<div className="flex flex-col items-center justify-center gap-y-2">
<h3 className="text-lg font-semibold text-theme-text-primary">
{t("chat.prompt.publish.success_title")}
</h3>
<p className="text-lg text-theme-text-primary text-center max-w-2xl">
{t("chat.prompt.publish.success_description")}
</p>
<p className="text-theme-text-secondary text-center text-sm">
{t("chat.prompt.publish.success_thank_you")}
</p>
<a
href={paths.communityHub.viewItem("system-prompt", itemId)}
target="_blank"
rel="noreferrer"
className="w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
>
{t("chat.prompt.publish.view_on_hub")}
</a>
</div>
</div>
);
}
return (
<>
<div className="w-full flex gap-x-2 items-center mb-3 -mt-8">
<h3 className="text-xl font-semibold text-theme-text-primary px-6 py-3">
{t(`chat.prompt.publish.modal_title`)}
</h3>
</div>
<form ref={formRef} className="flex" onSubmit={handleSubmit}>
<div className="w-1/2 p-6 pt-0 space-y-4">
<div>
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
{t("chat.prompt.publish.name_label")}
</label>
<div className="text-xs text-theme-text-secondary mb-2">
{t("chat.prompt.publish.name_description")}
</div>
<input
type="text"
name="name"
required
minLength={3}
maxLength={300}
placeholder={t("chat.prompt.publish.name_placeholder")}
className="w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder"
/>
</div>
<div>
<label className="block text-sm font-semibold text-theme-text-primary mb-1">
{t("chat.prompt.publish.description_label")}
</label>
<div className="text-xs text-white/60 mb-2">
{t("chat.prompt.publish.description_description")}
</div>
<textarea
name="description"
required
minLength={10}
maxLength={1000}
placeholder={t("chat.prompt.publish.description_description")}
className="w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder"
/>
</div>
<div>
<label className="block text-sm font-semibold text-white mb-1">
{t("chat.prompt.publish.tags_label")}
</label>
<div className="text-xs text-white/60 mb-2">
{t("chat.prompt.publish.tags_description")}
</div>
<div className="flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]">
{tags.map((tag, index) => (
<span
key={index}
className="flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md"
>
{tag}
<button
type="button"
onClick={() => removeTag(tag)}
className="border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer"
>
<X size={14} />
</button>
</span>
))}
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t("chat.prompt.publish.tags_placeholder")}
className="flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-white mb-1">
{t("chat.prompt.publish.visibility_label")}
</label>
<div className="text-xs text-white/60 mb-2">
{visibility === "public"
? t("chat.prompt.publish.public_description")
: t("chat.prompt.publish.private_description")}
</div>
<div className="w-fit h-[42px] bg-theme-bg-secondary rounded-lg p-0.5">
<div className="flex items-center" role="group">
<input
type="radio"
id="public"
name="visibility"
value="public"
className="peer/public hidden"
defaultChecked
onChange={(e) => setVisibility(e.target.value)}
/>
<input
type="radio"
id="private"
name="visibility"
value="private"
className="peer/private hidden"
onChange={(e) => setVisibility(e.target.value)}
/>
<label
htmlFor="public"
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/public:bg-theme-sidebar-item-hover peer-checked/public:text-theme-primary-button flex items-center justify-center"
>
Public
</label>
<label
htmlFor="private"
className="h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/private:bg-theme-sidebar-item-hover peer-checked/private:text-theme-primary-button flex items-center justify-center"
>
Private
</label>
</div>
</div>
</div>
</div>
<div className="w-1/2 p-6 pt-0 space-y-4">
<div>
<label className="block text-sm font-semibold text-white mb-1">
{t("chat.prompt.publish.prompt_label")}
</label>
<div className="text-xs text-white/60 mb-2">
{t("chat.prompt.publish.prompt_description")}
</div>
<textarea
name="prompt"
required
minLength={10}
defaultValue={entity}
placeholder={t("chat.prompt.publish.prompt_placeholder")}
className="w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[300px] placeholder:text-theme-text-placeholder"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting
? t("chat.prompt.publish.publishing")
: t("chat.prompt.publish.publish_button")}
</button>
</div>
</form>
</>
);
}

View File

@ -0,0 +1,43 @@
import { X } from "@phosphor-icons/react";
import { useCommunityHubAuth } from "@/hooks/useCommunityHubAuth";
import UnauthenticatedHubModal from "@/components/CommunityHub/UnauthenticatedHubModal";
import SystemPrompts from "./SystemPrompts";
import ModalWrapper from "@/components/ModalWrapper";
export default function PublishEntityModal({
show,
onClose,
entityType,
entity,
}) {
const { isAuthenticated, loading } = useCommunityHubAuth();
if (!show || loading) return null;
if (!isAuthenticated)
return <UnauthenticatedHubModal show={show} onClose={onClose} />;
const renderEntityForm = () => {
switch (entityType) {
case "system-prompt":
return <SystemPrompts entity={entity} />;
default:
return null;
}
};
return (
<ModalWrapper isOpen={show}>
<div className="relative max-w-[900px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border">
<div className="relative p-6">
<button
onClick={onClose}
type="button"
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
>
<X size={18} weight="bold" className="text-white" />
</button>
</div>
{renderEntityForm()}
</div>
</ModalWrapper>
);
}

View File

@ -0,0 +1,40 @@
import { X } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
import paths from "@/utils/paths";
import { Link } from "react-router-dom";
import ModalWrapper from "@/components/ModalWrapper";
export default function UnauthenticatedHubModal({ show, onClose }) {
const { t } = useTranslation();
if (!show) return null;
return (
<ModalWrapper isOpen={show}>
<div className="relative w-[400px] max-w-full bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border">
<div className="p-6">
<button
onClick={onClose}
type="button"
className="absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border"
>
<X size={18} weight="bold" className="text-white" />
</button>
<div className="flex flex-col items-center justify-center gap-y-4">
<h3 className="text-lg font-semibold text-white">
{t("chat.prompt.publish.unauthenticated.title")}
</h3>
<p className="text-lg text-white text-center max-w-[300px]">
{t("chat.prompt.publish.unauthenticated.description")}
</p>
<Link
to={paths.communityHub.authentication()}
className="w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center"
>
{t("chat.prompt.publish.unauthenticated.button")}
</Link>
</div>
</div>
</div>
</ModalWrapper>
);
}

View File

@ -0,0 +1,30 @@
import { useState, useEffect } from "react";
import CommunityHub from "@/models/communityHub";
/**
* Hook to check if the user is authenticated with the community hub by checking
* the user defined connection key in the settings.
* @returns {{isAuthenticated: boolean, loading: boolean}} An object containing the authentication status and loading state.
*/
export function useCommunityHubAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function checkCommunityHubAuth() {
setLoading(true);
try {
const { connectionKey } = await CommunityHub.getSettings();
setIsAuthenticated(!!connectionKey);
} catch (error) {
console.error("Error checking hub auth:", error);
setIsAuthenticated(false);
} finally {
setLoading(false);
}
}
checkCommunityHubAuth();
}, []);
return { isAuthenticated, loading };
}

View File

@ -17,6 +17,7 @@
--theme-bg-chat-input: #27282a; --theme-bg-chat-input: #27282a;
--theme-text-primary: #ffffff; --theme-text-primary: #ffffff;
--theme-text-secondary: rgba(255, 255, 255, 0.6); --theme-text-secondary: rgba(255, 255, 255, 0.6);
--theme-placeholder: #57585a;
--theme-sidebar-item-default: rgba(255, 255, 255, 0.1); --theme-sidebar-item-default: rgba(255, 255, 255, 0.1);
--theme-sidebar-item-selected: rgba(255, 255, 255, 0.3); --theme-sidebar-item-selected: rgba(255, 255, 255, 0.3);
--theme-sidebar-item-hover: #3f3f42; --theme-sidebar-item-hover: #3f3f42;
@ -116,6 +117,7 @@
--theme-bg-chat-input: #eaeaea; --theme-bg-chat-input: #eaeaea;
--theme-text-primary: #0e0f0f; --theme-text-primary: #0e0f0f;
--theme-text-secondary: #7a7d7e; --theme-text-secondary: #7a7d7e;
--theme-placeholder: #9ca3af;
--theme-sidebar-item-default: #ffffff; --theme-sidebar-item-default: #ffffff;
--theme-sidebar-item-selected: #ffffff; --theme-sidebar-item-selected: #ffffff;
--theme-sidebar-item-hover: #c8efff; --theme-sidebar-item-hover: #c8efff;

View File

@ -240,6 +240,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -241,6 +241,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -233,6 +233,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -335,11 +335,45 @@ const TRANSLATIONS = {
noHistory: "No system prompt history available", noHistory: "No system prompt history available",
restore: "Restore", restore: "Restore",
delete: "Delete", delete: "Delete",
publish: "Publish to Community Hub",
deleteConfirm: "Are you sure you want to delete this history item?", deleteConfirm: "Are you sure you want to delete this history item?",
clearAllConfirm: clearAllConfirm:
"Are you sure you want to clear all history? This action cannot be undone.", "Are you sure you want to clear all history? This action cannot be undone.",
expand: "Expand", expand: "Expand",
}, },
publish: {
public_description: "Public system prompts are visible to everyone.",
private_description: "Private system prompts are only visible to you.",
success_title: "Success!",
success_description:
"Your System Prompt has been published to the Community Hub!",
success_thank_you: "Thank you for sharing to the Community!",
view_on_hub: "View on Community Hub",
modal_title: "Publish System Prompt",
name_label: "Name",
name_description: "This is the display name of your system prompt.",
name_placeholder: "My System Prompt",
description_label: "Description",
description_description:
"This is the description of your system prompt. Use this to describe the purpose of your system prompt.",
tags_label: "Tags",
tags_description:
"Tags are used to label your system prompt for easier searching. You can add multiple tags. Max 5 tags. Max 20 characters per tag.",
tags_placeholder: "Type and press Enter to add tags",
visibility_label: "Visibility",
prompt_label: "Prompt",
prompt_description:
"This is the actual slash command that will be used to guide the LLM.",
prompt_placeholder: "Enter your system prompt here...",
publish_button: "Publish to Community Hub",
publishing: "Publishing...",
unauthenticated: {
title: "Authentication Required",
description:
"You need to authenticate with the AnythingLLM Community Hub before publishing prompts.",
button: "Connect to Community Hub",
},
},
}, },
refusal: { refusal: {
title: "Query mode refusal response", title: "Query mode refusal response",

View File

@ -235,6 +235,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -231,6 +231,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -236,6 +236,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -229,6 +229,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -234,6 +234,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -240,6 +240,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -229,6 +229,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -325,6 +325,35 @@ const TRANSLATIONS = {
clearAllConfirm: clearAllConfirm:
"Vai tiešām vēlaties nodzēst visu vēsturi? Šo darbību nevar atsaukt.", "Vai tiešām vēlaties nodzēst visu vēsturi? Šo darbību nevar atsaukt.",
expand: "Paplašināt", expand: "Paplašināt",
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -233,6 +233,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -322,6 +322,35 @@ const TRANSLATIONS = {
clearAllConfirm: clearAllConfirm:
"Tem certeza que deseja limpar todo o histórico? Esta ação é irreversível.", "Tem certeza que deseja limpar todo o histórico? Esta ação é irreversível.",
expand: "Expandir", expand: "Expandir",
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -242,6 +242,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -233,6 +233,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -232,6 +232,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -312,6 +312,35 @@ const TRANSLATIONS = {
deleteConfirm: "您确定要删除此历史记录吗?", deleteConfirm: "您确定要删除此历史记录吗?",
clearAllConfirm: "您确定要清除所有历史记录吗?此操作无法撤消。", clearAllConfirm: "您确定要清除所有历史记录吗?此操作无法撤消。",
expand: "展开", expand: "展开",
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -230,6 +230,35 @@ const TRANSLATIONS = {
deleteConfirm: null, deleteConfirm: null,
clearAllConfirm: null, clearAllConfirm: null,
expand: null, expand: null,
publish: null,
},
publish: {
public_description: null,
private_description: null,
success_title: null,
success_description: null,
success_thank_you: null,
view_on_hub: null,
modal_title: null,
name_label: null,
name_description: null,
name_placeholder: null,
description_label: null,
description_description: null,
tags_label: null,
tags_description: null,
tags_placeholder: null,
visibility_label: null,
prompt_label: null,
prompt_description: null,
prompt_placeholder: null,
publish_button: null,
publishing: null,
unauthenticated: {
title: null,
description: null,
button: null,
},
}, },
}, },
refusal: { refusal: {

View File

@ -153,6 +153,34 @@ const CommunityHub = {
}; };
}); });
}, },
/**
* Create a new system prompt in the community hub
* @param {Object} data - The system prompt data
* @param {string} data.name - The name of the prompt
* @param {string} data.description - The description of the prompt
* @param {string} data.prompt - The actual system prompt text
* @param {string[]} data.tags - Array of tags
* @param {string} data.visibility - Either 'public' or 'private'
* @returns {Promise<{success: boolean, error: string | null}>}
*/
createSystemPrompt: async (data) => {
return await fetch(`${API_BASE}/community-hub/system-prompt/create`, {
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 create system prompt");
return { success: true, error: null, itemId: response.item?.id };
})
.catch((e) => ({
success: false,
error: e.message,
}));
},
}; };
export default CommunityHub; export default CommunityHub;

View File

@ -14,6 +14,7 @@ export default function PromptHistoryItem({
user, user,
onRestore, onRestore,
setHistory, setHistory,
onPublishClick,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
@ -82,11 +83,21 @@ export default function PromptHistoryItem({
{showMenu && ( {showMenu && (
<div <div
ref={menuRef} ref={menuRef}
className="absolute right-0 top-6 bg-theme-bg-popup-menu rounded-lg z-50" className="absolute right-0 top-6 bg-theme-bg-popup-menu rounded-lg z-50 min-w-[200px]"
> >
<button <button
type="button" type="button"
className="px-[10px] py-[6px] text-sm text-white hover:bg-theme-hover cursor-pointer border-none" className="px-[10px] py-[6px] text-sm text-white hover:bg-theme-sidebar-item-hover rounded-t-lg cursor-pointer border-none w-full text-left whitespace-nowrap"
onClick={() => {
setShowMenu(false);
onPublishClick(prompt);
}}
>
{t("chat.prompt.history.publish")}
</button>
<button
type="button"
className="px-[10px] py-[6px] text-sm text-white hover:bg-red-500/60 light:hover:bg-red-300/80 rounded-b-lg cursor-pointer border-none w-full text-left whitespace-nowrap"
onClick={() => { onClick={() => {
setShowMenu(false); setShowMenu(false);
deleteHistory(id); deleteHistory(id);

View File

@ -7,7 +7,7 @@ import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css"; import "react-loading-skeleton/dist/skeleton.css";
export default forwardRef(function ChatPromptHistory( export default forwardRef(function ChatPromptHistory(
{ show, workspaceSlug, onRestore, onClose }, { show, workspaceSlug, onRestore, onClose, onPublishClick },
ref ref
) { ) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -92,6 +92,7 @@ export default forwardRef(function ChatPromptHistory(
id={item.id} id={item.id}
{...item} {...item}
onRestore={() => onRestore(item.prompt)} onRestore={() => onRestore(item.prompt)}
onPublishClick={onPublishClick}
setHistory={setHistory} setHistory={setHistory}
/> />
)) ))

View File

@ -6,6 +6,8 @@ import Highlighter from "react-highlight-words";
import { Link, useSearchParams } from "react-router-dom"; import { Link, useSearchParams } from "react-router-dom";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import ChatPromptHistory from "./ChatPromptHistory"; import ChatPromptHistory from "./ChatPromptHistory";
import PublishEntityModal from "@/components/CommunityHub/PublishEntityModal";
import { useModal } from "@/hooks/useModal";
// TODO: Move to backend and have user-language sensitive default prompt // TODO: Move to backend and have user-language sensitive default prompt
const DEFAULT_PROMPT = const DEFAULT_PROMPT =
@ -21,13 +23,12 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
const promptHistoryRef = useRef(null); const promptHistoryRef = useRef(null);
const historyButtonRef = useRef(null); const historyButtonRef = useRef(null);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const {
const handleRestore = (prompt) => { isOpen: showPublishModal,
setPrompt(prompt); closeModal: closePublishModal,
setShowPromptHistory(false); openModal: openPublishModal,
setHasChanges(true); } = useModal();
// TODO: Autosave on restore const [currentPrompt, setCurrentPrompt] = useState(chatPrompt(workspace));
};
useEffect(() => { useEffect(() => {
async function setupVariableHighlighting() { async function setupVariableHighlighting() {
@ -66,6 +67,18 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
}; };
}, []); }, []);
const handleRestore = (prompt) => {
setPrompt(prompt);
setShowPromptHistory(false);
setHasChanges(true);
};
const handlePublishClick = (prompt) => {
setCurrentPrompt(prompt);
setShowPromptHistory(false);
openPublishModal();
};
return ( return (
<> <>
<ChatPromptHistory <ChatPromptHistory
@ -73,6 +86,7 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
workspaceSlug={workspace.slug} workspaceSlug={workspace.slug}
show={showPromptHistory} show={showPromptHistory}
onRestore={handleRestore} onRestore={handleRestore}
onPublishClick={handlePublishClick}
onClose={() => { onClose={() => {
setShowPromptHistory(false); setShowPromptHistory(false);
}} }}
@ -120,7 +134,7 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
<button <button
ref={historyButtonRef} ref={historyButtonRef}
type="button" type="button"
className="text-theme-text-secondary hover:text-white light:hover:text-black text-sm font-medium" className="text-theme-text-secondary hover:text-white light:hover:text-black text-xs font-medium"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setShowPromptHistory(!showPromptHistory); setShowPromptHistory(!showPromptHistory);
@ -186,17 +200,49 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
</div> </div>
<div className="w-full flex flex-row items-center justify-between pt-2"> <div className="w-full flex flex-row items-center justify-between pt-2">
{prompt !== DEFAULT_PROMPT && ( {prompt !== DEFAULT_PROMPT && (
<button <>
type="button" <button
onClick={() => handleRestore(DEFAULT_PROMPT)} type="button"
className="text-theme-text-primary hover:text-white light:hover:text-black text-sm font-medium" onClick={() => handleRestore(DEFAULT_PROMPT)}
> className="text-theme-text-primary hover:text-white light:hover:text-black text-xs font-medium"
Clear >
</button> Clear
</button>
<PublishPromptCTA
hidden={
isEditing ||
prompt === DEFAULT_PROMPT ||
prompt?.trim().length < 10
}
onClick={() => {
setCurrentPrompt(prompt);
openPublishModal();
}}
/>
</>
)} )}
</div> </div>
</div> </div>
</div> </div>
<PublishEntityModal
show={showPublishModal}
onClose={closePublishModal}
entityType="system-prompt"
entity={currentPrompt}
/>
</> </>
); );
} }
function PublishPromptCTA({ hidden = false, onClick }) {
if (hidden) return null;
return (
<button
type="button"
onClick={onClick}
className="border-none text-primary-button hover:text-white light:hover:text-black text-xs font-medium"
>
Publish to Community Hub
</button>
);
}

View File

@ -184,6 +184,9 @@ export default {
viewMoreOfType: function (type) { viewMoreOfType: function (type) {
return `${this.website()}/list/${type}`; return `${this.website()}/list/${type}`;
}, },
viewItem: function (type, id) {
return `${this.website()}/i/${type}/${id}`;
},
trending: () => { trending: () => {
return `/settings/community-hub/trending`; return `/settings/community-hub/trending`;
}, },

View File

@ -61,6 +61,7 @@ export default {
text: { text: {
primary: 'var(--theme-text-primary)', primary: 'var(--theme-text-primary)',
secondary: 'var(--theme-text-secondary)', secondary: 'var(--theme-text-secondary)',
placeholder: 'var(--theme-placeholder)',
}, },
sidebar: { sidebar: {
item: { item: {

View File

@ -181,6 +181,39 @@ function communityHubEndpoints(app) {
} }
} }
); );
app.post(
"/community-hub/:communityHubItemType/create",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { communityHubItemType } = request.params;
const { connectionKey } = await SystemSettings.hubSettings();
if (!connectionKey)
throw new Error("Community Hub connection key not found");
const data = reqBody(request);
const { success, error, itemId } = await CommunityHub.createStaticItem(
communityHubItemType,
data,
connectionKey
);
if (!success) throw new Error(error);
await EventLogs.logEvent(
"community_hub_publish",
{ itemType: communityHubItemType },
response.locals?.user?.id
);
response
.status(200)
.json({ success: true, error: null, item: { id: itemId } });
} catch (error) {
console.error(error);
response.status(500).json({ success: false, error: error.message });
}
}
);
} }
module.exports = { communityHubEndpoints }; module.exports = { communityHubEndpoints };

View File

@ -9,6 +9,7 @@ const CommunityHub = {
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? "http://127.0.0.1:5001/anythingllm-hub/us-central1/external/v1" ? "http://127.0.0.1:5001/anythingllm-hub/us-central1/external/v1"
: "https://hub.external.anythingllm.com/v1", : "https://hub.external.anythingllm.com/v1",
supportedStaticItemTypes: ["system-prompt"],
/** /**
* Validate an import ID and return the entity type and ID. * Validate an import ID and return the entity type and ID.
@ -172,6 +173,41 @@ const CommunityHub = {
return { createdByMe: {}, teamItems: [] }; return { createdByMe: {}, teamItems: [] };
}); });
}, },
/**
* Create a new item in the community hub - Only supports STATIC items for now.
* @param {string} itemType - The type of item to create
* @param {object} data - The item data
* @param {string} connectionKey - The hub connection key
* @returns {Promise<{success: boolean, error: string | null}>}
*/
createStaticItem: async function (itemType, data, connectionKey) {
if (!connectionKey)
return { success: false, error: "Connection key is required" };
if (!this.supportedStaticItemTypes.includes(itemType))
return { success: false, error: "Unsupported item type" };
// If the item has specical considerations or preprocessing, we can delegate that below before sending the request.
// eg: Agent flow files and such.
return await fetch(`${this.apiBase}/${itemType}/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${connectionKey}`,
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((result) => {
if (!!result.error) throw new Error(result.error || "Unknown error");
return { success: true, error: null, itemId: result.item.id };
})
.catch((error) => {
console.error(`Error creating ${itemType}:`, error);
return { success: false, error: error.message };
});
},
}; };
module.exports = { CommunityHub }; module.exports = { CommunityHub };