SystemSculpt / obsidian-systemsculpt-ai

Enhance your Obsidian App experience with AI-powered tools for note-taking, task management, and much, MUCH more.
MIT License
100 stars 13 forks source link

Multiple AI Chat Tabs #43

Open jasonsparc opened 3 months ago

jasonsparc commented 3 months ago

Describe the enhancement

That is, instead of a single pane on the right side bar, have chats be opened each on its own separate tab, similar to how notes are opened.

When researching, I often have multiple tabs of Claude, Chat GPT, and Google AI Studio, all opened in my Chrome browser, with each separate topic in its own browser tab. It would be really great if something like that was integrated into Obsidian via your plugin.

Now, most AI chat plugins out there doesn't have this "multiple AI chat tabs" feature. I got fed up so I wrote my own script for the "Text Generator" plugin that parses a specialized note as a special chat note, where I could attach "context notes" and bounce off ideas with the AI using plain markdown, each in its own separate markdown tab in Obsidian. I'll share some of them below in case it would help others like me:

AI Chat.md ~~~md --- promptId: AI Chat name: ๐Ÿ“œ AI Chat description: Chat with the AI using a specialized note saved to your Obsidian vault! If the active note is not the specialized chat note, a new one will be created and opened. author: jasonsparc tags: version: 0.0.1 disableProvider: true commands: - generate mode: insert x.debug.log: true x.debug.noGen: true x.dir: "!ObsidianSetup/01 TextGen Chat/" --- {{#script}} ```js const workspace = app.workspace const vault = app.vault const activeView = workspace.getMostRecentLeaf()?.view const activeFile = activeView?.file if (activeFile?.extension != "md") return error("No active note.") const editor = activeView.editor if (!editor) return error("No active editor.") const MSG_HR_START = "โ”€โ”€โ•โ• " const MSG_HR_END = " โ•โ•โ”€โ”€" const MSG_HR_SPLITTER = /^โ”€โ”€โ•โ•[ \t]*(โœง*)[ \t]*โ•โ•โ”€โ”€[ \t]*(?:\r?\n|[\r\u2028\u2029]|$)/mu const MSG_HR_TEST = /^โ”€โ”€โ•โ•[ \t]*โœง*[ \t]*โ•โ•โ”€โ”€[ \t]*$/mu const USER_MARK = "โœงโœง" const AI_MARK = "โœง" const USER_HR = `${MSG_HR_START}${USER_MARK}${MSG_HR_END}` const AI_HR = `${MSG_HR_START}${AI_MARK}${MSG_HR_END}` const CHAT_SIGNATURE = "#ai-chat " const metadataCache = app.metadataCache const metadata = metadataCache.getFileCache(activeFile) if (this.newTab || !editor.getLine( (metadata?.frontmatterPosition?.end?.line ?? -1) + 1 ).startsWith(CHAT_SIGNATURE)) { const template = `${CHAT_SIGNATURE}#no-graph ${USER_MARK} : User ${AI_MARK} : AI # Context Notes ${await run("Suggest Context Notes", { noCursorMove: true, includeSelf: true })} # AI Chat History ${USER_HR} ` const chatsDir = PromptInfo["x.dir"] if (!chatsDir) return error('Template property not configured: "x.dir"') if (!vault.getAbstractFileByPath(chatsDir)) vault.createFolder(chatsDir) const timestamp = moment().format("YYYY-MM-DD HH-mm-ss") let filePath = `${chatsDir}/${timestamp}.md` for (let i = 1; vault.getAbstractFileByPath(filePath); i++) filePath = `${chatsDir}/${timestamp}-${i}.md` const newFile = await vault.create(filePath, template) const leaf = workspace.getLeaf(true) await leaf.openFile(newFile) workspace.setActiveLeaf(leaf) goToLastLine(leaf.view.editor) return "" // Done! Skip code below. } function goToLastLine(editor) { const lastLine_i = editor.lastLine() editor.setCursor(lastLine_i) editor.focus() return lastLine_i } const lastLine = editor.getLine(goToLastLine(editor)) let system = `You are an AI assistant provided by the "Text Generator" plugin for Obsidian. You engage in natural-sounding conversations and respond to a wide range of prompts and questions. Your responses should be informative, comprehensive, and tailored to the user's input. If you don't know an answer, admit it and suggest specific resources that might help, such as websites, books, research papers, or relevant Obsidian notes. Guidelines: 1. Follow the user's lead in conversation; don't change the subject unless asked. 2. Stay focused on the user's stated task or topic unless directed otherwise. 3. If a request is unclear, politely ask for clarification. 4. When providing multiple suggestions: a. Indicate your top recommendation(s), explaining your reasoning briefly. b. If there are multiple top choices or close seconds, acknowledge this and explain the trade-offs. c. Consider ranking or categorizing options if it adds value (e.g., "best for beginners," "most cost-effective"). 5. Always strive to follow given instructions. 6. Use provided Obsidian notes (if any) for context, acknowledging any discrepancies. 7. If no notes are provided or they're irrelevant, rely on your general knowledge. 8. Generally respond using Obsidian-flavored markdown. Your primary goal is to be honest, helpful, and assist the user in any way you can.` const children = metadata?.links ?.map(lc => metadataCache.getFirstLinkpathDest(lc.link, activeFile.path)) || [] const messages = [] const seenPaths = new Set([activeFile.path]) const CONTEXT_PATH = "path" const CONTEXT_TITLE = "title" const CONTEXT_NOTE = "CONTEXT NOTE" const CONTEXT_REPLY = 'โ€ฆ' let contextNotes_n = 0 for (const child of children) { if (!child) continue const path = child.path if (seenPaths.has(path)) continue seenPaths.add(path) const contextNote = `${CONTEXT_NOTE} ${contextNotes_n} ${CONTEXT_PATH}: ${path} ${CONTEXT_TITLE}: ${child.basename} ${await vault.read(child)}` messages.push(contextNote, CONTEXT_REPLY) contextNotes_n++ } if (contextNotes_n > 0) { const END_OF_CONTEXT_NOTES = "END OF CONTEXT NOTES" messages.push(END_OF_CONTEXT_NOTES, CONTEXT_REPLY) system += ` Obsidian Notes Context: - ${contextNotes_n} Obsidian notes will be provided as context. - Each note starts with the line "${CONTEXT_NOTE} N" (where N is an integer). - Respond only with "${CONTEXT_REPLY}" when you see a "${CONTEXT_NOTE} N" prompt. - Context notes include '${CONTEXT_PATH}' and '${CONTEXT_TITLE}' fields, followed by an empty line, then followed by the verbatim content of the note. - After all notes are provided, you'll receive an "${END_OF_CONTEXT_NOTES}" prompt. Respond only with "${CONTEXT_REPLY}" when you see this prompt. - Only then should you begin responding to user queries. These "${CONTEXT_NOTE} N" and "${END_OF_CONTEXT_NOTES}" prompts are provided by the system. The user may not be aware of these prompts and will simply assume that their first message to you is the user prompt after the "${END_OF_CONTEXT_NOTES}" prompt.` } const content = await vault.read(activeFile) const msgSlabs = content.split(MSG_HR_SPLITTER) for (let i = 1, N = msgSlabs.length, lastType = AI_MARK; i < N; i += 2) { const type = msgSlabs[i] if (type != USER_MARK && type != AI_MARK) return error(`Invalid chat mark: ${type}`) if (type == lastType) messages.push(' ') lastType = type messages.push(unindent1(msgSlabs[i + 1].trimEnd()) || ' ') } // Ensure odd count messages if (messages.length % 2 == 0) { if (messages.at(-1) == ' ') messages.pop() else messages.push(' ') } const genParams = { system, messages } if (PromptInfo["x.debug.log"]) console.log(genParams) const genOutput = PromptInfo["x.debug.noGen"] ? "DEMO RESPONSE" : (await this.gen(void 0, genParams)).trimEnd() this.vars.output = `${lastLine ? '\n' : ''}${AI_HR} ${MSG_HR_TEST.test(genOutput) ? indent1(genOutput) : genOutput } ${USER_HR} ` return "" // Done! function indent1(s) { return s.replaceAll(/^(?!$)/gm, ' ') // Just 1 space indentation } function unindent1(s) { const lines = s.split(/^/m) // Preserves line breaks for (let i = 0, N = lines.length; i < N; i++) { const line = lines[i] const m = line.match(/^(?:[ \t]|$)/m) // Just 1 whitespace indentation if (!m) return s // Some were not indented lines[i] = line.slice(m[0].length) } return lines.join('') } ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} *** {{#script}} ```js return this.vars.output || "" ``` {{/script}} ~~~
AI Chat in New Tab.md ~~~md --- promptId: AI Chat in New Tab name: ๐Ÿ“œ New AI Chat Tab description: Chat with the AI using a specialized note saved to your Obsidian vault! The specialized note will be created and opened in a new tab. author: jasonsparc tags: version: 0.0.1 disableProvider: true commands: - generate mode: insert --- {{#script}} ```js await run("AI Chat", { newTab: true }) return "" ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} ~~~
Suggest Context Notes.md ~~~md --- promptId: Suggest Context Notes name: ๐Ÿ—บ Suggest context notes description: Lists outgoing links in the active note and their respective outgoing links. author: jasonsparc tags: version: 0.0.1 disableProvider: true commands: - generate --- {{#script}} ```js const activeView = app.workspace.getMostRecentLeaf()?.view const activeFile = activeView?.file if (activeFile?.extension != "md") return error("No active note.") const metadataCache = app.metadataCache function getChildren(file) { if (file) { const children = metadataCache.getFileCache(file)?.links ?.map(lc => metadataCache.getFirstLinkpathDest(lc.link, file.path)) if (children) return children } return [] } const includeSelf = this.includeSelf const children = getChildren(activeFile) let output = Array.from( [...(includeSelf ? [activeFile] : []), ...children, ...children.flatMap(getChildren)] .reduce((set, file) => file?.extension == "md" && (includeSelf || file.path != activeFile.path) ? set.add(`[[${file.path.slice(0, -3)}]]\n`) : set, new Set()) ).join('') if (!this.noCursorMove) { const editor = activeView.editor if (editor) { const cursorLine_i = editor.getCursor().line if (editor.getLine(cursorLine_i)) { editor.setCursor(cursorLine_i) output = "\n" + output.slice(0, -1) } } } this.vars.output = output return "" ``` {{/script}} *** {{#script}} ```js return "" ``` {{/script}} *** {{#script}} ```js return this.vars.output || "" ``` {{/script}} ~~~

So far, I'm very happy with the results so far (even though the above may have bugs). I'm even thinking of uninstalling the SystemSculpt AI plugin because of that, but I feel like it would be helpful for others if I instead requested you to implement a similar feature for SystemSculpt AI.

Fun fact: I'm currently using Obsidian to write a GDD (Game Design Document) with the entire Obsidian vault itself serving as the GDD, and hence the need for me to open up multiple AI chat threads for diverse aspects of the game and GDD I'm working on.

SystemSculpt commented 3 months ago

Killer enhancement idea here, thanks for the insight - I love it and will definitely implement it. Will keep this open until it's implemented in the next release. Cheers!