From b391df0f71f4a909734a3e217c18a99f47373e18 Mon Sep 17 00:00:00 2001 From: Dominik Marti Date: Tue, 30 Sep 2025 22:16:51 +0200 Subject: [PATCH] feat: add custom system prompts to projects, is combined with assistants system prompt when sending messages --- web-app/src/containers/ChatInput.tsx | 29 +- web-app/src/containers/LeftPanel.tsx | 6 +- .../containers/dialogs/AddProjectDialog.tsx | 45 +- .../useChat.projectSystemPrompt.test.ts | 441 ++++++++++++++++++ .../__tests__/useThreadManagement.test.ts | 232 +++++++++ web-app/src/hooks/useChat.ts | 51 +- web-app/src/hooks/useThreadManagement.ts | 17 +- web-app/src/locales/de-DE/common.json | 6 +- web-app/src/locales/en/common.json | 3 +- web-app/src/routes/project/index.tsx | 6 +- 10 files changed, 784 insertions(+), 52 deletions(-) create mode 100644 web-app/src/hooks/__tests__/useChat.projectSystemPrompt.test.ts create mode 100644 web-app/src/hooks/__tests__/useThreadManagement.test.ts diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index cba580ebd6..2035b3125f 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -4,7 +4,6 @@ import TextareaAutosize from 'react-textarea-autosize' import { cn } from '@/lib/utils' import { usePrompt } from '@/hooks/usePrompt' import { useThreads } from '@/hooks/useThreads' -import { useThreadManagement } from '@/hooks/useThreadManagement' import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { @@ -50,8 +49,7 @@ type ChatInputProps = { const ChatInput = ({ model, className, - initialMessage, - projectId, + initialMessage }: ChatInputProps) => { const textareaRef = useRef(null) const [isFocused, setIsFocused] = useState(false) @@ -65,8 +63,6 @@ const ChatInput = ({ const prompt = usePrompt((state) => state.prompt) const setPrompt = usePrompt((state) => state.setPrompt) const currentThreadId = useThreads((state) => state.currentThreadId) - const updateThread = useThreads((state) => state.updateThread) - const { getFolderById } = useThreadManagement() const { t } = useTranslation() const spellCheckChatInput = useGeneralSetting( (state) => state.spellCheckChatInput @@ -187,27 +183,8 @@ const ChatInput = ({ ) setUploadedFiles([]) - // Handle project assignment for new threads - if (projectId && !currentThreadId) { - const project = getFolderById(projectId) - if (project) { - // Use setTimeout to ensure the thread is created first - setTimeout(() => { - const newCurrentThreadId = useThreads.getState().currentThreadId - if (newCurrentThreadId) { - updateThread(newCurrentThreadId, { - metadata: { - project: { - id: project.id, - name: project.name, - updated_at: project.updated_at, - }, - }, - }) - } - }, 100) - } - } + // Note: Project assignment for new threads is now handled directly in useChat.ts + // when creating the thread, ensuring the project system prompt is applied from the first message } useEffect(() => { diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 24f3bf9119..9053f0af22 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -208,11 +208,11 @@ const LeftPanel = () => { } } - const handleProjectSave = (name: string) => { + const handleProjectSave = (name: string, systemPrompt?: string) => { if (editingProjectKey) { - updateFolder(editingProjectKey, name) + updateFolder(editingProjectKey, name, systemPrompt) } else { - addFolder(name) + addFolder(name, systemPrompt) } setProjectDialogOpen(false) setEditingProjectKey(null) diff --git a/web-app/src/containers/dialogs/AddProjectDialog.tsx b/web-app/src/containers/dialogs/AddProjectDialog.tsx index f0fda648c2..cf709e7c87 100644 --- a/web-app/src/containers/dialogs/AddProjectDialog.tsx +++ b/web-app/src/containers/dialogs/AddProjectDialog.tsx @@ -8,6 +8,7 @@ import { } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' import { useThreadManagement } from '@/hooks/useThreadManagement' import { toast } from 'sonner' import { useTranslation } from '@/i18n/react-i18next-compat' @@ -20,8 +21,9 @@ interface AddProjectDialogProps { id: string name: string updated_at: number + systemPrompt?: string } - onSave: (name: string) => void + onSave: (name: string, systemPrompt?: string) => void } export default function AddProjectDialog({ @@ -33,11 +35,13 @@ export default function AddProjectDialog({ }: AddProjectDialogProps) { const { t } = useTranslation() const [name, setName] = useState(initialData?.name || '') + const [systemPrompt, setSystemPrompt] = useState(initialData?.systemPrompt || '') const { folders } = useThreadManagement() useEffect(() => { if (open) { setName(initialData?.name || '') + setSystemPrompt(initialData?.systemPrompt || '') } }, [open, initialData]) @@ -58,16 +62,23 @@ export default function AddProjectDialog({ return } - onSave(trimmedName) + onSave(trimmedName, systemPrompt.trim() || undefined) // Show detailed success message if (editingKey && initialData) { - toast.success( - t('projects.addProjectDialog.renameSuccess', { - oldName: initialData.name, - newName: trimmedName - }) - ) + const nameChanged = trimmedName !== initialData.name + + if (nameChanged) { + toast.success( + t('projects.addProjectDialog.renameSuccess', { + oldName: initialData.name, + newName: trimmedName + }) + ) + } else { + // Only system prompt changed + toast.success(t('projects.addProjectDialog.updateSuccess', { projectName: trimmedName })) + } } else { toast.success(t('projects.addProjectDialog.createSuccess', { projectName: trimmedName })) } @@ -78,11 +89,15 @@ export default function AddProjectDialog({ const handleCancel = () => { onOpenChange(false) setName('') + setSystemPrompt('') } // Check if the button should be disabled const isButtonDisabled = - !name.trim() || (editingKey && name.trim() === initialData?.name) + !name.trim() || + (editingKey && + name.trim() === initialData?.name && + systemPrompt.trim() === (initialData?.systemPrompt)) return ( @@ -110,6 +125,18 @@ export default function AddProjectDialog({ }} /> +
+ +