diff --git a/apps/client/src/services/date_notes.ts b/apps/client/src/services/date_notes.ts index 21709b1bb0..e96c92f28c 100644 --- a/apps/client/src/services/date_notes.ts +++ b/apps/client/src/services/date_notes.ts @@ -84,6 +84,14 @@ async function createSearchNote(opts = {}) { return await froca.getNote(note.noteId); } +async function createLlmChat() { + const note = await server.post("special-notes/llm-chat"); + + await ws.waitForMaxKnownEntityChangeId(); + + return await froca.getNote(note.noteId); +} + export default { getInboxNote, getTodayNote, @@ -94,5 +102,6 @@ export default { getMonthNote, getYearNote, createSqlConsole, - createSearchNote + createSearchNote, + createLlmChat }; diff --git a/apps/server/src/assets/translations/en/server.json b/apps/server/src/assets/translations/en/server.json index 56aa2697bf..b3a05c3732 100644 --- a/apps/server/src/assets/translations/en/server.json +++ b/apps/server/src/assets/translations/en/server.json @@ -297,7 +297,8 @@ }, "quarterNumber": "Quarter {quarterNumber}", "special_notes": { - "search_prefix": "Search:" + "search_prefix": "Search:", + "llm_chat_prefix": "Chat:" }, "test_sync": { "not-configured": "Sync server host is not configured. Please configure sync first.", @@ -308,6 +309,7 @@ "search-history-title": "Search History", "note-map-title": "Note Map", "sql-console-history-title": "SQL Console History", + "llm-chat-history-title": "AI Chat History", "shared-notes-title": "Shared Notes", "bulk-action-title": "Bulk Action", "backend-log-title": "Backend Log", diff --git a/apps/server/src/routes/api/special_notes.ts b/apps/server/src/routes/api/special_notes.ts index 5169260709..5a52dde685 100644 --- a/apps/server/src/routes/api/special_notes.ts +++ b/apps/server/src/routes/api/special_notes.ts @@ -86,6 +86,14 @@ function createSearchNote(req: Request) { return specialNotesService.createSearchNote(searchString, ancestorNoteId); } +function createLlmChat() { + return specialNotesService.createLlmChat(); +} + +function saveLlmChat(req: Request) { + return specialNotesService.saveLlmChat(req.body.llmChatNoteId); +} + function getHoistedNote() { return becca.getNote(cls.getHoistedNoteId()); } @@ -119,6 +127,8 @@ export default { saveSqlConsole, createSearchNote, saveSearchNote, + createLlmChat, + saveLlmChat, createLauncher, resetLauncher, createOrUpdateScriptLauncherFromApi diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index 5584184714..56c5a38c5e 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -292,6 +292,8 @@ function register(app: express.Application) { asyncApiRoute(PST, "/api/special-notes/save-sql-console", specialNotesRoute.saveSqlConsole); apiRoute(PST, "/api/special-notes/search-note", specialNotesRoute.createSearchNote); apiRoute(PST, "/api/special-notes/save-search-note", specialNotesRoute.saveSearchNote); + apiRoute(PST, "/api/special-notes/llm-chat", specialNotesRoute.createLlmChat); + apiRoute(PST, "/api/special-notes/save-llm-chat", specialNotesRoute.saveLlmChat); apiRoute(PST, "/api/special-notes/launchers/:noteId/reset", specialNotesRoute.resetLauncher); apiRoute(PST, "/api/special-notes/launchers/:parentNoteId/:launcherType", specialNotesRoute.createLauncher); apiRoute(PUT, "/api/special-notes/api-script-launcher", specialNotesRoute.createOrUpdateScriptLauncherFromApi); diff --git a/apps/server/src/services/hidden_subtree.ts b/apps/server/src/services/hidden_subtree.ts index 37863b3282..3908c94d10 100644 --- a/apps/server/src/services/hidden_subtree.ts +++ b/apps/server/src/services/hidden_subtree.ts @@ -66,6 +66,12 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS type: "doc", icon: "bx-data" }, + { + id: "_llmChat", + title: t("hidden-subtree.llm-chat-history-title"), + type: "doc", + icon: "bx-message-square-dots" + }, { id: "_share", title: t("hidden-subtree.shared-notes-title"), diff --git a/apps/server/src/services/special_notes.ts b/apps/server/src/services/special_notes.ts index cf71311e6d..165e710cf5 100644 --- a/apps/server/src/services/special_notes.ts +++ b/apps/server/src/services/special_notes.ts @@ -10,7 +10,7 @@ import SearchContext from "./search/search_context.js"; import { LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT } from "./hidden_subtree.js"; import { t } from "i18next"; import BNote from '../becca/entities/bnote.js'; -import { SaveSearchNoteResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; +import { SaveSearchNoteResponse, SaveSqlConsoleResponse, SaveLlmChatResponse } from "@triliumnext/commons"; function getInboxNote(date: string) { const workspaceNote = hoistedNoteService.getWorkspaceNote(); @@ -123,6 +123,58 @@ function saveSearchNote(searchNoteId: string) { return result satisfies SaveSearchNoteResponse; } +function createLlmChat() { + const { note } = noteService.createNewNote({ + parentNoteId: getMonthlyParentNoteId("_llmChat", "llmChat"), + title: `${t("special_notes.llm_chat_prefix")} ${dateUtils.localNowDateTime()}`, + content: JSON.stringify({ + version: 1, + messages: [] + }), + type: "llmChat", + mime: "application/json" + }); + + note.setLabel("iconClass", "bx bx-message-square-dots"); + note.setLabel("keepCurrentHoisting"); + + return note; +} + +function getLlmChatHome() { + const workspaceNote = hoistedNoteService.getWorkspaceNote(); + if (!workspaceNote) { + throw new Error("Unable to find workspace note"); + } + + if (!workspaceNote.isRoot()) { + return workspaceNote.searchNoteInSubtree("#workspaceLlmChatHome") || workspaceNote.searchNoteInSubtree("#llmChatHome") || workspaceNote; + } else { + const today = dateUtils.localNowDate(); + + return workspaceNote.searchNoteInSubtree("#llmChatHome") || dateNoteService.getDayNote(today); + } +} + +function saveLlmChat(llmChatNoteId: string) { + const llmChatNote = becca.getNote(llmChatNoteId); + if (!llmChatNote) { + throw new Error(`Unable to find LLM chat note ID: ${llmChatNoteId}`); + } + + const llmChatHome = getLlmChatHome(); + + const result = llmChatNote.cloneTo(llmChatHome.noteId); + + for (const parentBranch of llmChatNote.getParentBranches()) { + if (parentBranch.parentNote?.hasAncestor("_hidden")) { + parentBranch.markAsDeleted(); + } + } + + return result satisfies SaveLlmChatResponse; +} + function getMonthlyParentNoteId(rootNoteId: string, prefix: string) { const month = dateUtils.localNowDate().substring(0, 7); const labelName = `${prefix}MonthNote`; @@ -282,6 +334,8 @@ export default { saveSqlConsole, createSearchNote, saveSearchNote, + createLlmChat, + saveLlmChat, createLauncher, resetLauncher, createOrUpdateScriptLauncherFromApi diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index a15192fd28..3edf99ec64 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -214,6 +214,8 @@ export interface ConvertAttachmentToNoteResponse { export type SaveSqlConsoleResponse = CloneResponse; +export type SaveLlmChatResponse = CloneResponse; + export interface BacklinkCountResponse { count: number; }