Well the AI chat note type "kinda" works...

This commit is contained in:
perf3ct 2025-03-28 20:01:39 +00:00
parent 5456ac32ef
commit aaa3ee2697
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
16 changed files with 192 additions and 97 deletions

View File

@ -84,6 +84,7 @@ export type CommandMappings = {
showLaunchBarSubtree: CommandData; showLaunchBarSubtree: CommandData;
showRevisions: CommandData; showRevisions: CommandData;
showLlmChat: CommandData; showLlmChat: CommandData;
createAiChat: CommandData;
showOptions: CommandData & { showOptions: CommandData & {
section: string; section: string;
}; };

View File

@ -369,11 +369,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
const { note, viewScope } = this; 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"); const isNormalView = (viewScope?.viewMode === "default" || viewScope?.viewMode === "contextual-help");
let title = (isNormalView ? note.title : `${note.title}: ${viewScope?.viewMode}`); let title = (isNormalView ? note.title : `${note.title}: ${viewScope?.viewMode}`);

View File

@ -9,10 +9,9 @@ import froca from "../services/froca.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import LlmChatPanel from "../widgets/llm_chat_panel.js"; import LlmChatPanel from "../widgets/llm_chat_panel.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import noteCreateService from "../services/note_create.js";
export default class RootCommandExecutor extends Component { export default class RootCommandExecutor extends Component {
private llmChatPanel: any = null;
editReadOnlyNoteCommand() { editReadOnlyNoteCommand() {
const noteContext = appContext.tabManager.getActiveContext(); const noteContext = appContext.tabManager.getActiveContext();
if (noteContext?.viewScope) { if (noteContext?.viewScope) {
@ -231,22 +230,39 @@ export default class RootCommandExecutor extends Component {
} }
} }
async showLlmChatCommand() { async createAiChatCommand() {
console.log("showLlmChatCommand triggered");
toastService.showMessage("Opening LLM Chat...");
try { try {
// We'll use the Note Map approach - open a known note ID that corresponds to the LLM chat panel // Create a new AI Chat note
await appContext.tabManager.openTabWithNoteWithHoisting("_globalNoteMap", { const parentNoteId = appContext.tabManager.getActiveContextNotePath();
activate: true,
viewScope: { if (!parentNoteId) {
viewMode: "llmChat" // We'll need to handle this custom view mode elsewhere 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) { catch (e) {
console.error("Error opening LLM Chat:", e); console.error("Error creating AI Chat note:", e);
toastService.showError("Failed to open LLM Chat: " + (e as Error).message); toastService.showError("Failed to create AI Chat note: " + (e as Error).message);
} }
} }
} }

View File

@ -28,7 +28,8 @@ const NOTE_TYPE_ICONS = {
doc: "bx bxs-file-doc", doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget", contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap", 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 * end user. Those types should be used only for checking against, they are
* not for direct use. * 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 { export interface NotePathRecord {
isArchived: boolean; isArchived: boolean;

View File

@ -3,12 +3,12 @@ import type FNote from "../../entities/fnote.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import CommandButtonWidget from "./command_button.js"; import CommandButtonWidget from "./command_button.js";
export default class LlmChatButton extends CommandButtonWidget { export default class AiChatButton extends CommandButtonWidget {
constructor(note: FNote) { constructor(note: FNote) {
super(); super();
this.command("showLlmChat") this.command("createAiChat")
.title(() => note.title) .title(() => note.title)
.icon(() => note.getIcon()) .icon(() => note.getIcon())
.class("launcher-button"); .class("launcher-button");
@ -23,5 +23,4 @@ export default class LlmChatButton extends CommandButtonWidget {
this.refresh(); this.refresh();
} }
} }
} }

View File

@ -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();
}
}
}

View File

@ -13,7 +13,7 @@ import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js"; import QuickSearchLauncherWidget from "../quick_search_launcher.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { CommandNames } from "../../components/app_context.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 { interface InnerWidget extends BasicWidget {
settings?: { settings?: {
@ -124,8 +124,8 @@ export default class LauncherWidget extends BasicWidget {
return new TodayLauncher(note); return new TodayLauncher(note);
case "quickSearch": case "quickSearch":
return new QuickSearchLauncherWidget(this.isHorizontalLayout); return new QuickSearchLauncherWidget(this.isHorizontalLayout);
case "llmChatLauncher": case "aiChatLauncher":
return new LlmChatButton(note); return new AiChatButton(note);
default: default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
} }

View File

@ -28,7 +28,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
render: null, render: null,
search: null, search: null,
text: null, text: null,
webView: null webView: null,
aiChat: null
}; };
export const byBookType: Record<ViewTypeOptions, string | null> = { export const byBookType: Record<ViewTypeOptions, string | null> = {

View File

@ -35,8 +35,8 @@ import GeoMapTypeWidget from "./type_widgets/geo_map.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js"; import type { NoteType } from "../entities/fnote.js";
import type TypeWidget from "./type_widgets/type_widget.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 { MermaidTypeWidget } from "./type_widgets/mermaid.js";
import AiChatTypeWidget from "./type_widgets/ai_chat.js";
const TPL = ` const TPL = `
<div class="note-detail"> <div class="note-detail">
@ -75,7 +75,7 @@ const typeWidgetClasses = {
attachmentList: AttachmentListTypeWidget, attachmentList: AttachmentListTypeWidget,
mindMap: MindMapWidget, mindMap: MindMapWidget,
geoMap: GeoMapTypeWidget, geoMap: GeoMapTypeWidget,
llmChat: LlmChatTypeWidget, aiChat: AiChatTypeWidget,
// Split type editors // Split type editors
mermaid: MermaidTypeWidget mermaid: MermaidTypeWidget
@ -95,7 +95,7 @@ type ExtendedNoteType =
| "attachmentDetail" | "attachmentDetail"
| "attachmentList" | "attachmentList"
| "protectedSession" | "protectedSession"
| "llmChat"; | "aiChat";
export default class NoteDetailWidget extends NoteContextAwareWidget { export default class NoteDetailWidget extends NoteContextAwareWidget {
@ -228,9 +228,6 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
if (viewScope?.viewMode === "source") { if (viewScope?.viewMode === "source") {
resultingType = "readOnlyCode"; 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") { } else if (viewScope && viewScope.viewMode === "attachments") {
resultingType = viewScope.attachmentId ? "attachmentDetail" : "attachmentList"; resultingType = viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (type === "text" && (await this.noteContext?.isReadOnly())) { } else if (type === "text" && (await this.noteContext?.isReadOnly())) {

View File

@ -38,6 +38,7 @@ const NOTE_TYPES: NoteTypeMapping[] = [
// Misc note types // Misc note types
{ type: "render", mime: "", title: t("note_types.render-note"), selectable: true }, { type: "render", mime: "", title: t("note_types.render-note"), selectable: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), 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 // Code notes
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true }, { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true },

View File

@ -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 = $('<div class="ai-chat-widget-container" style="height: 100%;"></div>');
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;
}
}
}

View File

@ -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 = $('<div class="llm-chat-widget-container" style="height: 100%;"></div>');
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 {};
}
}

View File

@ -1550,7 +1550,8 @@
"widget": "Widget", "widget": "Widget",
"confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?", "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", "geo-map": "Geo Map",
"beta-feature": "Beta" "beta-feature": "Beta",
"ai-chat": "AI Chat"
}, },
"protect_note": { "protect_note": {
"toggle-on": "Protect the 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.", "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.", "empty_key_warning": "Warning: Empty API key. You need to configure your API key in settings.",
"enable_ai": "Enable AI Features", "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...", "enter_message": "Enter your message...",
"error_contacting_provider": "Error contacting AI provider. Please check your settings and internet connection.", "error_contacting_provider": "Error contacting AI provider. Please check your settings and internet connection.",
"error_generating_response": "Error generating AI response", "error_generating_response": "Error generating AI response",
@ -1842,15 +1848,17 @@
"notes_indexed": "{{ count }} note indexed", "notes_indexed": "{{ count }} note indexed",
"notes_indexed_plural": "{{ count }} notes indexed", "notes_indexed_plural": "{{ count }} notes indexed",
"reset_embeddings": "Reset Embeddings", "reset_embeddings": "Reset Embeddings",
"show_thinking": "Show Thinking",
"show_thinking_description": "Reveals the reasoning process used to generate responses",
"sources": "Sources", "sources": "Sources",
"start_indexing": "Start Indexing", "start_indexing": "Start Indexing",
"use_advanced_context": "Use Advanced Context", "use_advanced_context": "Use Advanced Context",
"use_enhanced_context": "Use Enhanced Note Context",
"processing": { "processing": {
"common": "Processing..." "common": "Processing...",
} "thinking": "Thinking...",
"loading": "Loading...",
"generating": "Generating..."
},
"create_new_ai_chat": "Create new AI Chat"
}, },
"switch_layout_button": { "switch_layout_button": {
"title_vertical": "Move editing pane to the bottom", "title_vertical": "Move editing pane to the bottom",

View File

@ -53,7 +53,7 @@ export interface HiddenSubtreeItem {
| "protectedSession" | "protectedSession"
| "calendar" | "calendar"
| "quickSearch" | "quickSearch"
| "llmChatLauncher"; | "aiChatLauncher";
command?: keyof typeof Command; command?: keyof typeof Command;
} }
@ -64,7 +64,7 @@ enum Command {
createNoteIntoInbox, createNoteIntoInbox,
showRecentChanges, showRecentChanges,
showOptions, showOptions,
showLlmChat createAiChat
} }
/* /*

View File

@ -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: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
{ {
id: "_lbLlmChat", id: "_lbLlmChat",
title: t("hidden-subtree.llm-chat-title"), title: t("hidden-subtree.ai-chat-title"),
type: "launcher", type: "launcher",
icon: "bx bx-bot", icon: "bx bx-bot",
builtinWidget: "llmChatLauncher", builtinWidget: "aiChatLauncher",
attributes: [ attributes: [
{ type: "label", name: "desktopOnly" } { type: "label", name: "desktopOnly" }
] ]

View File

@ -15,7 +15,8 @@ const noteTypes = [
{ type: "doc", defaultMime: "" }, { type: "doc", defaultMime: "" },
{ type: "contentWidget", defaultMime: "" }, { type: "contentWidget", defaultMime: "" },
{ type: "mindMap", defaultMime: "application/json" }, { type: "mindMap", defaultMime: "application/json" },
{ type: "geoMap", defaultMime: "application/json" } { type: "geoMap", defaultMime: "application/json" },
{ type: "aiChat", defaultMime: "application/json" }
]; ];
function getDefaultMimeForNoteType(typeName: string) { function getDefaultMimeForNoteType(typeName: string) {