mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-04-17 18:18:11 +00:00
[FEAT] Slash templates (#1314)
* WIP slash presets * WIP slash command customization CRUD + validations complete * backend slash command support * fix permission setting on new slash commands rework form submit and pattern on frontend * Add field updates for hooks, required=true to field add user<>command constraint to keep them unique enforce uniquness via teritary uid field on table for multi and non-multi user * reset migration --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
6eefd0d280
commit
d36c3ff8b2
11 changed files with 722 additions and 11 deletions
frontend/src
components/WorkspaceChat/ChatContainer/PromptInput/SlashCommands
models
server
endpoints
models
prisma
utils/chats
|
@ -0,0 +1,111 @@
|
|||
import { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { CMD_REGEX } from ".";
|
||||
|
||||
export default function AddPresetModal({ isOpen, onClose, onSave }) {
|
||||
const [command, setCommand] = useState("");
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.target);
|
||||
const sanitizedCommand = command.replace(CMD_REGEX, "");
|
||||
const saved = await onSave({
|
||||
command: `/${sanitizedCommand}`,
|
||||
prompt: form.get("prompt"),
|
||||
description: form.get("description"),
|
||||
});
|
||||
if (saved) setCommand("");
|
||||
};
|
||||
|
||||
const handleCommandChange = (e) => {
|
||||
const value = e.target.value.replace(CMD_REGEX, "");
|
||||
setCommand(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="relative w-full max-w-2xl max-h-full"
|
||||
>
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">Add New Preset</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Command
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<span className="text-white text-sm mr-2 font-bold">/</span>
|
||||
<input
|
||||
name="command"
|
||||
type="text"
|
||||
placeholder="your-command"
|
||||
value={command}
|
||||
onChange={handleCommandChange}
|
||||
maxLength={25}
|
||||
autoComplete="off"
|
||||
required={true}
|
||||
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-white">
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
name="prompt"
|
||||
autoComplete="off"
|
||||
placeholder="This is the content that will be injected in front of your prompt."
|
||||
required={true}
|
||||
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="border-none block mb-2 text-sm font-medium text-white">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
placeholder="Responds with a poem about LLMs."
|
||||
maxLength={80}
|
||||
autoComplete="off"
|
||||
required={true}
|
||||
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import { useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { CMD_REGEX } from ".";
|
||||
|
||||
export default function EditPresetModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
preset,
|
||||
}) {
|
||||
const [command, setCommand] = useState(preset?.command?.slice(1) || "");
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.target);
|
||||
const sanitizedCommand = command.replace(CMD_REGEX, "");
|
||||
onSave({
|
||||
id: preset.id,
|
||||
command: `/${sanitizedCommand}`,
|
||||
prompt: form.get("prompt"),
|
||||
description: form.get("description"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleCommandChange = (e) => {
|
||||
const value = e.target.value.replace(CMD_REGEX, "");
|
||||
setCommand(value);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmDelete = window.confirm(
|
||||
"Are you sure you want to delete this preset?"
|
||||
);
|
||||
if (!confirmDelete) return;
|
||||
|
||||
setDeleting(true);
|
||||
await onDelete(preset.id);
|
||||
setDeleting(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="relative w-full max-w-2xl max-h-full"
|
||||
>
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||
<h3 className="text-xl font-semibold text-white">Edit Preset</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="command"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Command
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<span className="text-white text-sm mr-2 font-bold">/</span>
|
||||
<input
|
||||
type="text"
|
||||
name="command"
|
||||
placeholder="your-command"
|
||||
value={command}
|
||||
onChange={handleCommandChange}
|
||||
required={true}
|
||||
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="prompt"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Prompt
|
||||
</label>
|
||||
<textarea
|
||||
name="prompt"
|
||||
placeholder="This is a test prompt. Please respond with a poem about LLMs."
|
||||
defaultValue={preset.prompt}
|
||||
required={true}
|
||||
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 text-sm font-medium text-white"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
defaultValue={preset.description}
|
||||
placeholder="Responds with a poem about LLMs."
|
||||
required={true}
|
||||
className="border-none bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<button
|
||||
disabled={deleting}
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-lg text-red-500 hover:bg-red-500/25 transition-all duration-300 disabled:opacity-50"
|
||||
>
|
||||
{deleting ? "Deleting..." : "Delete Preset"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useIsAgentSessionActive } from "@/utils/chat/agent";
|
||||
import AddPresetModal from "./AddPresetModal";
|
||||
import EditPresetModal from "./EditPresetModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import System from "@/models/system";
|
||||
import { DotsThree, Plus } from "@phosphor-icons/react";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
export const CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);
|
||||
export default function SlashPresets({ setShowing, sendCommand }) {
|
||||
const isActiveAgentSession = useIsAgentSessionActive();
|
||||
const {
|
||||
isOpen: isAddModalOpen,
|
||||
openModal: openAddModal,
|
||||
closeModal: closeAddModal,
|
||||
} = useModal();
|
||||
const {
|
||||
isOpen: isEditModalOpen,
|
||||
openModal: openEditModal,
|
||||
closeModal: closeEditModal,
|
||||
} = useModal();
|
||||
const [presets, setPresets] = useState([]);
|
||||
const [selectedPreset, setSelectedPreset] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPresets();
|
||||
}, []);
|
||||
if (isActiveAgentSession) return null;
|
||||
|
||||
const fetchPresets = async () => {
|
||||
const presets = await System.getSlashCommandPresets();
|
||||
setPresets(presets);
|
||||
};
|
||||
|
||||
const handleSavePreset = async (preset) => {
|
||||
const { error } = await System.createSlashCommandPreset(preset);
|
||||
if (!!error) {
|
||||
showToast(error, "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
fetchPresets();
|
||||
closeAddModal();
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleEditPreset = (preset) => {
|
||||
setSelectedPreset(preset);
|
||||
openEditModal();
|
||||
};
|
||||
|
||||
const handleUpdatePreset = async (updatedPreset) => {
|
||||
const { error } = await System.updateSlashCommandPreset(
|
||||
updatedPreset.id,
|
||||
updatedPreset
|
||||
);
|
||||
|
||||
if (!!error) {
|
||||
showToast(error, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
fetchPresets();
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
const handleDeletePreset = async (presetId) => {
|
||||
await System.deleteSlashCommandPreset(presetId);
|
||||
fetchPresets();
|
||||
closeEditModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{presets.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
onClick={() => {
|
||||
setShowing(false);
|
||||
sendCommand(`${preset.command} `, false);
|
||||
}}
|
||||
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-2 rounded-xl flex flex-row justify-start"
|
||||
>
|
||||
<div className="w-full flex-col text-left flex pointer-events-none">
|
||||
<div className="text-white text-sm font-bold">{preset.command}</div>
|
||||
<div className="text-white text-opacity-60 text-sm">
|
||||
{preset.description}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditPreset(preset);
|
||||
}}
|
||||
className="text-white text-sm p-1 hover:cursor-pointer hover:bg-zinc-900 rounded-full mt-1"
|
||||
>
|
||||
<DotsThree size={24} weight="bold" />
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={openAddModal}
|
||||
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-1 rounded-xl flex flex-col justify-start"
|
||||
>
|
||||
<div className="w-full flex-row flex pointer-events-none items-center gap-2">
|
||||
<Plus size={24} weight="fill" fill="white" />
|
||||
<div className="text-white text-sm font-medium">Add New Preset </div>
|
||||
</div>
|
||||
</button>
|
||||
<AddPresetModal
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={closeAddModal}
|
||||
onSave={handleSavePreset}
|
||||
/>
|
||||
{selectedPreset && (
|
||||
<EditPresetModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={closeEditModal}
|
||||
onSave={handleUpdatePreset}
|
||||
onDelete={handleDeletePreset}
|
||||
preset={selectedPreset}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@ import SlashCommandIcon from "./icons/slash-commands-icon.svg";
|
|||
import { Tooltip } from "react-tooltip";
|
||||
import ResetCommand from "./reset";
|
||||
import EndAgentSession from "./endAgentSession";
|
||||
import SlashPresets from "./SlashPresets";
|
||||
|
||||
export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
|
||||
return (
|
||||
|
@ -52,10 +53,11 @@ export function SlashCommands({ showing, setShowing, sendCommand }) {
|
|||
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
|
||||
<div
|
||||
ref={cmdRef}
|
||||
className="w-[600px] p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
|
||||
className="w-[600px] overflow-auto p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
|
||||
>
|
||||
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
|
||||
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
|
||||
<SlashPresets sendCommand={sendCommand} setShowing={setShowing} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -567,6 +567,74 @@ const System = {
|
|||
});
|
||||
},
|
||||
dataConnectors: DataConnector,
|
||||
|
||||
getSlashCommandPresets: async function () {
|
||||
return await fetch(`${API_BASE}/system/slash-command-presets`, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not fetch slash command presets.");
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => res.presets)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
createSlashCommandPreset: async function (presetData) {
|
||||
return await fetch(`${API_BASE}/system/slash-command-presets`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify(presetData),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not create slash command preset.");
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => {
|
||||
return { preset: res.preset, error: null };
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { preset: null, error: e.message };
|
||||
});
|
||||
},
|
||||
|
||||
updateSlashCommandPreset: async function (presetId, presetData) {
|
||||
return await fetch(`${API_BASE}/system/slash-command-presets/${presetId}`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify(presetData),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not update slash command preset.");
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => {
|
||||
return { preset: res.preset, error: null };
|
||||
})
|
||||
.catch((e) => {
|
||||
return { preset: null, error: "Failed to update this command." };
|
||||
});
|
||||
},
|
||||
|
||||
deleteSlashCommandPreset: async function (presetId) {
|
||||
return await fetch(`${API_BASE}/system/slash-command-presets/${presetId}`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not delete slash command preset.");
|
||||
return true;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default System;
|
||||
|
|
|
@ -50,6 +50,7 @@ const {
|
|||
resetPassword,
|
||||
generateRecoveryCodes,
|
||||
} = require("../utils/PasswordRecovery");
|
||||
const { SlashCommandPresets } = require("../models/slashCommandsPresets");
|
||||
|
||||
function systemEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
@ -1044,6 +1045,111 @@ function systemEndpoints(app) {
|
|||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/system/slash-command-presets",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const userPresets = await SlashCommandPresets.getUserPresets(user?.id);
|
||||
response.status(200).json({ presets: userPresets });
|
||||
} catch (error) {
|
||||
console.error("Error fetching slash command presets:", error);
|
||||
response.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/system/slash-command-presets",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const { command, prompt, description } = reqBody(request);
|
||||
const presetData = {
|
||||
command: SlashCommandPresets.formatCommand(String(command)),
|
||||
prompt: String(prompt),
|
||||
description: String(description),
|
||||
};
|
||||
|
||||
const preset = await SlashCommandPresets.create(user?.id, presetData);
|
||||
if (!preset) {
|
||||
return response
|
||||
.status(500)
|
||||
.json({ message: "Failed to create preset" });
|
||||
}
|
||||
response.status(201).json({ preset });
|
||||
} catch (error) {
|
||||
console.error("Error creating slash command preset:", error);
|
||||
response.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/system/slash-command-presets/:slashCommandId",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const { slashCommandId } = request.params;
|
||||
const { command, prompt, description } = reqBody(request);
|
||||
|
||||
// Valid user running owns the preset if user session is valid.
|
||||
const ownsPreset = await SlashCommandPresets.get({
|
||||
userId: user?.id ?? null,
|
||||
id: Number(slashCommandId),
|
||||
});
|
||||
if (!ownsPreset)
|
||||
return response.status(404).json({ message: "Preset not found" });
|
||||
|
||||
const updates = {
|
||||
command: SlashCommandPresets.formatCommand(String(command)),
|
||||
prompt: String(prompt),
|
||||
description: String(description),
|
||||
};
|
||||
|
||||
const preset = await SlashCommandPresets.update(
|
||||
Number(slashCommandId),
|
||||
updates
|
||||
);
|
||||
if (!preset) return response.sendStatus(422);
|
||||
response.status(200).json({ preset: { ...ownsPreset, ...updates } });
|
||||
} catch (error) {
|
||||
console.error("Error updating slash command preset:", error);
|
||||
response.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/system/slash-command-presets/:slashCommandId",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { slashCommandId } = request.params;
|
||||
const user = await userFromSession(request, response);
|
||||
|
||||
// Valid user running owns the preset if user session is valid.
|
||||
const ownsPreset = await SlashCommandPresets.get({
|
||||
userId: user?.id ?? null,
|
||||
id: Number(slashCommandId),
|
||||
});
|
||||
if (!ownsPreset)
|
||||
return response
|
||||
.status(403)
|
||||
.json({ message: "Failed to delete preset" });
|
||||
|
||||
await SlashCommandPresets.delete(Number(slashCommandId));
|
||||
response.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error("Error deleting slash command preset:", error);
|
||||
response.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { systemEndpoints };
|
||||
|
|
105
server/models/slashCommandsPresets.js
Normal file
105
server/models/slashCommandsPresets.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
const { v4 } = require("uuid");
|
||||
const prisma = require("../utils/prisma");
|
||||
const CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);
|
||||
|
||||
const SlashCommandPresets = {
|
||||
formatCommand: function (command = "") {
|
||||
if (!command || command.length < 2) return `/${v4().split("-")[0]}`;
|
||||
|
||||
let adjustedCmd = command.toLowerCase(); // force lowercase
|
||||
if (!adjustedCmd.startsWith("/")) adjustedCmd = `/${adjustedCmd}`; // Fix if no preceding / is found.
|
||||
return `/${adjustedCmd.slice(1).toLowerCase().replace(CMD_REGEX, "-")}`; // replace any invalid chars with '-'
|
||||
},
|
||||
|
||||
get: async function (clause = {}) {
|
||||
try {
|
||||
const preset = await prisma.slash_command_presets.findFirst({
|
||||
where: clause,
|
||||
});
|
||||
return preset || null;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
where: async function (clause = {}, limit) {
|
||||
try {
|
||||
const presets = await prisma.slash_command_presets.findMany({
|
||||
where: clause,
|
||||
take: limit || undefined,
|
||||
});
|
||||
return presets;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Command + userId must be unique combination.
|
||||
create: async function (userId = null, presetData = {}) {
|
||||
try {
|
||||
const preset = await prisma.slash_command_presets.create({
|
||||
data: {
|
||||
...presetData,
|
||||
// This field (uid) is either the user_id or 0 (for non-multi-user mode).
|
||||
// the UID field enforces the @@unique(userId, command) constraint since
|
||||
// the real relational field (userId) cannot be non-null so this 'dummy' field gives us something
|
||||
// to constrain against within the context of prisma and sqlite that works.
|
||||
uid: userId ? Number(userId) : 0,
|
||||
userId: userId ? Number(userId) : null,
|
||||
},
|
||||
});
|
||||
return preset;
|
||||
} catch (error) {
|
||||
console.error("Failed to create preset", error.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getUserPresets: async function (userId = null) {
|
||||
try {
|
||||
return (
|
||||
await prisma.slash_command_presets.findMany({
|
||||
where: { userId: !!userId ? Number(userId) : null },
|
||||
orderBy: { createdAt: "asc" },
|
||||
})
|
||||
)?.map((preset) => ({
|
||||
id: preset.id,
|
||||
command: preset.command,
|
||||
prompt: preset.prompt,
|
||||
description: preset.description,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to get user presets", error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
update: async function (presetId = null, presetData = {}) {
|
||||
try {
|
||||
const preset = await prisma.slash_command_presets.update({
|
||||
where: { id: Number(presetId) },
|
||||
data: presetData,
|
||||
});
|
||||
return preset;
|
||||
} catch (error) {
|
||||
console.error("Failed to update preset", error.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
delete: async function (presetId = null) {
|
||||
try {
|
||||
await prisma.slash_command_presets.delete({
|
||||
where: { id: Number(presetId) },
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete preset", error.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.SlashCommandPresets = SlashCommandPresets;
|
15
server/prisma/migrations/20240510032311_init/migration.sql
Normal file
15
server/prisma/migrations/20240510032311_init/migration.sql
Normal file
|
@ -0,0 +1,15 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "slash_command_presets" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"command" TEXT NOT NULL,
|
||||
"prompt" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"uid" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "slash_command_presets_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "slash_command_presets_uid_command_key" ON "slash_command_presets"("uid", "command");
|
|
@ -73,6 +73,7 @@ model users {
|
|||
recovery_codes recovery_codes[]
|
||||
password_reset_tokens password_reset_tokens[]
|
||||
workspace_agent_invocations workspace_agent_invocations[]
|
||||
slash_command_presets slash_command_presets[]
|
||||
}
|
||||
|
||||
model recovery_codes {
|
||||
|
@ -260,3 +261,17 @@ model event_logs {
|
|||
|
||||
@@index([event])
|
||||
}
|
||||
|
||||
model slash_command_presets {
|
||||
id Int @id @default(autoincrement())
|
||||
command String
|
||||
prompt String
|
||||
description String
|
||||
uid Int @default(0) // 0 is null user
|
||||
userId Int?
|
||||
createdAt DateTime @default(now())
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
user users? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([uid, command])
|
||||
}
|
||||
|
|
|
@ -4,14 +4,28 @@ const { resetMemory } = require("./commands/reset");
|
|||
const { getVectorDbClass, getLLMProvider } = require("../helpers");
|
||||
const { convertToPromptHistory } = require("../helpers/chat/responses");
|
||||
const { DocumentManager } = require("../DocumentManager");
|
||||
const { SlashCommandPresets } = require("../../models/slashCommandsPresets");
|
||||
|
||||
const VALID_COMMANDS = {
|
||||
"/reset": resetMemory,
|
||||
};
|
||||
|
||||
function grepCommand(message) {
|
||||
async function grepCommand(message, user = null) {
|
||||
const userPresets = await SlashCommandPresets.getUserPresets(user?.id);
|
||||
const availableCommands = Object.keys(VALID_COMMANDS);
|
||||
|
||||
// Check if the message starts with any preset command
|
||||
const foundPreset = userPresets.find((p) => message.startsWith(p.command));
|
||||
if (!!foundPreset) {
|
||||
// Replace the preset command with the corresponding prompt
|
||||
const updatedMessage = message.replace(
|
||||
foundPreset.command,
|
||||
foundPreset.prompt
|
||||
);
|
||||
return updatedMessage;
|
||||
}
|
||||
|
||||
// Check if the message starts with any built-in command
|
||||
for (let i = 0; i < availableCommands.length; i++) {
|
||||
const cmd = availableCommands[i];
|
||||
const re = new RegExp(`^(${cmd})`, "i");
|
||||
|
@ -20,7 +34,7 @@ function grepCommand(message) {
|
|||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return message;
|
||||
}
|
||||
|
||||
async function chatWithWorkspace(
|
||||
|
@ -31,10 +45,10 @@ async function chatWithWorkspace(
|
|||
thread = null
|
||||
) {
|
||||
const uuid = uuidv4();
|
||||
const command = grepCommand(message);
|
||||
const updatedMessage = await grepCommand(message, user);
|
||||
|
||||
if (!!command && Object.keys(VALID_COMMANDS).includes(command)) {
|
||||
return await VALID_COMMANDS[command](workspace, message, uuid, user);
|
||||
if (Object.keys(VALID_COMMANDS).includes(updatedMessage)) {
|
||||
return await VALID_COMMANDS[updatedMessage](workspace, message, uuid, user);
|
||||
}
|
||||
|
||||
const LLMConnector = getLLMProvider({
|
||||
|
@ -164,7 +178,7 @@ async function chatWithWorkspace(
|
|||
const messages = await LLMConnector.compressMessages(
|
||||
{
|
||||
systemPrompt: chatPrompt(workspace),
|
||||
userPrompt: message,
|
||||
userPrompt: updatedMessage,
|
||||
contextTexts,
|
||||
chatHistory,
|
||||
},
|
||||
|
|
|
@ -23,10 +23,10 @@ async function streamChatWithWorkspace(
|
|||
thread = null
|
||||
) {
|
||||
const uuid = uuidv4();
|
||||
const command = grepCommand(message);
|
||||
const updatedMessage = await grepCommand(message, user);
|
||||
|
||||
if (!!command && Object.keys(VALID_COMMANDS).includes(command)) {
|
||||
const data = await VALID_COMMANDS[command](
|
||||
if (Object.keys(VALID_COMMANDS).includes(updatedMessage)) {
|
||||
const data = await VALID_COMMANDS[updatedMessage](
|
||||
workspace,
|
||||
message,
|
||||
uuid,
|
||||
|
@ -185,7 +185,7 @@ async function streamChatWithWorkspace(
|
|||
const messages = await LLMConnector.compressMessages(
|
||||
{
|
||||
systemPrompt: chatPrompt(workspace),
|
||||
userPrompt: message,
|
||||
userPrompt: updatedMessage,
|
||||
contextTexts,
|
||||
chatHistory,
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue