θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content

Publish system prompts to hub #3976

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
43 changes: 43 additions & 0 deletions frontend/src/components/CommunityHub/PublishEntityModal/index.jsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
@@ -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>
);
}
30 changes: 30 additions & 0 deletions frontend/src/hooks/useCommunityHubAuth.js
Original file line number Diff line number Diff line change
@@ -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 };
}
2 changes: 2 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
--theme-bg-chat-input: #27282a;
--theme-text-primary: #ffffff;
--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-selected: rgba(255, 255, 255, 0.3);
--theme-sidebar-item-hover: #3f3f42;
Expand Down Expand Up @@ -116,6 +117,7 @@
--theme-bg-chat-input: #eaeaea;
--theme-text-primary: #0e0f0f;
--theme-text-secondary: #7a7d7e;
--theme-placeholder: #9ca3af;
--theme-sidebar-item-default: #ffffff;
--theme-sidebar-item-selected: #ffffff;
--theme-sidebar-item-hover: #c8efff;
Expand Down
Loading