diff --git a/server/src/routes/chat.ts b/server/src/routes/chat.ts index 12cdf114..f4d38517 100644 --- a/server/src/routes/chat.ts +++ b/server/src/routes/chat.ts @@ -3,11 +3,14 @@ import type { Db } from "@paperclipai/db"; import { assertBoard, assertCompanyAccess } from "./authz.js"; import { chatService } from "../services/chat.js"; import { issueService } from "../services/issues.js"; +import { z } from "zod"; import { createConversationSchema, updateConversationSchema, createMessageSchema, handoffSchema, + searchMessagesSchema, + branchConversationSchema, } from "@paperclipai/shared"; export function chatRoutes(db: Db): Router { @@ -207,5 +210,70 @@ export function chatRoutes(db: Db): Router { res.status(201).json(message); }); + // GET /api/companies/:companyId/messages/search + router.get("/companies/:companyId/messages/search", async (req, res) => { + assertBoard(req); + assertCompanyAccess(req, req.params.companyId!); + let parsed: { q: string; limit?: number }; + try { + parsed = searchMessagesSchema.parse({ q: req.query.q, limit: req.query.limit }); + } catch { + res.status(400).json({ error: "Query must be at least 2 characters" }); + return; + } + const result = await svc.searchMessages(req.params.companyId!, parsed.q, { limit: parsed.limit }); + res.json(result); + }); + + // POST /api/conversations/:id/bookmarks -- Toggle bookmark for a message + router.post("/conversations/:id/bookmarks", async (req, res) => { + assertBoard(req); + const messageId = z.string().uuid().safeParse(req.body.messageId); + if (!messageId.success) { + res.status(400).json({ error: "messageId must be a valid UUID" }); + return; + } + const conversationId = req.params.id!; + const conversation = await svc.getConversation(conversationId); + const result = await svc.toggleBookmark(conversation.companyId, messageId.data, conversationId); + res.json(result); + }); + + // GET /api/companies/:companyId/bookmarks + router.get("/companies/:companyId/bookmarks", async (req, res) => { + assertBoard(req); + assertCompanyAccess(req, req.params.companyId!); + const conversationId = req.query.conversationId as string | undefined; + const result = await svc.getBookmarks(req.params.companyId!, { conversationId }); + res.json(result); + }); + + // POST /api/conversations/:id/branch -- Branch conversation from a message + router.post("/conversations/:id/branch", async (req, res) => { + assertBoard(req); + const parsed = branchConversationSchema.parse(req.body); + const conv = await svc.getConversation(req.params.id!); + const newConversation = await svc.branchConversation(req.params.id!, parsed.branchFromMessageId, conv.companyId); + res.status(201).json(newConversation); + }); + + // GET /api/conversations/:id/branches -- List child conversations + router.get("/conversations/:id/branches", async (req, res) => { + assertBoard(req); + const branches = await svc.listBranches(req.params.id!); + res.json({ items: branches }); + }); + + // GET /api/conversations/:id/export -- Export conversation as Markdown or JSON file + router.get("/conversations/:id/export", async (req, res) => { + assertBoard(req); + const format = req.query.format === "json" ? "json" : "markdown"; + const { content, filename } = await svc.exportConversation(req.params.id!, format); + const mime = format === "json" ? "application/json" : "text/markdown"; + res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); + res.setHeader("Content-Type", mime); + res.send(content); + }); + return router; }