diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 9c85560a66..c18e0b8b1e 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -209,6 +209,7 @@ class AgentClient extends BaseClient { { spec: this.options.spec, iconURL: this.options.iconURL, + chatProjectId: this.options.chatProjectId, endpoint: this.options.endpoint, agent_id: this.options.agent.id, modelLabel: this.options.modelLabel, diff --git a/api/server/experimental.js b/api/server/experimental.js index 8fd94c6723..c20f6813ca 100644 --- a/api/server/experimental.js +++ b/api/server/experimental.js @@ -382,6 +382,7 @@ if (cluster.isMaster) { app.use('/api/messages', routes.messages); app.use('/api/convos', routes.convos); app.use('/api/presets', routes.presets); + app.use('/api/projects', routes.projects); app.use('/api/prompts', routes.prompts); app.use('/api/skills', routes.skills); app.use('/api/categories', routes.categories); diff --git a/api/server/index.js b/api/server/index.js index bd2d260e76..d96180cc4e 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -201,6 +201,7 @@ const startServer = async () => { app.use('/api/messages', routes.messages); app.use('/api/convos', routes.convos); app.use('/api/presets', routes.presets); + app.use('/api/projects', routes.projects); app.use('/api/prompts', routes.prompts); app.use('/api/skills', routes.skills); app.use('/api/categories', routes.categories); diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 8dfe8621ef..c9d77189cd 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -31,6 +31,9 @@ const assistantClients = { const router = express.Router(); router.use(requireJwtAuth); +const isValidProjectFilter = (projectId) => + !projectId || projectId === 'unassigned' || /^[a-f\d]{24}$/i.test(projectId); + router.get('/', async (req, res) => { const limit = parseInt(req.query.limit, 10) || 25; const cursor = req.query.cursor; @@ -38,6 +41,13 @@ router.get('/', async (req, res) => { const search = req.query.search ? decodeURIComponent(req.query.search) : undefined; const sortBy = req.query.sortBy || 'updatedAt'; const sortDirection = req.query.sortDirection || 'desc'; + const projectId = Array.isArray(req.query.projectId) + ? req.query.projectId[0] + : req.query.projectId; + + if (!isValidProjectFilter(projectId)) { + return res.status(400).json({ error: 'projectId must be a valid project id or unassigned' }); + } let tags; if (req.query.tags) { @@ -53,6 +63,7 @@ router.get('/', async (req, res) => { search, sortBy, sortDirection, + projectId, }); res.status(200).json(result); } catch (error) { diff --git a/api/server/routes/index.js b/api/server/routes/index.js index 6694e47f23..3322dd57e3 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -12,6 +12,7 @@ const staticRoute = require('./static'); const messages = require('./messages'); const memories = require('./memories'); const presets = require('./presets'); +const projects = require('./projects'); const prompts = require('./prompts'); const skills = require('./skills'); const balance = require('./balance'); @@ -59,6 +60,7 @@ module.exports = { config, models, prompts, + projects, skills, actions, presets, diff --git a/api/server/routes/projects.js b/api/server/routes/projects.js new file mode 100644 index 0000000000..fd782a1ba5 --- /dev/null +++ b/api/server/routes/projects.js @@ -0,0 +1,25 @@ +const express = require('express'); +const { createProjectHandlers } = require('@librechat/api'); +const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); +const db = require('~/models'); + +const router = express.Router(); +const handlers = createProjectHandlers({ + listChatProjects: db.listChatProjects, + createChatProject: db.createChatProject, + getChatProject: db.getChatProject, + updateChatProject: db.updateChatProject, + deleteChatProject: db.deleteChatProject, + assignConversationToProject: db.assignConversationToProject, +}); + +router.use(requireJwtAuth); + +router.get('/', handlers.listProjects); +router.post('/', handlers.createProject); +router.put('/conversations/:conversationId', handlers.assignConversationToProject); +router.get('/:projectId', handlers.getProject); +router.patch('/:projectId', handlers.updateProject); +router.delete('/:projectId', handlers.deleteProject); + +module.exports = router; diff --git a/api/server/services/Endpoints/agents/build.js b/api/server/services/Endpoints/agents/build.js index 19ae3ab7e8..efd7130091 100644 --- a/api/server/services/Endpoints/agents/build.js +++ b/api/server/services/Endpoints/agents/build.js @@ -7,7 +7,7 @@ const db = require('~/models'); const loadAgent = (params) => loadAgentFn(params, { getAgent: db.getAgent, getMCPServerTools }); const buildOptions = (req, endpoint, parsedBody, endpointType) => { - const { spec, iconURL, agent_id, ...model_parameters } = parsedBody; + const { spec, iconURL, agent_id, chatProjectId, ...model_parameters } = parsedBody; const agentPromise = loadAgent({ req, spec, @@ -28,6 +28,7 @@ const buildOptions = (req, endpoint, parsedBody, endpointType) => { endpoint, agent_id, endpointType, + chatProjectId, model_parameters, agent: agentPromise, addedConvo, diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index bbbcd535ce..3108f1d72b 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -857,6 +857,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { agent: primaryConfig, spec: endpointOption.spec, iconURL: endpointOption.iconURL, + chatProjectId: endpointOption.chatProjectId, attachments: primaryConfig.requestAttachments ?? primaryConfig.attachments, agentContextAttachmentsByAgentId, endpointType: endpointOption.endpointType, diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index 27530a5c22..1a03ba8041 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -4,14 +4,21 @@ import { useForm } from 'react-hook-form'; import { Spinner } from '@librechat/client'; import { useParams } from 'react-router-dom'; import { Constants, buildTree } from 'librechat-data-provider'; -import type { TMessage } from 'librechat-data-provider'; +import type { TChatProject, TMessage } from 'librechat-data-provider'; import type { ChatFormValues } from '~/common'; import { ChatContext, AddedChatContext, ChatFormProvider, useFileMapContext } from '~/Providers'; -import { useAddedResponse, useResumeOnLoad, useAdaptiveSSE, useChatHelpers } from '~/hooks'; +import { + useAddedResponse, + useResumeOnLoad, + useAdaptiveSSE, + useChatHelpers, + useLocalize, +} from '~/hooks'; import ConversationStarters from './Input/ConversationStarters'; import { useGetMessagesByConvoId } from '~/data-provider'; import MessagesView from './Messages/MessagesView'; import Presentation from './Presentation'; +import ProjectLandingChip from './ProjectLandingChip'; import ChatForm from './Input/ChatForm'; import Landing from './Landing'; import Header from './Header'; @@ -29,8 +36,9 @@ function LoadingSpinner() { ); } -function ChatView({ index = 0 }: { index?: number }) { +function ChatView({ index = 0, project }: { index?: number; project?: TChatProject }) { const { conversationId } = useParams(); + const localize = useLocalize(); const rootSubmission = useRecoilValue(store.submissionByIndex(index)); const isSubmitting = useRecoilValue(store.isSubmittingFamily(index)); const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding); @@ -70,6 +78,7 @@ function ChatView({ index = 0 }: { index?: number }) { (!messagesTree || messagesTree.length === 0) && (conversationId === Constants.NEW_CONVO || !conversationId); const isNavigating = (!messagesTree || messagesTree.length === 0) && conversationId != null; + const isProjectLandingPage = isLandingPage && project != null; if (isLoading && conversationId !== Constants.NEW_CONVO) { content = ; @@ -81,6 +90,11 @@ function ChatView({ index = 0 }: { index?: number }) { content = ; } + const chatFormPlaceholder = + isProjectLandingPage && project + ? localize('com_ui_new_chat_in_project', { name: project.name }) + : undefined; + return ( @@ -104,7 +118,8 @@ function ChatView({ index = 0 }: { index?: number }) { isLandingPage && 'max-w-3xl transition-all duration-200 xl:max-w-4xl', )} > - + {isProjectLandingPage && project && } + {isLandingPage ? :