From aaa3ee2697b3d90cfe99848b0b8d17c973657e72 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Fri, 28 Mar 2025 20:01:39 +0000 Subject: [PATCH] Well the AI chat note type "kinda" works... --- src/public/app/components/app_context.ts | 1 + src/public/app/components/note_context.ts | 5 - .../app/components/root_command_executor.ts | 44 ++++++--- src/public/app/entities/fnote.ts | 5 +- .../{llm_chat_button.ts => ai_chat_button.ts} | 5 +- .../widgets/buttons/create_ai_chat_button.ts | 27 +++++ src/public/app/widgets/containers/launcher.ts | 6 +- .../widgets/floating_buttons/help_button.ts | 3 +- src/public/app/widgets/note_detail.ts | 9 +- src/public/app/widgets/note_type.ts | 1 + .../app/widgets/type_widgets/ai_chat.ts | 99 +++++++++++++++++++ .../app/widgets/type_widgets/llm_chat.ts | 51 ---------- src/public/translations/en/translation.json | 22 +++-- src/services/hidden_subtree.ts | 4 +- src/services/hidden_subtree_launcherbar.ts | 4 +- src/services/note_types.ts | 3 +- 16 files changed, 192 insertions(+), 97 deletions(-) rename src/public/app/widgets/buttons/{llm_chat_button.ts => ai_chat_button.ts} (86%) create mode 100644 src/public/app/widgets/buttons/create_ai_chat_button.ts create mode 100644 src/public/app/widgets/type_widgets/ai_chat.ts delete mode 100644 src/public/app/widgets/type_widgets/llm_chat.ts diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index 056481c7c..ca9dc4c9c 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -84,6 +84,7 @@ export type CommandMappings = { showLaunchBarSubtree: CommandData; showRevisions: CommandData; showLlmChat: CommandData; + createAiChat: CommandData; showOptions: CommandData & { section: string; }; diff --git a/src/public/app/components/note_context.ts b/src/public/app/components/note_context.ts index 5799514d1..6c93fcc5b 100644 --- a/src/public/app/components/note_context.ts +++ b/src/public/app/components/note_context.ts @@ -369,11 +369,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> const { note, viewScope } = this; - // For llmChat viewMode, show a custom title - if (viewScope?.viewMode === "llmChat") { - return "Chat with Notes"; - } - const isNormalView = (viewScope?.viewMode === "default" || viewScope?.viewMode === "contextual-help"); let title = (isNormalView ? note.title : `${note.title}: ${viewScope?.viewMode}`); diff --git a/src/public/app/components/root_command_executor.ts b/src/public/app/components/root_command_executor.ts index 96e917d81..b4e6ed3c0 100644 --- a/src/public/app/components/root_command_executor.ts +++ b/src/public/app/components/root_command_executor.ts @@ -9,10 +9,9 @@ import froca from "../services/froca.js"; import utils from "../services/utils.js"; import LlmChatPanel from "../widgets/llm_chat_panel.js"; import toastService from "../services/toast.js"; +import noteCreateService from "../services/note_create.js"; export default class RootCommandExecutor extends Component { - private llmChatPanel: any = null; - editReadOnlyNoteCommand() { const noteContext = appContext.tabManager.getActiveContext(); if (noteContext?.viewScope) { @@ -231,22 +230,39 @@ export default class RootCommandExecutor extends Component { } } - async showLlmChatCommand() { - console.log("showLlmChatCommand triggered"); - toastService.showMessage("Opening LLM Chat..."); - + async createAiChatCommand() { try { - // We'll use the Note Map approach - open a known note ID that corresponds to the LLM chat panel - await appContext.tabManager.openTabWithNoteWithHoisting("_globalNoteMap", { - activate: true, - viewScope: { - viewMode: "llmChat" // We'll need to handle this custom view mode elsewhere - } + // Create a new AI Chat note + const parentNoteId = appContext.tabManager.getActiveContextNotePath(); + + if (!parentNoteId) { + toastService.showError("No active note to create AI Chat under"); + return; + } + + const result = await noteCreateService.createNote(parentNoteId, { + title: "New AI Chat", + type: "aiChat", + content: JSON.stringify({ + messages: [], + title: "New AI Chat" + }) }); + + if (!result.note) { + toastService.showError("Failed to create AI Chat note"); + return; + } + + await appContext.tabManager.openTabWithNoteWithHoisting(result.note.noteId, { + activate: true + }); + + toastService.showMessage("Created new AI Chat note"); } catch (e) { - console.error("Error opening LLM Chat:", e); - toastService.showError("Failed to open LLM Chat: " + (e as Error).message); + console.error("Error creating AI Chat note:", e); + toastService.showError("Failed to create AI Chat note: " + (e as Error).message); } } } diff --git a/src/public/app/entities/fnote.ts b/src/public/app/entities/fnote.ts index e2a13852e..3959e2d0f 100644 --- a/src/public/app/entities/fnote.ts +++ b/src/public/app/entities/fnote.ts @@ -28,7 +28,8 @@ const NOTE_TYPE_ICONS = { doc: "bx bxs-file-doc", contentWidget: "bx bxs-widget", mindMap: "bx bx-sitemap", - geoMap: "bx bx-map-alt" + geoMap: "bx bx-map-alt", + aiChat: "bx bx-bot" }; /** @@ -36,7 +37,7 @@ const NOTE_TYPE_ICONS = { * end user. Those types should be used only for checking against, they are * not for direct use. */ -export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap"; +export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "aiChat"; export interface NotePathRecord { isArchived: boolean; diff --git a/src/public/app/widgets/buttons/llm_chat_button.ts b/src/public/app/widgets/buttons/ai_chat_button.ts similarity index 86% rename from src/public/app/widgets/buttons/llm_chat_button.ts rename to src/public/app/widgets/buttons/ai_chat_button.ts index 94afab977..5ad3f8033 100644 --- a/src/public/app/widgets/buttons/llm_chat_button.ts +++ b/src/public/app/widgets/buttons/ai_chat_button.ts @@ -3,12 +3,12 @@ import type FNote from "../../entities/fnote.js"; import options from "../../services/options.js"; import CommandButtonWidget from "./command_button.js"; -export default class LlmChatButton extends CommandButtonWidget { +export default class AiChatButton extends CommandButtonWidget { constructor(note: FNote) { super(); - this.command("showLlmChat") + this.command("createAiChat") .title(() => note.title) .icon(() => note.getIcon()) .class("launcher-button"); @@ -23,5 +23,4 @@ export default class LlmChatButton extends CommandButtonWidget { this.refresh(); } } - } diff --git a/src/public/app/widgets/buttons/create_ai_chat_button.ts b/src/public/app/widgets/buttons/create_ai_chat_button.ts new file mode 100644 index 000000000..1ccd52cda --- /dev/null +++ b/src/public/app/widgets/buttons/create_ai_chat_button.ts @@ -0,0 +1,27 @@ +import { t } from "../../services/i18n.js"; +import options from "../../services/options.js"; +import CommandButtonWidget from "./command_button.js"; + +export default class CreateAiChatButton extends CommandButtonWidget { + constructor() { + super(); + + this.icon("bx bx-bot") + .title(t("ai.create_new_ai_chat")) + .titlePlacement("bottom") + .command("createAiChat") + .class("icon-action"); + } + + isEnabled() { + return options.get("aiEnabled") === "true"; + } + + async refreshWithNote() { + if (this.isEnabled()) { + this.$widget.show(); + } else { + this.$widget.hide(); + } + } +} diff --git a/src/public/app/widgets/containers/launcher.ts b/src/public/app/widgets/containers/launcher.ts index 737496923..e1bfc5a8b 100644 --- a/src/public/app/widgets/containers/launcher.ts +++ b/src/public/app/widgets/containers/launcher.ts @@ -13,7 +13,7 @@ import HistoryNavigationButton from "../buttons/history_navigation.js"; import QuickSearchLauncherWidget from "../quick_search_launcher.js"; import type FNote from "../../entities/fnote.js"; import type { CommandNames } from "../../components/app_context.js"; -import LlmChatButton from "../buttons/llm_chat_button.js"; +import AiChatButton from "../buttons/ai_chat_button.js"; interface InnerWidget extends BasicWidget { settings?: { @@ -124,8 +124,8 @@ export default class LauncherWidget extends BasicWidget { return new TodayLauncher(note); case "quickSearch": return new QuickSearchLauncherWidget(this.isHorizontalLayout); - case "llmChatLauncher": - return new LlmChatButton(note); + case "aiChatLauncher": + return new AiChatButton(note); default: throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); } diff --git a/src/public/app/widgets/floating_buttons/help_button.ts b/src/public/app/widgets/floating_buttons/help_button.ts index b14bfb7b7..088f95fd1 100644 --- a/src/public/app/widgets/floating_buttons/help_button.ts +++ b/src/public/app/widgets/floating_buttons/help_button.ts @@ -28,7 +28,8 @@ export const byNoteType: Record, string | null> = { render: null, search: null, text: null, - webView: null + webView: null, + aiChat: null }; export const byBookType: Record = { diff --git a/src/public/app/widgets/note_detail.ts b/src/public/app/widgets/note_detail.ts index 2cd662efc..eaa87b62d 100644 --- a/src/public/app/widgets/note_detail.ts +++ b/src/public/app/widgets/note_detail.ts @@ -35,8 +35,8 @@ import GeoMapTypeWidget from "./type_widgets/geo_map.js"; import utils from "../services/utils.js"; import type { NoteType } from "../entities/fnote.js"; import type TypeWidget from "./type_widgets/type_widget.js"; -import LlmChatTypeWidget from "./type_widgets/llm_chat.js"; import { MermaidTypeWidget } from "./type_widgets/mermaid.js"; +import AiChatTypeWidget from "./type_widgets/ai_chat.js"; const TPL = `
@@ -75,7 +75,7 @@ const typeWidgetClasses = { attachmentList: AttachmentListTypeWidget, mindMap: MindMapWidget, geoMap: GeoMapTypeWidget, - llmChat: LlmChatTypeWidget, + aiChat: AiChatTypeWidget, // Split type editors mermaid: MermaidTypeWidget @@ -95,7 +95,7 @@ type ExtendedNoteType = | "attachmentDetail" | "attachmentList" | "protectedSession" - | "llmChat"; + | "aiChat"; export default class NoteDetailWidget extends NoteContextAwareWidget { @@ -228,9 +228,6 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { if (viewScope?.viewMode === "source") { resultingType = "readOnlyCode"; - } else if (viewScope?.viewMode === "llmChat") { - // Special handling for our LLM Chat view mode - resultingType = "llmChat"; // This will need to be added to the ExtendedNoteType } else if (viewScope && viewScope.viewMode === "attachments") { resultingType = viewScope.attachmentId ? "attachmentDetail" : "attachmentList"; } else if (type === "text" && (await this.noteContext?.isReadOnly())) { diff --git a/src/public/app/widgets/note_type.ts b/src/public/app/widgets/note_type.ts index aa9b7628d..a008a0c61 100644 --- a/src/public/app/widgets/note_type.ts +++ b/src/public/app/widgets/note_type.ts @@ -38,6 +38,7 @@ const NOTE_TYPES: NoteTypeMapping[] = [ // Misc note types { type: "render", mime: "", title: t("note_types.render-note"), selectable: true }, { type: "webView", mime: "", title: t("note_types.web-view"), selectable: true }, + { type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: true }, // Code notes { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true }, diff --git a/src/public/app/widgets/type_widgets/ai_chat.ts b/src/public/app/widgets/type_widgets/ai_chat.ts new file mode 100644 index 000000000..6386fb898 --- /dev/null +++ b/src/public/app/widgets/type_widgets/ai_chat.ts @@ -0,0 +1,99 @@ +import TypeWidget from "./type_widget.js"; +import LlmChatPanel from "../llm_chat_panel.js"; +import { type EventData } from "../../components/app_context.js"; +import type FNote from "../../entities/fnote.js"; +import server from "../../services/server.js"; + +export default class AiChatTypeWidget extends TypeWidget { + private llmChatPanel: LlmChatPanel; + private isInitialized: boolean = false; + + constructor() { + super(); + this.llmChatPanel = new LlmChatPanel(); + } + + static getType() { + return "aiChat"; + } + + doRender() { + this.$widget = $('
'); + this.$widget.append(this.llmChatPanel.render()); + + return this.$widget; + } + + async doRefresh(note: FNote | null | undefined) { + // Initialize the chat panel if not already done + if (!this.isInitialized) { + console.log("Initializing AI Chat Panel for note:", note?.noteId); + await this.llmChatPanel.refresh(); + this.isInitialized = true; + } + + // If this is a newly created note, we can initialize the content + if (note) { + try { + const content = await note.getContent(); + // Check if content is empty + if (!content || content === '{}') { + // Initialize with empty chat history + await this.saveData({ + messages: [], + title: note.title + }); + } + } catch (e) { + console.error("Error initializing AI Chat note content:", e); + } + } + } + + async entitiesReloadedEvent(data: EventData<"entitiesReloaded">) { + // We don't need to refresh on entities reloaded for the chat + } + + async activeContextChangedEvent(data: EventData<"activeContextChanged">) { + // Only refresh when this becomes active and we're not initialized yet + if (this.isActive() && !this.isInitialized) { + await this.llmChatPanel.refresh(); + this.isInitialized = true; + } + } + + // Save chat data to the note + async saveData(data: any) { + if (!this.note) { + return; + } + + try { + await server.put(`notes/${this.note.noteId}/content`, { + content: JSON.stringify(data, null, 2) + }); + } catch (e) { + console.error("Error saving AI Chat data:", e); + } + } + + // Get data from the note + async getData() { + if (!this.note) { + return null; + } + + try { + const content = await this.note.getContent(); + + if (!content) { + return null; + } + + return JSON.parse(content as string); + } catch (e) { + console.error("Error loading AI Chat data:", e); + return null; + } + } +} diff --git a/src/public/app/widgets/type_widgets/llm_chat.ts b/src/public/app/widgets/type_widgets/llm_chat.ts deleted file mode 100644 index 7d4b41faf..000000000 --- a/src/public/app/widgets/type_widgets/llm_chat.ts +++ /dev/null @@ -1,51 +0,0 @@ -import TypeWidget from "./type_widget.js"; -import LlmChatPanel from "../llm_chat_panel.js"; -import { type EventData } from "../../components/app_context.js"; -import type FNote from "../../entities/fnote.js"; - -export default class LlmChatTypeWidget extends TypeWidget { - private llmChatPanel: LlmChatPanel; - private isInitialized: boolean = false; - - constructor() { - super(); - this.llmChatPanel = new LlmChatPanel(); - } - - static getType() { - return "llmChat"; - } - - doRender() { - this.$widget = $('
'); - this.$widget.append(this.llmChatPanel.render()); - - return this.$widget; - } - - async doRefresh(note: FNote | null | undefined) { - // Initialize only once - if (!this.isInitialized) { - console.log("Initializing LLM Chat Panel"); - await this.llmChatPanel.refresh(); - this.isInitialized = true; - } - } - - async entitiesReloadedEvent(data: EventData<"entitiesReloaded">) { - // We don't need to refresh on entities reloaded for the chat - } - - async activeContextChangedEvent(data: EventData<"activeContextChanged">) { - // Only refresh when this becomes active and we're not initialized yet - if (this.isActive() && !this.isInitialized) { - await this.llmChatPanel.refresh(); - this.isInitialized = true; - } - } - - // Handle data saving - we don't need to save anything - getData() { - return {}; - } -} diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index a7cd6d721..5317a733e 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1550,7 +1550,8 @@ "widget": "Widget", "confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?", "geo-map": "Geo Map", - "beta-feature": "Beta" + "beta-feature": "Beta", + "ai-chat": "AI Chat" }, "protect_note": { "toggle-on": "Protect the note", @@ -1826,7 +1827,12 @@ "confirm_delete_embeddings": "Are you sure you want to delete all AI embeddings? This will remove all semantic search capabilities until notes are reindexed, which can take a significant amount of time.", "empty_key_warning": "Warning: Empty API key. You need to configure your API key in settings.", "enable_ai": "Enable AI Features", - "enhanced_context_description": "Uses semantic search to find relevant information across your notes", + "name": "AI", + "openai": "OpenAI", + "use_enhanced_context": "Use enhanced context", + "enhanced_context_description": "Provides the AI with more context from the note and its related notes for better responses", + "show_thinking": "Show thinking", + "show_thinking_description": "Show the AI's chain of thought process", "enter_message": "Enter your message...", "error_contacting_provider": "Error contacting AI provider. Please check your settings and internet connection.", "error_generating_response": "Error generating AI response", @@ -1842,15 +1848,17 @@ "notes_indexed": "{{ count }} note indexed", "notes_indexed_plural": "{{ count }} notes indexed", "reset_embeddings": "Reset Embeddings", - "show_thinking": "Show Thinking", - "show_thinking_description": "Reveals the reasoning process used to generate responses", "sources": "Sources", "start_indexing": "Start Indexing", "use_advanced_context": "Use Advanced Context", - "use_enhanced_context": "Use Enhanced Note Context", "processing": { - "common": "Processing..." - } + "common": "Processing...", + "thinking": "Thinking...", + "loading": "Loading...", + "generating": "Generating..." + }, + "create_new_ai_chat": "Create new AI Chat" + }, "switch_layout_button": { "title_vertical": "Move editing pane to the bottom", diff --git a/src/services/hidden_subtree.ts b/src/services/hidden_subtree.ts index 4892a36ce..9e991169a 100644 --- a/src/services/hidden_subtree.ts +++ b/src/services/hidden_subtree.ts @@ -53,7 +53,7 @@ export interface HiddenSubtreeItem { | "protectedSession" | "calendar" | "quickSearch" - | "llmChatLauncher"; + | "aiChatLauncher"; command?: keyof typeof Command; } @@ -64,7 +64,7 @@ enum Command { createNoteIntoInbox, showRecentChanges, showOptions, - showLlmChat + createAiChat } /* diff --git a/src/services/hidden_subtree_launcherbar.ts b/src/services/hidden_subtree_launcherbar.ts index a63cc86bf..061b7207d 100644 --- a/src/services/hidden_subtree_launcherbar.ts +++ b/src/services/hidden_subtree_launcherbar.ts @@ -70,10 +70,10 @@ export default function buildLaunchBarConfig() { { id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" }, { id: "_lbLlmChat", - title: t("hidden-subtree.llm-chat-title"), + title: t("hidden-subtree.ai-chat-title"), type: "launcher", icon: "bx bx-bot", - builtinWidget: "llmChatLauncher", + builtinWidget: "aiChatLauncher", attributes: [ { type: "label", name: "desktopOnly" } ] diff --git a/src/services/note_types.ts b/src/services/note_types.ts index a242fc7b2..3b2dc8d66 100644 --- a/src/services/note_types.ts +++ b/src/services/note_types.ts @@ -15,7 +15,8 @@ const noteTypes = [ { type: "doc", defaultMime: "" }, { type: "contentWidget", defaultMime: "" }, { type: "mindMap", defaultMime: "application/json" }, - { type: "geoMap", defaultMime: "application/json" } + { type: "geoMap", defaultMime: "application/json" }, + { type: "aiChat", defaultMime: "application/json" } ]; function getDefaultMimeForNoteType(typeName: string) {