Chat prompt history menu (#3770)
* wip system prompt history sidebar ui * lint * backend/frontend implementation for prompt history wip * lint * rework ui * add delete menu and delete chat history by id * lint * ref for menu button * reorganize components + light mode styles * lint * UI refactor * backend refactor * remove unused import * add border-none to all buttons * fix spacing on dots 3 icon button * add window to confirm * add english translations * normalize translations * lin * patch store logic * sticky header --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
7a137c119b
commit
10e65fc021
@ -231,6 +231,16 @@ const TRANSLATIONS = {
|
||||
title: "النداء",
|
||||
description:
|
||||
"النداء التي سيتم استخدامه في مساحة العمل هذه. حدد السياق والتعليمات للذكاء الاصطناعي للاستجابة. يجب عليك تقديم نداء مصمم بعناية حتى يتمكن الذكاء الاصطناعي من إنشاء استجابة دقيقة وذات صلة.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "الرد على رفض وضعية الاستعلام",
|
||||
|
||||
@ -232,6 +232,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"Prompten, der vil blive brugt i dette arbejdsområde. Definér konteksten og instruktionerne til, at AI'en kan generere et svar. Du bør levere en omhyggeligt udformet prompt, så AI'en kan generere et relevant og præcist svar.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Afvisningssvar for forespørgsels-tilstand",
|
||||
|
||||
@ -224,6 +224,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"Der Prompt, der in diesem Arbeitsbereich verwendet wird. Definieren Sie den Kontext und die Anweisungen für die KI, um eine Antwort zu generieren. Sie sollten einen sorgfältig formulierten Prompt bereitstellen, damit die KI eine relevante und genaue Antwort generieren kann.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Abfragemodus-Ablehnungsantwort",
|
||||
|
||||
@ -325,9 +325,20 @@ const TRANSLATIONS = {
|
||||
"Anything more than 45 is likely to lead to continuous chat failures depending on message size.",
|
||||
},
|
||||
prompt: {
|
||||
title: "Prompt",
|
||||
title: "System Prompt",
|
||||
description:
|
||||
"The prompt that will be used on this workspace. Define the context and instructions for the AI to generate a response. You should to provide a carefully crafted prompt so the AI can generate a relevant and accurate response.",
|
||||
history: {
|
||||
title: "System Prompt History",
|
||||
clearAll: "Clear All",
|
||||
noHistory: "No system prompt history available",
|
||||
restore: "Restore",
|
||||
delete: "Delete",
|
||||
deleteConfirm: "Are you sure you want to delete this history item?",
|
||||
clearAllConfirm:
|
||||
"Are you sure you want to clear all history? This action cannot be undone.",
|
||||
expand: "Expand",
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Query mode refusal response",
|
||||
|
||||
@ -226,6 +226,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"El prompt que se utilizará en este espacio de trabajo. Define el contexto y las instrucciones para que la IA genere una respuesta. Debes proporcionar un prompt cuidadosamente elaborado para que la IA pueda generar una respuesta relevante y precisa.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Respuesta de rechazo en modo consulta",
|
||||
|
||||
@ -222,6 +222,16 @@ const TRANSLATIONS = {
|
||||
title: "پیشمتن",
|
||||
description:
|
||||
"پیشمتنی که در این فضای کاری استفاده خواهد شد. زمینه و دستورالعملها را برای تولید پاسخ توسط هوش مصنوعی تعریف کنید. باید یک پیشمتن دقیق ارائه دهید تا هوش مصنوعی بتواند پاسخی مرتبط و دقیق تولید کند.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "پاسخ رد در حالت پرسوجو",
|
||||
|
||||
@ -227,6 +227,16 @@ const TRANSLATIONS = {
|
||||
title: "Invite",
|
||||
description:
|
||||
"L'invite qui sera utilisée sur cet espace de travail. Définissez le contexte et les instructions pour que l'IA génère une réponse. Vous devez fournir une invite soigneusement conçue pour que l'IA puisse générer une réponse pertinente et précise.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Réponse de refus en mode requête",
|
||||
|
||||
@ -220,6 +220,16 @@ const TRANSLATIONS = {
|
||||
title: "בקשה",
|
||||
description:
|
||||
"הבקשה שתיעשה שימוש בה בסביבת העבודה הזו. הגדר את ההקשר וההוראות עבור ה-AI כדי ליצור תגובה. עליך לספק בקשה מעוצבת בקפידה כדי שה-AI יוכל ליצור תגובה רלוונטית ומדויקת.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "תגובת סירוב במצב שאילתה",
|
||||
|
||||
@ -225,6 +225,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"Il prompt che verrà utilizzato in quest'area di lavoro. Definisci il contesto e le istruzioni affinché l'IA generi una risposta. Dovresti fornire un prompt elaborato con cura in modo che l'IA possa generare una risposta pertinente e accurata.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Risposta al rifiuto nella modalità di query",
|
||||
|
||||
@ -231,6 +231,16 @@ const TRANSLATIONS = {
|
||||
title: "プロンプト",
|
||||
description:
|
||||
"このワークスペースで使用するプロンプトです。AIが適切な応答を生成できるよう、コンテキストや指示を定義してください。",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "クエリモード拒否応答",
|
||||
|
||||
@ -220,6 +220,16 @@ const TRANSLATIONS = {
|
||||
title: "프롬프트",
|
||||
description:
|
||||
"이 워크스페이스에서 사용할 프롬프트입니다. AI가 응답을 생성하기 위해 문맥과 지침을 정의합니다. AI가 질문에 대하여 정확한 응답을 생성할 수 있도록 신중하게 프롬프트를 제공해야 합니다.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "쿼리 모드 거부 응답 메시지",
|
||||
|
||||
@ -224,6 +224,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"De prompt die in deze werkruimte zal worden gebruikt. Definieer de context en instructies voor de AI om een reactie te genereren. Je moet een zorgvuldig samengestelde prompt geven zodat de AI een relevante en nauwkeurige reactie kan genereren.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Afwijzingsreactie in Querymodus",
|
||||
|
||||
@ -231,6 +231,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"O prompt que será usado neste workspace. Defina o contexto e as instruções para que a IA gere uma resposta. Você deve fornecer um prompt cuidadosamente elaborado para que a IA possa gerar uma resposta relevante e precisa.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Resposta de Recusa no Modo de Consulta",
|
||||
|
||||
@ -233,6 +233,16 @@ const TRANSLATIONS = {
|
||||
title: "Подсказка",
|
||||
description:
|
||||
"Подсказка, которая будет использоваться в этом рабочем пространстве. Определите контекст и инструкции для AI для создания ответа. Вы должны предоставить тщательно разработанную подсказку, чтобы AI мог генерировать релевантный и точный ответ.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Ответ об отказе в режиме запроса",
|
||||
|
||||
@ -224,6 +224,16 @@ const TRANSLATIONS = {
|
||||
title: "Komut (Prompt)",
|
||||
description:
|
||||
"Bu çalışma alanında kullanılacak komut. Yapay zekanın yanıt üretmesi için bağlam ve talimatları tanımlayın. Uygun ve doğru yanıtlar almak için özenle hazırlanmış bir komut sağlamalısınız.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Sorgu Modu Ret Yanıtı",
|
||||
|
||||
@ -223,6 +223,16 @@ const TRANSLATIONS = {
|
||||
title: "Prompt",
|
||||
description:
|
||||
"The prompt that will be used on this workspace. Define the context and instructions for the AI to generate a response. You should to provide a carefully crafted prompt so the AI can generate a relevant and accurate response.",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "Query mode refusal response",
|
||||
|
||||
@ -222,6 +222,16 @@ const TRANSLATIONS = {
|
||||
title: "聊天提示",
|
||||
description:
|
||||
"将在此工作区上使用的提示。定义 AI 生成响应的上下文和指令。你应该提供精心设计的提示,以便人工智能可以生成相关且准确的响应。",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "查询模式拒绝响应",
|
||||
|
||||
@ -221,6 +221,16 @@ const TRANSLATIONS = {
|
||||
title: "提示詞",
|
||||
description:
|
||||
"將在此工作區中使用的提示詞。定義 AI 產生回應的上下文和指示。您應該提供精心設計的提示詞,以便 AI 可以產生相關且準確的回應。",
|
||||
history: {
|
||||
title: null,
|
||||
clearAll: null,
|
||||
noHistory: null,
|
||||
restore: null,
|
||||
delete: null,
|
||||
deleteConfirm: null,
|
||||
clearAllConfirm: null,
|
||||
expand: null,
|
||||
},
|
||||
},
|
||||
refusal: {
|
||||
title: "查詢模式拒絕回應",
|
||||
|
||||
84
frontend/src/models/promptHistory.js
Normal file
84
frontend/src/models/promptHistory.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
|
||||
/**
|
||||
* @typedef {Object} PromptHistory
|
||||
* @property {number} id - The ID of the prompt history entry
|
||||
* @property {number} workspaceId - The ID of the workspace
|
||||
* @property {string} prompt - The prompt text
|
||||
* @property {number|null} modifiedBy - The ID of the user who modified the prompt
|
||||
* @property {Date} modifiedAt - The date when the prompt was modified
|
||||
* @property {Object|null} user - The user who modified the prompt
|
||||
*/
|
||||
|
||||
const PromptHistory = {
|
||||
/**
|
||||
* Get all prompt history for a workspace
|
||||
* @param {number} workspaceId - The ID of the workspace
|
||||
* @returns {Promise<PromptHistory[]>} - An array of prompt history entries
|
||||
*/
|
||||
forWorkspace: async function (workspaceId) {
|
||||
try {
|
||||
return await fetch(
|
||||
`${API_BASE}/workspace/${workspaceId}/prompt-history`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.history || [])
|
||||
.catch((error) => {
|
||||
console.error("Error fetching prompt history:", error);
|
||||
return [];
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching prompt history:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete all prompt history for a workspace
|
||||
* @param {number} workspaceId - The ID of the workspace
|
||||
* @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message
|
||||
*/
|
||||
clearAll: async function (workspaceId) {
|
||||
try {
|
||||
return await fetch(
|
||||
`${API_BASE}/workspace/${workspaceId}/prompt-history`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error("Error clearing prompt history:", error);
|
||||
return { success: false, error };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error clearing prompt history:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
delete: async function (id) {
|
||||
try {
|
||||
return await fetch(`${API_BASE}/workspace/prompt-history/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error("Error deleting prompt history:", error);
|
||||
return { success: false, error };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting prompt history:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default PromptHistory;
|
||||
@ -0,0 +1,121 @@
|
||||
import { DotsThreeVertical } from "@phosphor-icons/react";
|
||||
import moment from "moment";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import PromptHistory from "@/models/promptHistory";
|
||||
import truncate from "truncate";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const MAX_PROMPT_LENGTH = 200; // chars
|
||||
|
||||
export default function PromptHistoryItem({
|
||||
id,
|
||||
prompt,
|
||||
modifiedAt,
|
||||
user,
|
||||
onRestore,
|
||||
setHistory,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef(null);
|
||||
const menuButtonRef = useRef(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const deleteHistory = async (id) => {
|
||||
if (window.confirm(t("chat.prompt.history.deleteConfirm"))) {
|
||||
const { success } = await PromptHistory.delete(id);
|
||||
if (success) {
|
||||
setHistory((prevHistory) =>
|
||||
prevHistory.filter((item) => item.id !== id)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
showMenu &&
|
||||
!menuRef.current.contains(event.target) &&
|
||||
!menuButtonRef.current.contains(event.target)
|
||||
) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [showMenu]);
|
||||
|
||||
return (
|
||||
<div className="text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs">
|
||||
{user && (
|
||||
<>
|
||||
<span className="text-primary-button">{user.username}</span>{" "}
|
||||
<span className="mx-1 text-white">•</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-theme-home-text-secondary">
|
||||
{moment(modifiedAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="border-none text-sm cursor-pointer text-theme-text-primary hover:text-primary-button"
|
||||
onClick={onRestore}
|
||||
>
|
||||
{t("chat.prompt.history.restore")}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
ref={menuButtonRef}
|
||||
className="border-none text-theme-text-secondary cursor-pointer hover:text-primary-button flex items-center justify-center"
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
>
|
||||
<DotsThreeVertical size={16} weight="bold" />
|
||||
</button>
|
||||
{showMenu && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute right-0 top-6 bg-black light:bg-white rounded-lg z-50"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="px-[10px] py-[6px] text-theme-text-secondary hover:bg-theme-hover cursor-pointer border-none"
|
||||
onClick={() => {
|
||||
setShowMenu(false);
|
||||
deleteHistory(id);
|
||||
}}
|
||||
>
|
||||
{t("chat.prompt.history.delete")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center mt-1">
|
||||
<div className="text-theme-text-primary text-sm font-medium break-all whitespace-pre-wrap">
|
||||
{prompt.length > MAX_PROMPT_LENGTH && !expanded ? (
|
||||
<>
|
||||
{truncate(prompt, MAX_PROMPT_LENGTH)}{" "}
|
||||
<button
|
||||
type="button"
|
||||
className="text-theme-text-secondary hover:text-primary-button border-none"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{t("chat.prompt.history.expand")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
prompt
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
import { useEffect, useState, forwardRef } from "react";
|
||||
import PromptHistory from "@/models/promptHistory";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import PromptHistoryItem from "./PromptHistoryItem";
|
||||
import * as Skeleton from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default forwardRef(function ChatPromptHistory(
|
||||
{ show, workspaceSlug, onRestore, onClose },
|
||||
ref
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const [history, setHistory] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
function loadHistory() {
|
||||
if (!workspaceSlug) return;
|
||||
setLoading(true);
|
||||
PromptHistory.forWorkspace(workspaceSlug)
|
||||
.then((historyData) => {
|
||||
setHistory(historyData);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function handleClearAll() {
|
||||
if (!workspaceSlug) return;
|
||||
if (window.confirm(t("chat.prompt.history.clearAllConfirm"))) {
|
||||
PromptHistory.clearAll(workspaceSlug)
|
||||
.then(({ success }) => {
|
||||
if (success) setHistory([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (show && workspaceSlug) loadHistory();
|
||||
}, [show, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`fixed right-3 top-3 bottom-3 w-[375px] bg-theme-action-menu-bg light:bg-theme-home-update-card-bg rounded-xl py-4 px-4 z-[9999] overflow-y-hidden ${
|
||||
show
|
||||
? "translate-x-0 opacity-100 visible"
|
||||
: "translate-x-full opacity-0 invisible"
|
||||
} transition-all duration-300`}
|
||||
>
|
||||
<div className="sticky flex items-center justify-between">
|
||||
<div className="text-theme-text-primary text-sm font-semibold">
|
||||
{t("chat.prompt.history.title")}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{history.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium text-theme-text-secondary cursor-pointer hover:text-primary-button border-none"
|
||||
onClick={handleClearAll}
|
||||
>
|
||||
{t("chat.prompt.history.clearAll")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="text-theme-text-secondary cursor-pointer hover:text-primary-button border-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-y-[14px] h-full overflow-y-scroll pb-[50px]">
|
||||
{loading ? (
|
||||
<LoaderSkeleton />
|
||||
) : history.length === 0 ? (
|
||||
<div className="flex text-theme-text-secondary text-sm text-center w-full h-full flex items-center justify-center">
|
||||
{t("chat.prompt.history.noHistory")}
|
||||
</div>
|
||||
) : (
|
||||
history.map((item) => (
|
||||
<PromptHistoryItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
{...item}
|
||||
onRestore={() => onRestore(item.prompt)}
|
||||
setHistory={setHistory}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function LoaderSkeleton() {
|
||||
const highlightColor = "var(--theme-bg-primary)";
|
||||
const baseColor = "var(--theme-bg-secondary)";
|
||||
return (
|
||||
<Skeleton.default
|
||||
height="85px"
|
||||
width="100%"
|
||||
highlightColor={highlightColor}
|
||||
baseColor={baseColor}
|
||||
count={8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -5,15 +5,30 @@ import SystemPromptVariable from "@/models/systemPromptVariable";
|
||||
import Highlighter from "react-highlight-words";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import paths from "@/utils/paths";
|
||||
import ChatPromptHistory from "./ChatPromptHistory";
|
||||
|
||||
// TODO: Move to backend and have user-language sensitive default prompt
|
||||
const DEFAULT_PROMPT =
|
||||
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.";
|
||||
|
||||
export default function ChatPromptSettings({ workspace, setHasChanges }) {
|
||||
const { t } = useTranslation();
|
||||
const [availableVariables, setAvailableVariables] = useState([]);
|
||||
const [prompt, setPrompt] = useState(chatPrompt(workspace));
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [showPromptHistory, setShowPromptHistory] = useState(false);
|
||||
const promptRef = useRef(null);
|
||||
const promptHistoryRef = useRef(null);
|
||||
const historyButtonRef = useRef(null);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const handleRestore = (prompt) => {
|
||||
setPrompt(prompt);
|
||||
setShowPromptHistory(false);
|
||||
setHasChanges(true);
|
||||
// TODO: Autosave on restore
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function setupVariableHighlighting() {
|
||||
const { variables } = await SystemPromptVariable.getAll();
|
||||
@ -33,12 +48,42 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
promptHistoryRef.current &&
|
||||
!promptHistoryRef.current.contains(event.target) &&
|
||||
historyButtonRef.current &&
|
||||
!historyButtonRef.current.contains(event.target)
|
||||
) {
|
||||
setShowPromptHistory(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatPromptHistory
|
||||
ref={promptHistoryRef}
|
||||
workspaceSlug={workspace.slug}
|
||||
show={showPromptHistory}
|
||||
onRestore={handleRestore}
|
||||
onClose={() => {
|
||||
setShowPromptHistory(false);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
{t("chat.prompt.title")}
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
{t("chat.prompt.description")}
|
||||
</p>
|
||||
@ -71,15 +116,23 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="openAiPrompt" defaultValue={prompt} />
|
||||
|
||||
<div className="relative">
|
||||
<span
|
||||
className={`${!!prompt ? "hidden" : "block"} text-sm pointer-events-none absolute top-0 left-0 p-2.5 w-full h-full !text-theme-settings-input-placeholder opacity-60`}
|
||||
<div className="relative w-full flex flex-col items-end">
|
||||
<button
|
||||
ref={historyButtonRef}
|
||||
type="button"
|
||||
className="text-theme-text-secondary hover:text-white light:hover:text-black text-sm font-medium"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowPromptHistory(!showPromptHistory);
|
||||
}}
|
||||
>
|
||||
Given the following conversation, relevant context, and a follow up
|
||||
question, reply with an answer to the current question the user is
|
||||
asking. Return only your response to the question given the above
|
||||
information following the users instructions as needed.
|
||||
{showPromptHistory ? "Hide History" : "View History"}
|
||||
</button>
|
||||
<div className="relative w-full">
|
||||
<span
|
||||
className={`${!!prompt ? "hidden" : "block"} text-sm pointer-events-none absolute top-2 left-0 p-2.5 w-full h-full !text-theme-settings-input-placeholder opacity-60`}
|
||||
>
|
||||
{DEFAULT_PROMPT}
|
||||
</span>
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
@ -131,6 +184,19 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full flex flex-row items-center justify-between pt-2">
|
||||
{prompt !== DEFAULT_PROMPT && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRestore(DEFAULT_PROMPT)}
|
||||
className="text-theme-text-primary hover:text-white light:hover:text-black text-sm font-medium"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,6 +96,7 @@ function workspaceEndpoints(app) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
await Workspace.trackChange(currWorkspace, data, user);
|
||||
const { workspace, message } = await Workspace.update(
|
||||
currWorkspace.id,
|
||||
@ -975,6 +976,67 @@ function workspaceEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/workspace/:slug/prompt-history",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
|
||||
async (_, response) => {
|
||||
try {
|
||||
response.status(200).json({
|
||||
history: await Workspace.promptHistory({
|
||||
workspaceId: response.locals.workspace.id,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching prompt history:", error);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/workspace/:slug/prompt-history",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
||||
validWorkspaceSlug,
|
||||
],
|
||||
async (_, response) => {
|
||||
try {
|
||||
response.status(200).json({
|
||||
success: await Workspace.deleteAllPromptHistory({
|
||||
workspaceId: response.locals.workspace.id,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error clearing prompt history:", error);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/workspace/prompt-history/:id",
|
||||
[
|
||||
validatedRequest,
|
||||
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
||||
validWorkspaceSlug,
|
||||
],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
response.status(200).json({
|
||||
success: await Workspace.deletePromptHistory({
|
||||
workspaceId: response.locals.workspace.id,
|
||||
id: Number(id),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting prompt history:", error);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { workspaceEndpoints };
|
||||
|
||||
111
server/models/promptHistory.js
Normal file
111
server/models/promptHistory.js
Normal file
@ -0,0 +1,111 @@
|
||||
const prisma = require("../utils/prisma");
|
||||
|
||||
const PromptHistory = {
|
||||
new: async function ({ workspaceId, prompt, modifiedBy = null }) {
|
||||
try {
|
||||
const history = await prisma.prompt_history.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
prompt,
|
||||
modifiedBy,
|
||||
},
|
||||
});
|
||||
return { history, message: null };
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return { history: null, message: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the prompt history for a workspace.
|
||||
* @param {number} workspaceId - The ID of the workspace to get prompt history for.
|
||||
* @param {number|null} limit - The maximum number of history items to return.
|
||||
* @param {string|null} orderBy - The field to order the history by.
|
||||
* @returns {Promise<Array<{id: number, prompt: string, modifiedAt: Date, modifiedBy: number, user: {username: string}}>>} A promise that resolves to an array of prompt history objects.
|
||||
*/
|
||||
forWorkspace: async function (
|
||||
workspaceId = null,
|
||||
limit = null,
|
||||
orderBy = null
|
||||
) {
|
||||
if (!workspaceId) return [];
|
||||
try {
|
||||
const history = await prisma.prompt_history.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
...(orderBy !== null
|
||||
? { orderBy }
|
||||
: { orderBy: { modifiedAt: "desc" } }),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return history;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
get: async function (clause = {}, limit = null, orderBy = null) {
|
||||
try {
|
||||
const history = await prisma.prompt_history.findFirst({
|
||||
where: clause,
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
...(orderBy !== null ? { orderBy } : {}),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return history || null;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
delete: async function (clause = {}) {
|
||||
try {
|
||||
await prisma.prompt_history.deleteMany({
|
||||
where: clause,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility method to handle prompt changes and create history entries
|
||||
* @param {import('./workspace').Workspace} workspaceData - The workspace object (previous state)
|
||||
* @param {{id: number, role: string}|null} user - The user making the change
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handlePromptChange: async function (workspaceData, user = null) {
|
||||
try {
|
||||
await this.new({
|
||||
workspaceId: workspaceData.id,
|
||||
prompt: workspaceData.openAiPrompt, // Store previous prompt as history
|
||||
modifiedBy: user?.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to create prompt history:", error.message);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { PromptHistory };
|
||||
@ -5,12 +5,32 @@ const { WorkspaceUser } = require("./workspaceUsers");
|
||||
const { ROLES } = require("../utils/middleware/multiUserProtected");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { User } = require("./user");
|
||||
const { PromptHistory } = require("./promptHistory");
|
||||
|
||||
function isNullOrNaN(value) {
|
||||
if (value === null) return true;
|
||||
return isNaN(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Workspace
|
||||
* @property {number} id - The ID of the workspace
|
||||
* @property {string} name - The name of the workspace
|
||||
* @property {string} slug - The slug of the workspace
|
||||
* @property {string} openAiPrompt - The OpenAI prompt of the workspace
|
||||
* @property {string} openAiTemp - The OpenAI temperature of the workspace
|
||||
* @property {number} openAiHistory - The OpenAI history of the workspace
|
||||
* @property {number} similarityThreshold - The similarity threshold of the workspace
|
||||
* @property {string} chatProvider - The chat provider of the workspace
|
||||
* @property {string} chatModel - The chat model of the workspace
|
||||
* @property {number} topN - The top N of the workspace
|
||||
* @property {string} chatMode - The chat mode of the workspace
|
||||
* @property {string} agentProvider - The agent provider of the workspace
|
||||
* @property {string} agentModel - The agent model of the workspace
|
||||
* @property {string} queryRefusalResponse - The query refusal response of the workspace
|
||||
* @property {string} vectorSearchMode - The vector search mode of the workspace
|
||||
*/
|
||||
|
||||
const Workspace = {
|
||||
defaultPrompt:
|
||||
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
|
||||
@ -416,15 +436,30 @@ const Workspace = {
|
||||
}
|
||||
},
|
||||
|
||||
// We are only tracking this change to determine the need to a prompt library or
|
||||
// prompt assistant feature. If this is something you would like to see - tell us on GitHub!
|
||||
_trackWorkspacePromptChange: async function (prevData, newData, user) {
|
||||
/**
|
||||
* We are tracking this change to determine the need to a prompt library or
|
||||
* prompt assistant feature. If this is something you would like to see - tell us on GitHub!
|
||||
* We now track the prompt change in the PromptHistory model.
|
||||
* which is a sub-model of the Workspace model.
|
||||
* @param {Workspace} prevData - The previous data of the workspace.
|
||||
* @param {Workspace} newData - The new data of the workspace.
|
||||
* @param {{id: number, role: string}|null} user - The user who made the change.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_trackWorkspacePromptChange: async function (prevData, newData, user = null) {
|
||||
if (
|
||||
!!newData?.openAiPrompt && // new prompt is set
|
||||
prevData?.openAiPrompt !== this.defaultPrompt && // previous prompt was not default
|
||||
newData?.openAiPrompt !== prevData?.openAiPrompt // previous and new prompt are not the same
|
||||
)
|
||||
await PromptHistory.handlePromptChange(prevData, user); // log the change to the prompt history
|
||||
|
||||
const { Telemetry } = require("./telemetry");
|
||||
const { EventLogs } = require("./eventLogs");
|
||||
if (
|
||||
!newData?.openAiPrompt ||
|
||||
newData?.openAiPrompt === this.defaultPrompt ||
|
||||
newData?.openAiPrompt === prevData?.openAiPrompt
|
||||
!newData?.openAiPrompt || // no prompt change
|
||||
newData?.openAiPrompt === this.defaultPrompt || // new prompt is default prompt
|
||||
newData?.openAiPrompt === prevData?.openAiPrompt // same prompt
|
||||
)
|
||||
return;
|
||||
|
||||
@ -471,6 +506,53 @@ const Workspace = {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the prompt history for a workspace.
|
||||
* @param {Object} options - The options to get prompt history for.
|
||||
* @param {number} options.workspaceId - The ID of the workspace to get prompt history for.
|
||||
* @returns {Promise<Array<{id: number, prompt: string, modifiedAt: Date, modifiedBy: number, user: {id: number, username: string, role: string}}>>} A promise that resolves to an array of prompt history objects.
|
||||
*/
|
||||
promptHistory: async function ({ workspaceId }) {
|
||||
try {
|
||||
const results = await PromptHistory.forWorkspace(workspaceId);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the prompt history for a workspace.
|
||||
* @param {Object} options - The options to delete the prompt history for.
|
||||
* @param {number} options.workspaceId - The ID of the workspace to delete prompt history for.
|
||||
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating the success of the operation.
|
||||
*/
|
||||
deleteAllPromptHistory: async function ({ workspaceId }) {
|
||||
try {
|
||||
return await PromptHistory.delete({ workspaceId });
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the prompt history for a workspace.
|
||||
* @param {Object} options - The options to delete the prompt history for.
|
||||
* @param {number} options.workspaceId - The ID of the workspace to delete prompt history for.
|
||||
* @param {number} options.id - The ID of the prompt history to delete.
|
||||
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating the success of the operation.
|
||||
*/
|
||||
deletePromptHistory: async function ({ workspaceId, id }) {
|
||||
try {
|
||||
return await PromptHistory.delete({ id, workspaceId });
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { Workspace };
|
||||
|
||||
13
server/prisma/migrations/20250506214129_init/migration.sql
Normal file
13
server/prisma/migrations/20250506214129_init/migration.sql
Normal file
@ -0,0 +1,13 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "prompt_history" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"workspaceId" INTEGER NOT NULL,
|
||||
"prompt" TEXT NOT NULL,
|
||||
"modifiedBy" INTEGER,
|
||||
"modifiedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "prompt_history_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "prompt_history_modifiedBy_fkey" FOREIGN KEY ("modifiedBy") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "prompt_history_workspaceId_idx" ON "prompt_history"("workspaceId");
|
||||
@ -81,6 +81,7 @@ model users {
|
||||
browser_extension_api_keys browser_extension_api_keys[]
|
||||
temporary_auth_tokens temporary_auth_tokens[]
|
||||
system_prompt_variables system_prompt_variables[]
|
||||
prompt_history prompt_history[]
|
||||
}
|
||||
|
||||
model recovery_codes {
|
||||
@ -146,6 +147,7 @@ model workspaces {
|
||||
embed_configs embed_configs[]
|
||||
threads workspace_threads[]
|
||||
workspace_agent_invocations workspace_agent_invocations[]
|
||||
prompt_history prompt_history[]
|
||||
}
|
||||
|
||||
model workspace_threads {
|
||||
@ -341,3 +343,15 @@ model system_prompt_variables {
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model prompt_history {
|
||||
id Int @id @default(autoincrement())
|
||||
workspace workspaces @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
workspaceId Int
|
||||
prompt String
|
||||
modifiedBy Int?
|
||||
modifiedAt DateTime @default(now())
|
||||
user users? @relation(fields: [modifiedBy], references: [id])
|
||||
|
||||
@@index([workspaceId])
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user