diff --git a/db/migrations/0229__ai_llm_options.sql b/db/migrations/0229__ai_llm_options.sql index 065373c48..dcbd42b8b 100644 --- a/db/migrations/0229__ai_llm_options.sql +++ b/db/migrations/0229__ai_llm_options.sql @@ -1,22 +1,26 @@ -- Add new options for AI/LLM integration -INSERT INTO options (name, value, isSynced) VALUES ('aiEnabled', 'false', 1); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('aiEnabled', 'false', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); -- OpenAI settings -INSERT INTO options (name, value, isSynced) VALUES ('openaiApiKey', '', 1); -INSERT INTO options (name, value, isSynced) VALUES ('openaiDefaultModel', 'gpt-4o', 1); -INSERT INTO options (name, value, isSynced) VALUES ('openaiBaseUrl', 'https://api.openai.com/v1', 1); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('openaiApiKey', '', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('openaiDefaultModel', 'gpt-4o', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('openaiBaseUrl', 'https://api.openai.com/v1', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); -- Anthropic settings -INSERT INTO options (name, value, isSynced) VALUES ('anthropicApiKey', '', 1); -INSERT INTO options (name, value, isSynced) VALUES ('anthropicDefaultModel', 'claude-3-opus-20240229', 1); -INSERT INTO options (name, value, isSynced) VALUES ('anthropicBaseUrl', 'https://api.anthropic.com/v1', 1); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('anthropicApiKey', '', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('anthropicDefaultModel', 'claude-3-opus-20240229', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('anthropicBaseUrl', 'https://api.anthropic.com/v1', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); -- Ollama settings -INSERT INTO options (name, value, isSynced) VALUES ('ollamaEnabled', 'false', 1); -INSERT INTO options (name, value, isSynced) VALUES ('ollamaBaseUrl', 'http://localhost:11434', 1); -INSERT INTO options (name, value, isSynced) VALUES ('ollamaDefaultModel', 'llama3', 1); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('ollamaEnabled', 'false', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('ollamaBaseUrl', 'http://localhost:11434', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('ollamaDefaultModel', 'llama3', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('ollamaEmbeddingModel', 'nomic-embed-text', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); -- General AI settings -INSERT INTO options (name, value, isSynced) VALUES ('aiProviderPrecedence', 'openai,anthropic,ollama', 1); -INSERT INTO options (name, value, isSynced) VALUES ('aiTemperature', '0.7', 1); -INSERT INTO options (name, value, isSynced) VALUES ('aiSystemPrompt', '', 1); \ No newline at end of file +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('aiProviderPrecedence', 'openai,anthropic,ollama', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('aiTemperature', '0.7', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('aiSystemPrompt', '', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); + +-- Embedding settings +INSERT INTO options (name, value, isSynced, utcDateModified) VALUES ('embeddingsDefaultProvider', 'openai', 1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); \ No newline at end of file diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index 8c62a5586..b3b8f0b2d 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -83,6 +83,7 @@ export type CommandMappings = { closeHlt: CommandData; showLaunchBarSubtree: CommandData; showRevisions: CommandData; + showLlmChat: CommandData; showOptions: CommandData & { section: string; }; diff --git a/src/public/app/components/note_context.ts b/src/public/app/components/note_context.ts index 0b4798cea..2562d22e3 100644 --- a/src/public/app/components/note_context.ts +++ b/src/public/app/components/note_context.ts @@ -369,6 +369,11 @@ 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 eb46e3139..96e917d81 100644 --- a/src/public/app/components/root_command_executor.ts +++ b/src/public/app/components/root_command_executor.ts @@ -7,8 +7,12 @@ import protectedSessionService from "../services/protected_session.js"; import options from "../services/options.js"; 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"; export default class RootCommandExecutor extends Component { + private llmChatPanel: any = null; + editReadOnlyNoteCommand() { const noteContext = appContext.tabManager.getActiveContext(); if (noteContext?.viewScope) { @@ -226,4 +230,23 @@ export default class RootCommandExecutor extends Component { appContext.tabManager.activateNoteContext(tab.ntxId); } } + + async showLlmChatCommand() { + console.log("showLlmChatCommand triggered"); + toastService.showMessage("Opening LLM Chat..."); + + 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 + } + }); + } + catch (e) { + console.error("Error opening LLM Chat:", e); + toastService.showError("Failed to open LLM Chat: " + (e as Error).message); + } + } } diff --git a/src/public/app/widgets/containers/launcher.ts b/src/public/app/widgets/containers/launcher.ts index 86fbabb96..942bdb60b 100644 --- a/src/public/app/widgets/containers/launcher.ts +++ b/src/public/app/widgets/containers/launcher.ts @@ -123,6 +123,8 @@ export default class LauncherWidget extends BasicWidget { return new TodayLauncher(note); case "quickSearch": return new QuickSearchLauncherWidget(this.isHorizontalLayout); + case "llmChatLauncher": + return new ScriptLauncher(note); default: throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); } diff --git a/src/public/app/widgets/llm_chat_panel.ts b/src/public/app/widgets/llm_chat_panel.ts new file mode 100644 index 000000000..84ab68f77 --- /dev/null +++ b/src/public/app/widgets/llm_chat_panel.ts @@ -0,0 +1,246 @@ +import BasicWidget from "./basic_widget.js"; +import toastService from "../services/toast.js"; +import server from "../services/server.js"; +import appContext from "../components/app_context.js"; +import utils from "../services/utils.js"; +import { t } from "../services/i18n.js"; + +interface ChatResponse { + id: string; + messages: Array<{role: string; content: string}>; + sources?: Array<{noteId: string; title: string}>; +} + +interface SessionResponse { + id: string; + title: string; +} + +export default class LlmChatPanel extends BasicWidget { + private noteContextChatMessages!: HTMLElement; + private noteContextChatForm!: HTMLFormElement; + private noteContextChatInput!: HTMLTextAreaElement; + private noteContextChatSendButton!: HTMLButtonElement; + private chatContainer!: HTMLElement; + private loadingIndicator!: HTMLElement; + private sourcesList!: HTMLElement; + private sessionId: string | null = null; + private currentNoteId: string | null = null; + + doRender() { + this.$widget = $(` +
$1