diff --git a/manifest.json b/manifest.json index 87b5b64..b2fdcd6 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-weaver", "name": "Weaver", - "version": "0.5.0", + "version": "0.5.2", "minAppVersion": "1.0.0", "description": "Weaver is a useful Obsidian plugin that integrates ChatGPT/GPT-3 into your note-taking workflow. This plugin makes it easy to access AI-generated suggestions and insights within Obsidian, helping you improve your writing and brainstorming process.", "author": "Vasile Câmpeanu", diff --git a/package-lock.json b/package-lock.json index db4a133..efdff52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-weaver", - "version": "0.4.0", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-weaver", - "version": "0.4.0", + "version": "0.5.2", "license": "MIT", "dependencies": { "bson": "^5.3.0", diff --git a/package.json b/package.json index d13dc42..5cb7bc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-weaver", - "version": "0.5.0", + "version": "0.5.2", "description": "Weaver is a useful Obsidian plugin that integrates ChatGPT/GPT-3 into your note-taking workflow. This plugin makes it easy to access AI-generated suggestions and insights within Obsidian, helping you improve your writing and brainstorming process.", "main": "main.js", "scripts": { diff --git a/src/components/Conversation/ConversationDialogue.tsx b/src/components/Conversation/ConversationDialogue.tsx index 350f8a4..cfc1210 100644 --- a/src/components/Conversation/ConversationDialogue.tsx +++ b/src/components/Conversation/ConversationDialogue.tsx @@ -19,13 +19,20 @@ export const ConversationDialogue: React.FC = ({ setConversationSession }) => { const [selectedChildren, setSelectedChildren] = useState<{ [key: string]: number }>({}); - const [activeEngine, setActiveEngine] = useState<"gpt-3.5-turbo" | "gpt-4">(plugin.settings.engine as any); + const [activeEngine, setActiveEngine] = useState<"gpt-3.5-turbo" | "gpt-4">(); + const [activeMode, setActiveMode] = useState<"creative" | "balanced" | "precise">(); const [showEngineInfo, setShowEngineInfo] = useState(false); + const [showConversationEngineInfo, setShowConversationEngineInfo] = useState(plugin.settings.engineInfo); const dialogueTimelineRef = useRef(null); const rootMessage = conversation?.messages.find((msg) => msg.role === "system"); const TIMEOUT_DELAY = 250; + useEffect(() => { + setActiveEngine(conversation?.model as "gpt-3.5-turbo" | "gpt-4") + setActiveMode(conversation?.mode as "creative" | "balanced" | "precise") + }, [conversation?.model, conversation?.mode]) + useEffect(() => { const timer = setTimeout(() => { setShowEngineInfo(true); @@ -116,18 +123,68 @@ export const ConversationDialogue: React.FC = ({ } }; - const handleSetGPT3 = () => { + const handleSetGPT3 = async () => { setActiveEngine("gpt-3.5-turbo"); - plugin.settings.engine = "gpt-3.5-turbo"; - plugin.saveSettings(); + + if (conversation) { + const updatedConversation = { ...conversation, model: "gpt-3.5-turbo" }; + setConversationSession(updatedConversation) + await ConversationManager.updateConversationModel(plugin, conversation!?.id, "gpt-3.5-turbo"); + } } - const handleSetGPT4 = () => { + const handleSetGPT4 = async () => { setActiveEngine("gpt-4"); - plugin.settings.engine = "gpt-4"; - plugin.saveSettings(); + + if (conversation) { + const updatedConversation = { ...conversation, model: "gpt-4" }; + setConversationSession(updatedConversation) + await ConversationManager.updateConversationModel(plugin, conversation!?.id, "gpt-4"); + } } + const handleShowInfoClick = async () => { + setShowConversationEngineInfo(!showConversationEngineInfo); + plugin.settings.engineInfo = true; + await plugin.saveSettings(); + }; + + const handleHideInfoClick = async () => { + setShowConversationEngineInfo(!showConversationEngineInfo); + plugin.settings.engineInfo = false; + await plugin.saveSettings(); + }; + + const handleModeChange = async (newMode: string) => { + setActiveMode(newMode as "creative" | "balanced" | "precise"); + + let systemPromptContent = "" + + if (newMode === "creative") { + systemPromptContent = plugin.settings.creativeSystemRolePrompt; + } else if (newMode === "balanced") { + systemPromptContent = plugin.settings.balancedSystemRolePrompt; + } else if (newMode === "precise") { + systemPromptContent = plugin.settings.preciseSystemRolePrompt; + } + + if (conversation) { + const updatedConversation = { ...conversation, mode: newMode }; + + // Update the system message content in the updated conversation + const systemPrompt = updatedConversation.messages.find(message => message.role === 'system'); + + if (systemPrompt) { + systemPrompt.content = systemPromptContent; // update to your desired content + } + + setConversationSession(updatedConversation); + + await ConversationManager.updateSystemPrompt(plugin, conversation!?.id, systemPromptContent); + await ConversationManager.updateConversationMode(plugin, conversation!?.id, newMode); + } + }; + return (
{ @@ -145,30 +202,93 @@ export const ConversationDialogue: React.FC = ({ showEngineInfo && (
- - + ) : ( + + )} +
+
+
- - GPT-4 - - +
+ + GPT-4 + + {showConversationEngineInfo === true ? ( + + ) : ( + + )} +
+
+
+ {showConversationEngineInfo && } +
+
+ Choose a conversation style +
+
+ + + +
- + ) ) diff --git a/src/components/Conversation/ConversationHeader.tsx b/src/components/Conversation/ConversationHeader.tsx index 834ae5e..4271771 100644 --- a/src/components/Conversation/ConversationHeader.tsx +++ b/src/components/Conversation/ConversationHeader.tsx @@ -122,8 +122,11 @@ export const ConversationHeader: React.FC = ({ } }; - const handleTabSwitch = () => { + const handleTabSwitch = async () => { onTabSwitch("thread-page"); + plugin.settings.lastConversationId = ""; + plugin.settings.loadLastConversationState = false; + await plugin.saveSettings(); } const handleToggleContext = () => { diff --git a/src/components/Conversation/ConversationInput.tsx b/src/components/Conversation/ConversationInput.tsx index 0718b08..180a3a3 100644 --- a/src/components/Conversation/ConversationInput.tsx +++ b/src/components/Conversation/ConversationInput.tsx @@ -142,10 +142,12 @@ export const ConversationInput: React.FC = ({ setInputText(''); } - const onCancelRequest = useCallback(() => { + const onCancelRequest = useCallback(async () => { if (messageDispatcherRef.current) { - messageDispatcherRef.current.handleStopStreaming(); + await messageDispatcherRef.current.handleStopStreaming(); } + + setIsLoading(false); }, []); const handleRegenerateMessage = async () => { diff --git a/src/components/Conversation/ConversationMessageBubble.tsx b/src/components/Conversation/ConversationMessageBubble.tsx index eb3b508..2456847 100644 --- a/src/components/Conversation/ConversationMessageBubble.tsx +++ b/src/components/Conversation/ConversationMessageBubble.tsx @@ -53,35 +53,64 @@ export const ConversationMessageBubble: React.FC }; return ( -
1 ? 'ow-message-bubble-has-top-bar' : ''} ${contextClass}`} key={message.id}> -
- {previousMessage?.children && previousMessage?.children.length > 1 && ( -
-
- - {selectedChild + 1} - + message.role === 'info' ? ( +
+ {message.model === "gpt-3.5-turbo" ? ( + <> +
+
-
+ Using GPT-3.5 + + ) : ( + <> +
+ +
+ Using GPT-4 + )} -
' : ''}` }} - />
-
- + {selectedChild + 1} + +
+
)} - +
' : ''}` }} + /> +
+
+ {message.role === 'user' ? ( + <> + {/* + + */} + + ) : null} + +
-
- ); + ) + ) }; diff --git a/src/components/Thread/ThreadHeader.tsx b/src/components/Thread/ThreadHeader.tsx index 2b4d27e..45542bc 100644 --- a/src/components/Thread/ThreadHeader.tsx +++ b/src/components/Thread/ThreadHeader.tsx @@ -1,6 +1,7 @@ import { IConversation } from "interfaces/IThread"; import Weaver from "main" -import React from "react" +import LocalJsonModal from "modals/ImportModal"; +import React, { useState } from "react" import { ConversationManager } from "utils/ConversationManager"; interface ThreadHeaderProps { @@ -20,36 +21,59 @@ export const ThreadHeader: React.FC = ({ onTabSwitch, onConversationLoad }) => { + // declare a new state variable to keep track of visibility status + const [isSearchVisible, setSearchVisibility] = useState(false); + const handleCreateNewConversation = async () => { const newConversation = await ConversationManager.createNewConversation(plugin); onConversationLoad(newConversation); onTabSwitch("conversation-page"); } + const handleHideSearch = () => { + setSearchVisibility(!isSearchVisible); + } + + const handleOpenImportModal = () => { + new LocalJsonModal(plugin).open(); + } + return (
- Base Thread + Thread View
+ +
-
- -
+ {isSearchVisible && ( +
+ +
+ )}
{messagesCount} conversations
diff --git a/src/components/Thread/ThreadTabsManager.tsx b/src/components/Thread/ThreadTabsManager.tsx index faedb2a..50dbdca 100644 --- a/src/components/Thread/ThreadTabsManager.tsx +++ b/src/components/Thread/ThreadTabsManager.tsx @@ -6,12 +6,13 @@ import { Thread } from "./Thread" import { eventEmitter } from 'utils/EventEmitter'; import { Conversation } from "components/Conversation/Conversation"; import { IConversation } from "interfaces/IThread"; +import { ThreadManager } from "utils/ThreadManager"; interface ThreadTabsManagerProps { plugin: Weaver, } -export const ThreadTabsManager: React.FC = ({plugin}) => { +export const ThreadTabsManager: React.FC = ({ plugin }) => { const [activeTab, setActiveTab] = useState("thread-page"); const [reloadTrigger, setReloadTrigger] = React.useState(0); const [conversation, setConversation] = useState(); @@ -33,21 +34,41 @@ export const ThreadTabsManager: React.FC = ({plugin}) => setActiveTab(tabId); } - const handleConversationLoad = (conversation: IConversation) => { + const handleConversationLoad = async (conversation: IConversation) => { setConversation(conversation); + plugin.settings.lastConversationId = conversation.id; + plugin.settings.loadLastConversationState = true; + await plugin.saveSettings(); }; + useEffect(() => { + (async () => { + if (plugin.settings.loadLastConversation && plugin.settings.loadLastConversationState && plugin.settings.lastConversationId !== "") { + // Add a timeout of 500 milliseconds (0.5 seconds) + const timeout = setTimeout(async () => { + const conversationData = await ThreadManager.getAllConversations(plugin, `${plugin.settings.weaverFolderPath}/threads/base`); + const conversationToLoad = conversationData.find(conversation => conversation.id === plugin.settings.lastConversationId); + handleTabSwitch("conversation-page"); + handleConversationLoad(conversationToLoad as IConversation); + }, 500); + + // Clean up the timeout if the component unmounts before it expires + return () => clearTimeout(timeout); + } + })(); + }, []); + return (
{activeTab === "thread-page" ? ( - ) : ( - ; + mode: string; + model: string; } export interface IThreadDescriptor { diff --git a/src/main.ts b/src/main.ts index 49627f1..8d3cee2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -71,6 +71,11 @@ export default class Weaver extends Plugin { // Check for legacy data await MigrationAssistant.migrateData(this); + if (this.settings.systemRolePrompt !== this.settings.balancedSystemRolePrompt) { + this.settings.systemRolePrompt = this.settings.balancedSystemRolePrompt; + this.saveSettings(); + } + // Refresh thread view eventEmitter.emit('reloadThreadViewEvent'); }, 500); diff --git a/src/settings.ts b/src/settings.ts index f71b5a3..29af11c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -11,17 +11,23 @@ export interface WeaverSettings { activeThreadTitle: string | null, apiKey: string, engine: string, + engineInfo: boolean, frequencyPenalty: number, maxTokens: number, models: any, openOnStartUp: boolean, - showWelcomeMessage: boolean, stream: boolean, systemRolePrompt: string, + creativeSystemRolePrompt: string, + balancedSystemRolePrompt: string, + preciseSystemRolePrompt: string, temperature: number, weaverFolderPath: string, threadViewIdentationGuides: boolean, - threadViewCompactMode: boolean + threadViewCompactMode: boolean, + lastConversationId: string, + loadLastConversationState: boolean, + loadLastConversation: boolean } export const DEFAULT_SETTINGS: WeaverSettings = { @@ -29,17 +35,23 @@ export const DEFAULT_SETTINGS: WeaverSettings = { activeThreadTitle: null, apiKey: "", engine: "gpt-3.5-turbo", + engineInfo: true, frequencyPenalty: 0.5, maxTokens: 512, models: DEFAULT_MODELS, openOnStartUp: true, - showWelcomeMessage: true, stream: false, - systemRolePrompt: "You are a personal knowledge management assistant designed to work within Obsidian, a popular note-taking and knowledge management tool. Your purpose is to help users organize, manage, and expand their knowledge base by providing well-structured, informative, and relevant responses. Please ensure that you format your responses using Markdown syntax, which is the default formatting language used in Obsidian. This includes, but is not limited to, using appropriate headers, lists, links and code blocks. In addition to Markdown, please utilize LaTeX formatting when necessary to render mathematical symbols and equations in a clear and concise manner. This includes, but is not limited to, using symbols such as $\alpha$, $\beta$, $\gamma$, $\delta$, and $\theta$ and equations like $f(x) = x^2 + 2x + 1$ and $\int_{0}^{\infty} e^{-x^2} dx$. For formulas that are on a single line, enclose the LaTeX code between four dollar signs ($$$$) Please ensure that you follow proper LaTeX syntax and formatting guidelines to ensure the readability and coherence of your responses.", + systemRolePrompt: "You are a balanced personal knowledge management assistant designed to provide support within Obsidian, a favored note-taking and knowledge management platform. Your task is to provide medium-length responses that are equally informative, relevant, and organized. You should format your responses using Markdown syntax, which includes appropriate use of headers, lists, links, and code blocks. When needed, utilize LaTeX formatting to represent mathematical symbols and equations, such as $alpha$, $\beta$, $gamma$, $delta$, and $\theta$ and equations like $f(x) = x^2 + 2x + 1$ and $int_{0}^{infty} e^{-x^2} dx$. Enclose single line formulas in four dollar signs ($$$$). Strive to achieve a balance between exactness and creativity while maintaining the readability of your responses through proper LaTeX and Markdown syntax.", + creativeSystemRolePrompt: "You are a creative personal knowledge management assistant designed to flourish within Obsidian, a leading note-taking and knowledge management ecosystem. Your mission is to generate the longest, most imaginative responses that help users expand their knowledge base in novel ways. Format your responses using Markdown syntax, which includes the use of headers, lists, links, and code blocks. When depicting mathematical symbols and equations, use LaTeX formatting to paint a clear picture. This includes symbols such as $alpha$, $\beta$, $gamma$, $delta$, and $\theta$ and equations like $f(x) = x^2 + 2x + 1$ and $int_{0}^{infty} e^{-x^2} dx$. For single line formulas, enclose the LaTeX code between four dollar signs ($$$$). Infuse creativity into your responses while upholding the integrity of LaTeX and Markdown syntax.", + balancedSystemRolePrompt: "You are a balanced personal knowledge management assistant designed to provide support within Obsidian, a favored note-taking and knowledge management platform. Your task is to provide medium-length responses that are equally informative, relevant, and organized. You should format your responses using Markdown syntax, which includes appropriate use of headers, lists, links, and code blocks. When needed, utilize LaTeX formatting to represent mathematical symbols and equations, such as $alpha$, $\beta$, $gamma$, $delta$, and $\theta$ and equations like $f(x) = x^2 + 2x + 1$ and $int_{0}^{infty} e^{-x^2} dx$. Enclose single line formulas in four dollar signs ($$$$). Strive to achieve a balance between exactness and creativity while maintaining the readability of your responses through proper LaTeX and Markdown syntax.", + preciseSystemRolePrompt: "You are a precision-oriented personal knowledge management assistant, designed to operate within Obsidian, a renowned note-taking and knowledge management software. Your role is to deliver the shortest, most accurate, and relevant responses that streamline users' knowledge bases. Format your responses using Markdown syntax, including headers, lists, links, and code blocks. When necessary, use LaTeX formatting to precisely represent mathematical symbols and equations, such as $alpha$, $\beta$, $gamma$, $delta$, and $\theta$ and equations like $f(x) = x^2 + 2x + 1$ and $int_{0}^{infty} e^{-x^2} dx$. Encase single line formulas in four dollar signs ($$$$). Aim to ensure absolute precision in your responses, maintaining strict adherence to LaTeX and Markdown syntax.", temperature: 0.7, weaverFolderPath: "bins/weaver", threadViewIdentationGuides: true, - threadViewCompactMode: false + threadViewCompactMode: false, + lastConversationId: "", + loadLastConversationState: true, + loadLastConversation: true }; export class WeaverSettingTab extends PluginSettingTab { @@ -99,15 +111,37 @@ export class WeaverSettingTab extends PluginSettingTab { containerEl.createEl('h2', { text: 'Model Configuration' }); new Setting(containerEl) - .setName('System Role Prompt') - .setDesc('This setting determines the behavior of the assistant.') - .addText(text => text - .setValue(this.plugin.settings.systemRolePrompt) + .setName('Creative System Role Prompt') + .setDesc('This setting determines the behavior of the assistant in creative mode.') + .addTextArea(text => text + .setValue(this.plugin.settings.creativeSystemRolePrompt) + .onChange(async (value) => { + this.plugin.settings.creativeSystemRolePrompt = value; + await this.plugin.saveSettings(); + }) + ) + + new Setting(containerEl) + .setName('Balanced System Role Prompt') + .setDesc('This setting determines the behavior of the assistant in balanced mode.') + .addTextArea(text => text + .setValue(this.plugin.settings.balancedSystemRolePrompt) .onChange(async (value) => { this.plugin.settings.systemRolePrompt = value; + this.plugin.settings.balancedSystemRolePrompt = value; + await this.plugin.saveSettings(); + }) + ) + + new Setting(containerEl) + .setName('Precise System Role Prompt') + .setDesc('This setting determines the behavior of the assistant in precise mode.') + .addTextArea(text => text + .setValue(this.plugin.settings.preciseSystemRolePrompt) + .onChange(async (value) => { + this.plugin.settings.preciseSystemRolePrompt = value; await this.plugin.saveSettings(); }) - .inputEl.setAttribute('size', '50') ) new Setting(containerEl) @@ -166,12 +200,12 @@ export class WeaverSettingTab extends PluginSettingTab { })); new Setting(containerEl) - .setName('Insert Welcome Message') - .setDesc('Controls whether or not a welcome message will be automatically added when a new chat session is created.') + .setName('Load Last Selectd Conversation') + .setDesc('Loads by default the last conversation you have used.') .addToggle(v => v - .setValue(this.plugin.settings.showWelcomeMessage) + .setValue(this.plugin.settings.loadLastConversation) .onChange(async (value) => { - this.plugin.settings.showWelcomeMessage = value; + this.plugin.settings.loadLastConversation = value; await this.plugin.saveSettings(); })); diff --git a/src/styles/components/ConversationDialogue.scss b/src/styles/components/ConversationDialogue.scss index f65424a..363baf7 100644 --- a/src/styles/components/ConversationDialogue.scss +++ b/src/styles/components/ConversationDialogue.scss @@ -43,7 +43,7 @@ padding: 3.5px; border-radius: 10px; - button + .ow-btn-change-model { all: unset; padding: 8.5px 21px; @@ -66,24 +66,37 @@ } } - span + .ow-engine-wrapper { - color: var(--text-muted); - font-size: 13px; - } + display: flex; + gap: 5px; - .ow-btn-icon - { - display: none; - align-self: center; + span + { + align-self: center; + color: var(--text-muted); + font-size: 13px; + } - svg + .ow-btn-show-info { - place-self: center; - width: 16px; - height: 16px; - stroke-width: 3; - color: var(--text-normal) !important; + all: unset; + display: none; + align-self: center; + border-radius: 2.5px; + + &:hover + { + background-color: var(--background-modifier-hover); + } + + svg + { + place-self: center; + width: 16px; + height: 16px; + color: var(--icon-color) !important; + } } } @@ -102,14 +115,17 @@ } } - span - { - color: var(--text-normal); - } - - .ow-btn-icon + .ow-engine-wrapper { - display: grid; + span + { + color: var(--text-normal); + } + + .ow-btn-show-info + { + display: grid; + } } } } @@ -231,6 +247,67 @@ } } } + + .ow-change-mode + { + display: grid; + gap: 20px; + margin-top: 300px; + + &.showConversationEngineInfoEnabled + { + margin-top: 0px; + } + + .ow-title + { + justify-self: center; + font-size: 13px; + } + + .ow-mode-list + { + justify-self: center; + display: flex; + gap: 5px; + + background-color: var(--interactive-normal); + box-shadow: rgb(0 0 0 / 10%) 0px 0px 5px 0px, rgb(0 0 0 / 10%) 0px 0px 1px 0px; + padding: 3.5px; + border: 1px solid var(--prompt-border-color); + border-radius: 5px; + + .ow-mode-wrapper + { + all: unset; + display: grid; + gap: 2.5px; + padding: 7px 20px; + border-radius: 5px; + + background-color: var(--ui2); + + &.active + { + background-color: var(--interactive-accent); + } + + .ow-more + { + text-align: center; + font-size: 11px; + line-height: 1em; + } + + .ow-mode + { + font-size: 13px; + line-height: 1em; + font-weight: 500; + } + } + } + } } } } diff --git a/src/styles/components/ConversationInputArea.scss b/src/styles/components/ConversationInputArea.scss index 39cc7f0..18da86e 100644 --- a/src/styles/components/ConversationInputArea.scss +++ b/src/styles/components/ConversationInputArea.scss @@ -53,14 +53,13 @@ height: 40px; width: 40px; border-radius: 50%; - background-color: hsla(var(--interactive-accent-hsl), 0.55); + background-color: var(--interactive-accent); box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 5px 0px, rgba(0, 0, 0, 0.1) 0px 0px 1px 0px; color: #ffffff; cursor: var(--cursor); &:hover { - background-color: hsla(var(--interactive-accent-hsl), .75); } svg @@ -68,7 +67,6 @@ place-self: center; width: 21px; height: 21px; - aspect-ratio: 1/1; } grid-column: 1; diff --git a/src/styles/components/ConversationMessageBubble.scss b/src/styles/components/ConversationMessageBubble.scss index 884474b..948a87f 100644 --- a/src/styles/components/ConversationMessageBubble.scss +++ b/src/styles/components/ConversationMessageBubble.scss @@ -1,3 +1,28 @@ +.ow-message-info-bubble +{ + display: flex; + gap: 5px; + + .ow-icon + { + align-self: center; + svg + { + color: var(--interactive-accent); + fill: var(--interactive-accent); + width: 11px; + height: 11px; + } + } + + span + { + align-self: center; + font-size: 11px; + color: var(--text-muted); + } +} + .ow-message-bubble { display: flex; @@ -54,6 +79,10 @@ .ow-message-actions { + display: flex; + align-content: start; + align-items: start; + gap: 5px; opacity: 0; .ow-copy-button @@ -77,6 +106,28 @@ } } } + + .ow-edit-button + { + all: unset; + cursor: var(--cursor); + + svg + { + width: 13px; + height: 13px; + color: var(--icon-color); + opacity: .5; + } + + &:hover + { + svg + { + opacity: .75; + } + } + } } &:hover .ow-message-actions @@ -109,7 +160,7 @@ .ow-message-bubble-content { order: 2; - background-color: hsla(var(--interactive-accent-hsl), 0.55); + background-color: var(--interactive-accent); .ow-message-bubble-top-bar { diff --git a/src/styles/components/Thread.scss b/src/styles/components/Thread.scss index 964bd62..2e0ae95 100644 --- a/src/styles/components/Thread.scss +++ b/src/styles/components/Thread.scss @@ -3,7 +3,7 @@ display: grid; height: 100%; overflow-y: hidden !important; - + .ow-thread-tabs-manager { align-self: stretch; @@ -20,7 +20,6 @@ { display: grid; grid-template-columns: 1fr; - gap: 5px; padding-bottom: 15px; .ow-thread-title @@ -39,6 +38,7 @@ .ow-actions { + display: flex; align-self: center; button @@ -84,8 +84,7 @@ .ow-messages-count { font-size: 11px; - color: var(--text-muted); - margin: 2.5px 0px; + color: var(--text-faint); } hr @@ -145,7 +144,6 @@ display: flex; gap: 5px; align-self: center; - font-weight: bold; color: var(--text-muted); text-transform: uppercase; diff --git a/src/styles/components/ThreadListItem.scss b/src/styles/components/ThreadListItem.scss index b3c6d1d..6e2ef20 100644 --- a/src/styles/components/ThreadListItem.scss +++ b/src/styles/components/ThreadListItem.scss @@ -25,16 +25,21 @@ grid-template-columns: auto 1fr; gap: 10px; padding: 5px 7px; - border-radius: 3.5px; + padding-left: 2px; + // border-bottom: var(--nav-indentation-guide-width) solid var(--nav-indentation-guide-color); + border-left: 3px solid transparent; &:hover { background-color: var(--nav-item-background-active); + border-left: 3px solid var(--interactive-accent); + border-radius: 3.5px; } .ow-chat-icon { display: grid; + margin-left: 3px; .ow-icon { @@ -50,7 +55,7 @@ width: 13px; height: 13px; color: var(--nav-item-color); - strokeWidth: 2; + stroke-width: 2; } } } @@ -81,7 +86,7 @@ overflow: hidden; text-overflow: ellipsis; width: 100%; - font-size: 14px; + font-size: 13px; color: var(--nav-item-color); } } @@ -122,6 +127,7 @@ place-self: center; width: 11px; height: 11px; + color: var(--icon-color); } } diff --git a/src/utils/ConversationManager.ts b/src/utils/ConversationManager.ts index 98d9935..242e52c 100644 --- a/src/utils/ConversationManager.ts +++ b/src/utils/ConversationManager.ts @@ -29,7 +29,7 @@ export class ConversationManager { // Create a new conversation object const newConversation: IConversation = { - id: Date.now().toString(), // Generate a unique id + id: uuidv4(), // Generate a unique id title: uniqueTitle, identifier: 'obsidian-weaver', currentNode: currentNodeId, @@ -43,12 +43,20 @@ export class ConversationManager { context: false, creationDate: new Date().toISOString(), id: currentNodeId, + model: plugin.settings.engine, + mode: "balanced", role: "system", parent: uuidv4(), } - ] + ], + mode: "balanced", + model: plugin.settings.engine }; + plugin.settings.lastConversationId = newConversation.id; + plugin.settings.loadLastConversationState = true; + await plugin.saveSettings(); + // Ensure the folder exists await FileIOManager.ensureWeaverFolderPathExists(plugin); await FileIOManager.ensureFolderPathExists(plugin, "threads/base"); @@ -129,28 +137,28 @@ export class ConversationManager { static async updateConversationTitleById(plugin: Weaver, id: string, newTitle: string): Promise<{ success: boolean; errorMessage?: string }> { try { - const adapter = plugin.app.vault.adapter as FileSystemAdapter; + const adapter = plugin.app.vault.adapter as FileSystemAdapter; const folderPath = `${plugin.settings.weaverFolderPath}/threads/base`; const folderContent = await adapter.list(folderPath); const filesInFolder = folderContent.files.filter(filePath => filePath.endsWith('.json')); - + const conversations: IConversation[] = []; - + // Iterate through files to build conversations list for (const filePath of filesInFolder) { const fileContent = await adapter.read(filePath); const conversation = JSON.parse(fileContent) as IConversation; conversations.push(conversation); } - + // Check for duplicate titles and adjust newTitle if necessary newTitle = this.getUniqueTitle(newTitle, conversations); - + // Iterate through files again to update the conversation with the correct id for (const filePath of filesInFolder) { const fileContent = await adapter.read(filePath); const conversation = JSON.parse(fileContent) as IConversation; - + if (conversation.id === id && conversation.identifier === 'obsidian-weaver') { plugin.isRenamingFromInside = true; conversation.title = newTitle; @@ -160,7 +168,7 @@ export class ConversationManager { plugin.isRenamingFromInside = false; } } - + return { success: true }; } catch (error) { console.error(`Error updating conversation title: ${error}`); @@ -233,4 +241,126 @@ export class ConversationManager { console.error(`Conversation with ID: ${id} not found`); return []; } + + static async addChildToMessage(plugin: Weaver, conversationId: string, messageId: string, newChildId: string): Promise { + const folderPath = `${plugin.settings.weaverFolderPath}/threads/base`; + const adapter = plugin.app.vault.adapter as FileSystemAdapter; + + const folderContent = await adapter.list(folderPath); + const filesInFolder = folderContent.files.filter(filePath => filePath.endsWith('.json')); + + for (const filePath of filesInFolder) { + const fileContent = await adapter.read(filePath); + const conversation = JSON.parse(fileContent) as IConversation; + + if (conversation.id === conversationId && conversation.identifier === 'obsidian-weaver') { + // Find the target message in the conversation's messages + const targetMessage = conversation.messages.find(message => message.id === messageId); + + if (!targetMessage) { + console.error('Target message not found in the conversation.'); + return false; + } + + // Add the new child id to the target message's children array + targetMessage.children.push(newChildId); + + // Update lastModified + conversation.lastModified = new Date().toISOString(); + + // Write the updated conversation back to the file + await adapter.write(filePath, JSON.stringify(conversation, null, 4)); + + return true; + } + } + + console.error(`Conversation with ID: ${conversationId} not found`); + return false; + } + + static async updateConversationModel(plugin: Weaver, id: string, newModel: string): Promise { + const folderPath = `${plugin.settings.weaverFolderPath}/threads/base`; + const adapter = plugin.app.vault.adapter as FileSystemAdapter; + + const folderContent = await adapter.list(folderPath); + const filesInFolder = folderContent.files.filter(filePath => filePath.endsWith('.json')); + + for (const filePath of filesInFolder) { + const fileContent = await adapter.read(filePath); + const conversation = JSON.parse(fileContent) as IConversation; + + if (conversation.id === id && conversation.identifier === 'obsidian-weaver') { + conversation.model = newModel; + conversation.lastModified = new Date().toISOString(); + + await adapter.write(filePath, JSON.stringify(conversation, null, 4)); + return true; + } + } + + console.error(`Conversation with ID: ${id} not found`); + return false; + } + + static async updateConversationMode(plugin: Weaver, id: string, newMode: string): Promise { + const folderPath = `${plugin.settings.weaverFolderPath}/threads/base`; + const adapter = plugin.app.vault.adapter as FileSystemAdapter; + + const folderContent = await adapter.list(folderPath); + const filesInFolder = folderContent.files.filter(filePath => filePath.endsWith('.json')); + + for (const filePath of filesInFolder) { + const fileContent = await adapter.read(filePath); + const conversation = JSON.parse(fileContent) as IConversation; + + if (conversation.id === id && conversation.identifier === 'obsidian-weaver') { + conversation.mode = newMode; + conversation.lastModified = new Date().toISOString(); + + await adapter.write(filePath, JSON.stringify(conversation, null, 4)); + return true; + } + } + + console.error(`Conversation with ID: ${id} not found`); + return false; + } + + static async updateSystemPrompt(plugin: Weaver, id: string, newPrompt: string): Promise { + const folderPath = `${plugin.settings.weaverFolderPath}/threads/base`; + const adapter = plugin.app.vault.adapter as FileSystemAdapter; + + const folderContent = await adapter.list(folderPath); + const filesInFolder = folderContent.files.filter(filePath => filePath.endsWith('.json')); + + for (const filePath of filesInFolder) { + const fileContent = await adapter.read(filePath); + const conversation = JSON.parse(fileContent) as IConversation; + + if (conversation.id === id && conversation.identifier === 'obsidian-weaver') { + // Find the system prompt in the conversation's messages + const systemPrompt = conversation.messages.find(message => message.role === 'system'); + + if (!systemPrompt) { + console.error('System prompt not found in the conversation.'); + return false; + } + + // Update the content of the system prompt + systemPrompt.content = newPrompt; + + // Update lastModified + conversation.lastModified = new Date().toISOString(); + + // Write the updated conversation back to the file + await adapter.write(filePath, JSON.stringify(conversation, null, 4)); + + return true; + } + } + + console.error(`Conversation with ID: ${id} not found`); + return false; + } } diff --git a/src/utils/MigrationAssistant.ts b/src/utils/MigrationAssistant.ts index 50a99f2..d057f84 100644 --- a/src/utils/MigrationAssistant.ts +++ b/src/utils/MigrationAssistant.ts @@ -77,7 +77,9 @@ export class MigrationAssistant { creationDate: message.timestamp, id: messageId, role: message.role, - parent: previousMessageId + parent: previousMessageId, + mode: "balanced", + model: plugin.settings.engine }; if (previousMessage) { @@ -114,7 +116,9 @@ export class MigrationAssistant { identifier: "obsidian-weaver", lastModified: conversation.timestamp, title: conversationTitle, - messages: newMessages + messages: newMessages, + model: plugin.settings.engine, + mode: "balanced" }; let conversationPath = normalizePath(`${plugin.settings.weaverFolderPath}/threads/base/${conversationTitle}.json`); diff --git a/src/utils/WeaverImporter.ts b/src/utils/WeaverImporter.ts index b39ff2d..c4503e1 100644 --- a/src/utils/WeaverImporter.ts +++ b/src/utils/WeaverImporter.ts @@ -51,20 +51,25 @@ export class WeaverImporter { for (const nodeId in conversation.mapping) { const node = conversation.mapping[nodeId]; const messageData = node.message; - + if (messageData) { + const contentParts = messageData.content?.parts; + const content = Array.isArray(contentParts) ? contentParts.join(' ') : ''; + messages.push({ - content: messageData.content.parts.join(' '), + content: content, context: false, creationDate: new Date(messageData.create_time * 1000).toISOString(), id: messageData.id, role: messageData.author.role, parent: node.parent, - children: node.children + children: node.children, + mode: "balanced", + model: plugin.settings.engine }); } } - + const conversationData: IConversation = { context: true, creationDate: new Date(conversation.create_time * 1000).toISOString(), @@ -74,6 +79,8 @@ export class WeaverImporter { lastModified: new Date(conversation.update_time * 1000).toISOString(), title: conversationTitle, messages: messages, + mode: "balanced", + model: plugin.settings.engine }; await FileIOManager.ensureFolderPathExists(plugin, "threads/base"); diff --git a/src/utils/api/OpenAIContentProvider.ts b/src/utils/api/OpenAIContentProvider.ts index 7309fd6..702dc7e 100644 --- a/src/utils/api/OpenAIContentProvider.ts +++ b/src/utils/api/OpenAIContentProvider.ts @@ -1,6 +1,6 @@ import Weaver from "main"; import OpenAIRequestFormatter from "./OpenAIRequestFormatter"; -import { IChatMessage } from "interfaces/IThread"; +import { IChatMessage, IConversation } from "interfaces/IThread"; import { Notice } from "obsidian"; import { OpenAIRequestManager } from "./OpenAIRequestManager"; @@ -12,18 +12,19 @@ export default class OpenAIContentProvider { constructor(plugin: Weaver) { this.plugin = plugin; this.OpenAIRequestFormatter = new OpenAIRequestFormatter(this.plugin); - this.streamManager = new OpenAIRequestManager(); + this.streamManager = new OpenAIRequestManager(plugin); } public async generateResponse( parameters: any = this.plugin.settings, additionalParameters: any = {}, + conversation: IConversation, conversationContext: IChatMessage[], userMessage: IChatMessage, addMessage: (message: IChatMessage) => void, updateCurrentAssistantMessageContent: (content: string) => void, ) { - const requestParameters = this.OpenAIRequestFormatter.prepareChatRequestParameters(parameters, additionalParameters, conversationContext); + const requestParameters = this.OpenAIRequestFormatter.prepareChatRequestParameters(parameters, additionalParameters, conversation, conversationContext); try { await this.streamManager.handleOpenAIStreamSSE( @@ -31,6 +32,7 @@ export default class OpenAIContentProvider { userMessage, addMessage, updateCurrentAssistantMessageContent, + conversation ); } catch (error) { console.error('Error in handleOpenAIStreamSSE:', error.data); diff --git a/src/utils/api/OpenAIMessageDispatcher.ts b/src/utils/api/OpenAIMessageDispatcher.ts index 7ea2027..8d7dfcc 100644 --- a/src/utils/api/OpenAIMessageDispatcher.ts +++ b/src/utils/api/OpenAIMessageDispatcher.ts @@ -9,9 +9,9 @@ export class OpenAIMessageDispatcher { private conversation?: IConversation; private setConversationSession: Function; private updateConversation: Function; - private userMessage?: IChatMessage; private loadingAssistantMessage?: IChatMessage; + private infoMessage?: IChatMessage; private openAIContentProvider: OpenAIContentProvider; constructor(plugin: Weaver, conversation: IConversation, setConversationSession: Function, updateConversation: Function) { @@ -21,6 +21,7 @@ export class OpenAIMessageDispatcher { this.setConversationSession = setConversationSession; this.userMessage = undefined; this.loadingAssistantMessage = undefined; + this.infoMessage = undefined; this.openAIContentProvider = new OpenAIContentProvider(plugin) if (!this.conversation) { @@ -28,24 +29,31 @@ export class OpenAIMessageDispatcher { } } - private createUserMessage(inputText: string): IChatMessage { + private createUserMessage(inputText: string, shouldSetInfoMessageAsParent: boolean): IChatMessage { const currentNode = this.conversation?.currentNode; const currentMessage: IChatMessage | undefined = this.conversation?.messages.find((message) => message.id === currentNode); - const userMessageParentId: string = currentMessage?.id ?? uuidv4(); - + let userMessageParentId: string = currentMessage?.id ?? uuidv4(); + + // If the condition is true, set the parent as the info message + if (shouldSetInfoMessageAsParent) { + userMessageParentId = this.infoMessage!.id; + } + const userMessage: IChatMessage = { children: [], context: false, content: inputText, creationDate: new Date().toISOString(), id: uuidv4(), + mode: this.conversation!?.mode, + model: this.conversation!?.model, role: 'user', parent: userMessageParentId }; return userMessage; } - + private createAssistantLoadingMessage(userMessageId: string): IChatMessage { const loadingAssistantMessage: IChatMessage = { children: [], @@ -54,6 +62,8 @@ export class OpenAIMessageDispatcher { creationDate: '', id: uuidv4(), isLoading: true, + mode: this.conversation!?.mode, + model: this.conversation!?.model, role: 'assistant', parent: userMessageId }; @@ -61,6 +71,22 @@ export class OpenAIMessageDispatcher { return loadingAssistantMessage; } + private createInfoMessage(parentId: string): IChatMessage { + const infoMessage: IChatMessage = { + children: [], + context: false, + content: 'Info...', + creationDate: new Date().toISOString(), + id: uuidv4(), + mode: this.conversation!?.mode, + model: this.conversation!?.model, + role: 'info', + parent: parentId + }; + + return infoMessage; + } + public async addMessage(message: IChatMessage) { await this.updateConversation(message, (contextMessages: IChatMessage[]) => { this.setConversationSession((conversation: IConversation) => { @@ -111,8 +137,31 @@ export class OpenAIMessageDispatcher { setIsLoading: Function ) { setIsLoading(true) + + const shouldAddInfoMessage = this.conversation?.messages.length === 1; + + if (shouldAddInfoMessage) { + const systemMessage = this.conversation!.messages[0]; + this.infoMessage = this.createInfoMessage(systemMessage.id); + + // Add info message to conversation + await this.updateConversation(this.infoMessage, (contextMessages: IChatMessage[]) => { + this.setConversationSession((conversation: IConversation) => { + if (conversation) { + return { + ...conversation, + currentNode: this.infoMessage!.id, + lastModified: new Date().toISOString(), + messages: contextMessages + }; + } else { + return conversation; + } + }); + }); + } - this.userMessage = this.createUserMessage(inputText); + this.userMessage = this.createUserMessage(inputText, shouldAddInfoMessage); await this.updateConversation(this.userMessage, (contextMessages: IChatMessage[]) => { this.setConversationSession((conversation: IConversation) => { @@ -133,8 +182,12 @@ export class OpenAIMessageDispatcher { if (this.conversation?.context === true) { const rootMessage = this.conversation?.messages.find((msg) => msg.role === "system"); - let currentNodeMessages = rootMessage ? getRenderedMessages(this.conversation) : []; - contextMessages = [...(currentNodeMessages), this.userMessage]; + let currentNodeMessages: IChatMessage[] = rootMessage ? getRenderedMessages(this.conversation) : []; + let filteredMessages: IChatMessage[] = currentNodeMessages.filter(message => { + return message.role === 'assistant' || message.role === 'user' || message.role === 'system'; + }); + console.log(filteredMessages); + contextMessages = [...(filteredMessages), this.userMessage]; } else { contextMessages = [this.userMessage]; } @@ -170,12 +223,13 @@ export class OpenAIMessageDispatcher { await this.openAIContentProvider.generateResponse( this.plugin.settings, {}, + this.conversation as IConversation, contextMessages, this.userMessage, this.addMessage.bind(this), this.updateCurrentAssistantMessageContent.bind(this) ); - + setIsLoading(false) } @@ -189,10 +243,16 @@ export class OpenAIMessageDispatcher { ) { setIsLoading(true) - let currentNodeMessages = getRenderedMessages(this.conversation); - const reverseMessages = currentNodeMessages.reverse(); + let currentNodeMessages: IChatMessage[] = getRenderedMessages(this.conversation); + console.log(currentNodeMessages); + let filteredMessages: IChatMessage[] = currentNodeMessages.filter(message => { + return message.role === 'assistant' || message.role === 'user' || message.role === 'system'; + }); + + const reverseMessages = filteredMessages.reverse(); const lastUserMessage = reverseMessages.find((message: { role: string; }) => message.role === 'user'); - currentNodeMessages.reverse(); + + filteredMessages.reverse(); if (!lastUserMessage) { console.error('No user message found to regenerate.'); @@ -201,10 +261,10 @@ export class OpenAIMessageDispatcher { this.userMessage = lastUserMessage; - if(this.conversation?.context === false) { - currentNodeMessages = [this.userMessage]; + if (this.conversation?.context === false) { + filteredMessages = [this.userMessage]; } else { - currentNodeMessages.splice(currentNodeMessages.length - 1, 1); + filteredMessages.splice(filteredMessages.length - 1, 1); } this.loadingAssistantMessage = this.createAssistantLoadingMessage(this.userMessage!.id); @@ -239,7 +299,8 @@ export class OpenAIMessageDispatcher { await this.openAIContentProvider.generateResponse( this.plugin.settings, {}, - currentNodeMessages, + this.conversation as IConversation, + filteredMessages, this.userMessage as IChatMessage, this.addMessage.bind(this), this.updateCurrentAssistantMessageContent.bind(this) diff --git a/src/utils/api/OpenAIRequestFormatter.ts b/src/utils/api/OpenAIRequestFormatter.ts index 41a625d..8778cf5 100644 --- a/src/utils/api/OpenAIRequestFormatter.ts +++ b/src/utils/api/OpenAIRequestFormatter.ts @@ -3,7 +3,7 @@ import Weaver from "main"; import { WeaverSettings } from "settings"; // Interfaces -import { IChatMessage } from "interfaces/IThread"; +import { IChatMessage, IConversation } from "interfaces/IThread"; interface BodyParameters { frequency_penalty: number; @@ -21,7 +21,7 @@ export default class OpenAIRequestFormatter { this.plugin = plugin; } - prepareChatRequestParameters(parameters: WeaverSettings, additionalParameters: any = {}, conversationHistory: IChatMessage[] = []) { + prepareChatRequestParameters(parameters: WeaverSettings, additionalParameters: any = {}, conversation: IConversation, conversationHistory: IChatMessage[] = []) { try { const requestUrlBase = "https://api.openai.com/v1"; let requestUrl = `${requestUrlBase}/chat/completions`; @@ -29,7 +29,7 @@ export default class OpenAIRequestFormatter { const bodyParameters: BodyParameters = { frequency_penalty: parameters.frequencyPenalty, max_tokens: parameters.maxTokens, - model: parameters.engine, + model: conversation.model, temperature: parameters.temperature, stream: true }; diff --git a/src/utils/api/OpenAIRequestManager.ts b/src/utils/api/OpenAIRequestManager.ts index 337b748..bf0dcd7 100644 --- a/src/utils/api/OpenAIRequestManager.ts +++ b/src/utils/api/OpenAIRequestManager.ts @@ -1,25 +1,35 @@ -import { IChatMessage } from "interfaces/IThread"; +import { IChatMessage, IConversation } from "interfaces/IThread"; import { SSE } from "../../js/sse/sse"; import { v4 as uuidv4 } from 'uuid'; +import Weaver from "main"; export class OpenAIRequestManager { + private readonly plugin: Weaver; private stopRequested = false; private assistantResponseChunks: string[] = []; private DONE_MESSAGE = '[DONE]'; - constructor() { } + constructor(plugin: Weaver) { + this.plugin = plugin; + } stopStreaming(): void { this.stopRequested = true; } - private createAssistantMessage(userMessage: IChatMessage, content: string): IChatMessage { + private createAssistantMessage( + userMessage: IChatMessage, + content: string, + conversation: IConversation + ): IChatMessage { return { children: [], content: content, context: false, creationDate: new Date().toISOString(), id: uuidv4(), + mode: conversation.mode, + model: conversation.model, role: 'assistant', parent: userMessage.id }; @@ -31,11 +41,16 @@ export class OpenAIRequestManager { userMessage: IChatMessage, addMessage: (message: IChatMessage) => void, updateCurrentAssistantMessageContent: (content: string) => void, - resolve: (value: string | null) => void + resolve: (value: string | null) => void, + conversation: IConversation ) => { if (this.stopRequested) { response?.close(); - addMessage(this.createAssistantMessage(userMessage, this.assistantResponseChunks.join(''))); + addMessage(this.createAssistantMessage( + userMessage, + this.assistantResponseChunks.join(''), + conversation + )); this.stopRequested = false; this.assistantResponseChunks = []; return; @@ -53,7 +68,11 @@ export class OpenAIRequestManager { updateCurrentAssistantMessageContent(this.assistantResponseChunks.join('')); } else { response?.close(); - addMessage(this.createAssistantMessage(userMessage, this.assistantResponseChunks.join(''))); + addMessage(this.createAssistantMessage( + userMessage, + this.assistantResponseChunks.join(''), + conversation + )); this.assistantResponseChunks = []; resolve(this.assistantResponseChunks.join('')); } @@ -64,7 +83,8 @@ export class OpenAIRequestManager { response: SSE, userMessage: IChatMessage, addMessage: (message: IChatMessage) => void, - reject: (reason?: any) => void + reject: (reason?: any) => void, + conversation: IConversation ) => { const errorStatus = JSON.parse(error.status); let errorMessage = ""; @@ -86,7 +106,11 @@ export class OpenAIRequestManager { errorMessage = `Error: HTTP error! status: ${errorStatus}`; } - addMessage(this.createAssistantMessage(userMessage, errorMessage)); + addMessage(this.createAssistantMessage( + userMessage, + errorMessage, + conversation + )); response?.close(); reject(new Error(errorMessage)); @@ -96,7 +120,8 @@ export class OpenAIRequestManager { requestParameters: any, userMessage: IChatMessage, addMessage: (message: IChatMessage) => void, - updateCurrentAssistantMessageContent: (content: string) => void + updateCurrentAssistantMessageContent: (content: string) => void, + conversation: IConversation ): Promise => { return new Promise((resolve, reject) => { try { @@ -110,8 +135,8 @@ export class OpenAIRequestManager { payload: requestParameters.body, }); - response.addEventListener('message', (e: any) => this.onMessage(e, response, userMessage, addMessage, updateCurrentAssistantMessageContent, resolve)); - response.addEventListener('error', (error: any) => this.onError(error, response, userMessage, addMessage, reject)); + response.addEventListener('message', (e: any) => this.onMessage(e, response, userMessage, addMessage, updateCurrentAssistantMessageContent, resolve, conversation)); + response.addEventListener('error', (error: any) => this.onError(error, response, userMessage, addMessage, reject, conversation)); response.stream(); } catch (err) { diff --git a/versions.json b/versions.json index 892af57..0a8fa09 100644 --- a/versions.json +++ b/versions.json @@ -2,5 +2,6 @@ "0.3.0": "1.0.0", "0.3.2": "1.0.0", "0.4.0": "1.0.0", - "0.5.0": "1.0.0" + "0.5.0": "1.0.0", + "0.5.2": "1.0.0" }