diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index d81e07aee28..0f67634d180 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -52,7 +52,8 @@ exports.startup = function() { basicAuthUsername: params["basic-auth-username"], basicAuthUsernameFromStore: params["basic-auth-username-from-store"], basicAuthPassword: params["basic-auth-password"], - basicAuthPasswordFromStore: params["basic-auth-password-from-store"] + basicAuthPasswordFromStore: params["basic-auth-password-from-store"], + bearerAuthTokenFromStore: params["bearer-auth-token-from-store"] }); }); $tw.rootWidget.addEventListener("tm-http-cancel-all-requests",function(event) { diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 65bdfd1e526..f16f1c51274 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -104,6 +104,8 @@ basicAuthUsername: plain username for basic authentication basicAuthUsernameFromStore: name of password store entry containing username basicAuthPassword: plain password for basic authentication basicAuthPasswordFromStore: name of password store entry containing password +bearerAuthToken: plain text token for bearer authentication +bearerAuthTokenFromStore: name of password store entry contain bear authorization token */ function HttpClientRequest(options) { var self = this; @@ -135,8 +137,11 @@ function HttpClientRequest(options) { }); this.basicAuthUsername = options.basicAuthUsername || (options.basicAuthUsernameFromStore && $tw.utils.getPassword(options.basicAuthUsernameFromStore)) || ""; this.basicAuthPassword = options.basicAuthPassword || (options.basicAuthPasswordFromStore && $tw.utils.getPassword(options.basicAuthPasswordFromStore)) || ""; + this.bearerAuthToken = options.bearerAuthToken || (options.bearerAuthTokenFromStore && $tw.utils.getPassword(options.bearerAuthTokenFromStore)) || ""; if(this.basicAuthUsername && this.basicAuthPassword) { this.requestHeaders.Authorization = "Basic " + $tw.utils.base64Encode(this.basicAuthUsername + ":" + this.basicAuthPassword); + } else if(this.bearerAuthToken) { + this.requestHeaders.Authorization = "Bearer " + this.bearerAuthToken; } } diff --git a/core/modules/widgets/action-createtiddler.js b/core/modules/widgets/action-createtiddler.js index b49eaad2059..4b883d0c34a 100644 --- a/core/modules/widgets/action-createtiddler.js +++ b/core/modules/widgets/action-createtiddler.js @@ -104,7 +104,7 @@ CreateTiddlerWidget.prototype.invokeAction = function(triggeringWidget,event) { } this.setVariable("createTiddler-title",title); this.setVariable("createTiddler-draftTitle",draftTitle); - this.refreshChildren(); + this.refreshChildren([]); return true; // Action was invoked }; diff --git a/core/modules/widgets/importvariables.js b/core/modules/widgets/importvariables.js index 3e1ac3fc6fc..e394853a83f 100644 --- a/core/modules/widgets/importvariables.js +++ b/core/modules/widgets/importvariables.js @@ -107,6 +107,7 @@ ImportVariablesWidget.prototype.execute = function(tiddlerList) { Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering */ ImportVariablesWidget.prototype.refresh = function(changedTiddlers) { + changedTiddlers = changedTiddlers || {}; // Recompute our attributes and the filter list var changedAttributes = this.computeAttributes(), tiddlerList = this.wiki.filterTiddlers(this.getAttribute("filter"),this); diff --git a/editions/prerelease/tiddlers/system/DefaultTiddlers.tid b/editions/prerelease/tiddlers/system/DefaultTiddlers.tid index c947fd59a2f..8894ab50bc4 100644 --- a/editions/prerelease/tiddlers/system/DefaultTiddlers.tid +++ b/editions/prerelease/tiddlers/system/DefaultTiddlers.tid @@ -2,7 +2,4 @@ created: 20131127215321439 modified: 20140912135951542 title: $:/DefaultTiddlers -[[TiddlyWiki Pre-release]] -HelloThere -GettingStarted -Community +$:/plugins/tiddlywiki/ai-tools \ No newline at end of file diff --git a/editions/prerelease/tiddlywiki.info b/editions/prerelease/tiddlywiki.info index c469dcf996a..6e242822155 100644 --- a/editions/prerelease/tiddlywiki.info +++ b/editions/prerelease/tiddlywiki.info @@ -17,7 +17,9 @@ "tiddlywiki/jszip", "tiddlywiki/confetti", "tiddlywiki/dynannotate", - "tiddlywiki/tour" + "tiddlywiki/tour", + "tiddlywiki/markdown", + "tiddlywiki/ai-tools" ], "themes": [ "tiddlywiki/vanilla", diff --git a/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid b/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid index e10c566b9d7..cf4e7ba34f2 100644 --- a/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid +++ b/editions/tw5.com/tiddlers/system/DefaultTiddlers.tid @@ -3,6 +3,4 @@ modified: 20140912135951542 title: $:/DefaultTiddlers type: text/vnd.tiddlywiki -HelloThere -GettingStarted -Community +$:/plugins/tiddlywiki/ai-tools \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/system/SiteTitle.tid b/editions/tw5.com/tiddlers/system/SiteTitle.tid index c407957d69b..b198a938868 100644 --- a/editions/tw5.com/tiddlers/system/SiteTitle.tid +++ b/editions/tw5.com/tiddlers/system/SiteTitle.tid @@ -3,4 +3,4 @@ modified: 20131211131023829 title: $:/SiteTitle type: text/vnd.tiddlywiki -TiddlyWiki @@font-size:small; v<>@@ \ No newline at end of file +TiddlyWiki AI Tools Plugin diff --git a/editions/tw5.com/tiddlywiki.info b/editions/tw5.com/tiddlywiki.info index 2f3ddade89c..80f4ac3d42d 100644 --- a/editions/tw5.com/tiddlywiki.info +++ b/editions/tw5.com/tiddlywiki.info @@ -7,7 +7,9 @@ "tiddlywiki/menubar", "tiddlywiki/confetti", "tiddlywiki/dynannotate", - "tiddlywiki/tour" + "tiddlywiki/tour", + "tiddlywiki/qrcode", + "tiddlywiki/ai-tools" ], "themes": [ "tiddlywiki/vanilla", diff --git a/plugins/tiddlywiki/ai-tools/docs.tid b/plugins/tiddlywiki/ai-tools/docs.tid new file mode 100644 index 00000000000..a000cf90536 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/docs.tid @@ -0,0 +1,53 @@ +title: $:/plugins/tiddlywiki/ai-tools/docs + +!! Setting Up + +See the ''settings'' tab for set up instructions. + +!! Live AI Conversations in ~TiddlyWiki + +# Click the {{||$:/plugins/tiddlywiki/ai-tools/page-menu}} icon in the sidebar to open a new conversation +# Choose the server from the dropdown: +#* ''Locally running Llamafile server'' +#* ''~OpenAI Service'' (requires API key to be specified in ''settings'') +# Type a prompt for the LLM in the text box +#* If using ~OpenAI it is possible to attach a single image to a prompt +# Click "Send" and wait for the output of the LLM + +!! Import ~ChatGPT Conversation Archives + +# [[Follow the instructions|https://help.openai.com/en/articles/7260999-how-do-i-export-my-chatgpt-history-and-data]] to request an export of your ~ChatGPT data +# You will receive a link to download your data as a ZIP file +# Download and unzip the file +# Locate the file `conversations.json` within the archive and import it into your TiddlyWiki +# Visit the ''tools'' tab and locate your `conversations.json` tiddler +# Click the associated ''import'' button +# See the imported conversations listed in the ''tools'' tab +# The imported tiddler `conversations.json` is no longer required and can be deleted + +!! Conversation Format + +This plugin defines a simple schema for representing conversations with an LLM. + +In a nutshell, tiddlers tagged <> define conversations. The individual messages are tiddlers that are tagged with the title of the conversation tiddler. + +Currently, the ordering of the messages is determined by the value of their "created" field. The ordering defined by the tag mechanism is ignored. It is intended to change this behaviour so that the ordering of messages is defined by the tag mechanism. + +The fields with defined meanings for conversation tiddlers are: + +|!Field |!Description | +|''system-prompt'' |Defines the system prompt for the conversation | +|''tags'' |Must include <> | +|''text'' |Optional description or notes displayed at the top of the conversation | +|''current-response-image'' |Optional title of an image tiddler to be attached to the current user response | +|''current-response-text'' |Text of the current user response before it is sent | + +The fields with defined meanings for conversation tiddlers are: + +|!Field |!Description | +|''created'' |Creation date of the message (currently used for ordering) | +|''image'' |Optional image associated with this message | +|''role'' |Possible values include ''user'' and ''assistant'' | +|''tags'' |Must include the title of the parent conversation | +|''type'' |Typically ''text/markdown'' | + diff --git a/plugins/tiddlywiki/ai-tools/globals.tid b/plugins/tiddlywiki/ai-tools/globals.tid new file mode 100644 index 00000000000..e6436913d57 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/globals.tid @@ -0,0 +1,219 @@ +title: $:/plugins/tiddlywiki/ai-tools/globals +tags: $:/tags/Global + +\function ai-tools-default-llm-completion-server() +[all[shadows+tiddlers]tag[$:/tags/AI/CompletionServer]sort[caption]first[]] +\end + + +\procedure ai-tools-get-llm-completion(conversationTitle,resultTitlePrefix,resultTags,ai-tools-status-title,completionServer) +<$let + completionServer={{{ [!is[blank]else] }}} +> + <$importvariables filter="[]"> + <$wikify name="json" text=<>> + <$action-log message="ai-tools-get-llm-completion"/> + <$action-log/> + <$action-sendmessage + $message="tm-http-request" + url={{{ [get[url]] }}} + body=<> + header-content-type="application/json" + bearer-auth-token-from-store="openai-secret-key" + method="POST" + oncompletion=<> + bind-status=<> + var-resultTitlePrefix=<> + var-resultTags=<> + /> + + + +\end ai-tools-get-llm-completion + + +\function ai-tools-status-title() +[addprefix[$:/temp/ai-tools/status/]] +\end ai-tools-status-title + + +\procedure ai-tools-message(tiddler,field,role,makeLink:"yes") +<$qualify + name="state" + title={{{ [[$:/state/ai-tools-message-state/]addsuffix] }}} +> + <$let + editStateTiddler={{{ [addsuffix[-edit-state]] }}} + editState={{{ [get[text]else[view]] }}} + > +
addprefix[ai-tools-message-role-]] +[join[ ]] }}}> +
+
+ <$genesis $type={{{ [match[yes]then[$link]else[span]] }}} to=<>> + <$text text=<>/> + +
+
+ <%if [!match[edit]] %> + <$button class="ai-tools-message-toolbar-button"> + <$action-setfield $tiddler=<> text="edit"/> + edit + + <%endif%> + <%if [!match[view]] %> + <$button class="ai-tools-message-toolbar-button"> + <$action-setfield $tiddler=<> text="view"/> + view + + <%endif%> + <$button class="ai-tools-message-toolbar-button"> + <$action-sendmessage $message="tm-copy-to-clipboard" $param={{{ [getelse[]] }}}/> + copy + + <$button class="ai-tools-message-toolbar-button"> + <$action-deletetiddler $tiddler=<>/> + delete + +
+
+
+ <%if [match[view]] %> + <$transclude $tiddler=<> $field=<> $mode="block"/> + <%else%> + <$edit-text tiddler=<> field=<> tag="textarea" class="tc-edit-texteditor"/> + <%endif%> + <%if [get[image]else[]!match[]] %> + <$image source={{{ [get[image]] }}}/> + <%endif%> +
+
+ + +\end ai-tools-message + + +\procedure ai-tools-action-get-response() +<$let + resultTitlePrefix={{{ [addsuffix[ - Prompt]] }}} + resultTags={{{ [format:titlelist[]] }}} +> + <$action-createtiddler + $basetitle=<> + tags=<> + type="text/markdown" + role="user" + text={{!!current-response-text}} + image={{!!current-response-image}} + > + <$action-deletefield $tiddler=<> $field="current-response-text"/> + <$action-deletefield $tiddler=<> $field="current-response-image"/> + <$transclude + $variable="ai-tools-get-llm-completion" + conversationTitle=<> + completionServer={{!!completion-server}} + resultTitlePrefix=<> + resultTags=<> + ai-tools-status-title=<> + /> + + +\end ai-tools-action-get-response + +\procedure ai-tools-conversation(conversationTitle) +<$let currentTiddler=<>> + + Server: <$select tiddler=<> field="completion-server" default=<>> + <$list filter="[all[shadows+tiddlers]tag[$:/tags/AI/CompletionServer]sort[caption]]"> + + + + +
+ <$transclude + $variable="ai-tools-message" + tiddler=<> + field="system-prompt" + role="system" + makeLink="no" + /> + <$list filter="[all[shadows+tiddlers]tag!is[draft]sort[created]]" variable="message" storyview="pop"> + <$transclude + $variable="ai-tools-message" + tiddler=<> + field="text" + role={{{ [get[role]] }}} + /> + + <%if [get[text]else[complete]match[pending]] %> +
+
+
+ <%endif%> +
+
+ <$edit-text tiddler=<> field="current-response-text" tag="textarea" class="tc-edit-texteditor"/> + <$button + class="ai-user-prompt-send" + actions=<> + disabled={{{ [get[text]else[complete]match[pending]then[yes]] [get[current-response-text]else[]match[]then[yes]] ~[[no]] }}} + > + Send + +
+
+
+ <$let state=<>> + <$button popup=<> class="tc-btn-invisible tc-btn-dropdown">Choose an image {{$:/core/images/down-arrow}} + <$link to={{!!current-response-image}}> + <$text text={{!!current-response-image}}/> + + <$reveal state=<> type="popup" position="belowleft" text="" default="" class="tc-popup-keep"> +
+ <$transclude + $variable="image-picker" + filter="[all[shadows+tiddlers]is[image]is[binary]!has[_canonical_uri]] -[type[application/pdf]] +[!has[draft.of]sort[title]]" + actions=""" + <$action-setfield + $tiddler=<> + current-response-image=<> + /> + <$action-deletetiddler $tiddler=<>/> + """ + /> +
+ + + <$image source={{!!current-response-image}}/> +
+
+
+
+ +\end ai-tools-conversation + +\procedure ai-tools-new-conversation() +<$action-createtiddler + $basetitle="AI Conversation" + tags="$:/tags/AI/Conversation" + system-prompt="Transcript of a never ending dialog, where the User interacts with an Assistant. The Assistant is helpful, kind, honest, good at writing, and never fails to answer the User's requests immediately and with precision." + current-response-text="Please list the 10 most important mechanical inventions of the Twentieth Century" +> +<$action-navigate $to=<>/> + +\end ai-tools-new-conversation + +\procedure ai-tools-import-conversations() +<$action-navigate $to="$:/plugins/tiddlywiki/ai-tools/tools"/> +\end ai-tools-import-conversations diff --git a/plugins/tiddlywiki/ai-tools/icon.tid b/plugins/tiddlywiki/ai-tools/icon.tid new file mode 100644 index 00000000000..3fdc090b555 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/icon.tid @@ -0,0 +1,5 @@ +title: $:/plugins/tiddlywiki/ai-tools/icon +tags: $:/tags/Image + + + \ No newline at end of file diff --git a/plugins/tiddlywiki/ai-tools/modules/conversations-archive-importer.js b/plugins/tiddlywiki/ai-tools/modules/conversations-archive-importer.js new file mode 100644 index 00000000000..22b2cb1bdc0 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/modules/conversations-archive-importer.js @@ -0,0 +1,87 @@ +/*\ +title: $:/plugins/tiddlywiki/ai-tools/modules/conversations-archive-importer.js +type: application/javascript +module-type: library + +Conversations archive importer + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +function ConversationsArchiveImporter() { +} + +ConversationsArchiveImporter.prototype.import = function(widget,conversationsTitle) { + var logger = new $tw.utils.Logger("ai-tools"); + var jsonConversations = widget.wiki.getTiddlerData(conversationsTitle,[]); + var tiddlers = []; + $tw.utils.each(jsonConversations,function(jsonConversation) { + var conversationTitle = (jsonConversation.title || "Untitled") + " (" + jsonConversation.conversation_id + ")", + conversationCreated = convertDate(jsonConversation.create_time), + conversationModified = convertDate(jsonConversation.update_time); + var conversationFields = { + title: conversationTitle, + tags: $tw.utils.stringifyList(["$:/tags/AI/Conversation"]), + created: conversationCreated, + modified: conversationModified + }; + tiddlers.push(conversationFields); + var messageIndex = 1; + $tw.utils.each(jsonConversation.mapping,function(jsonMessage,messageId) { + // Skip messages where "message" is null + if(jsonMessage.message) { + var messageFields = { + title: conversationTitle + " " + (messageIndex + 1), + created: convertDate(jsonMessage.message.create_time) || conversationCreated, + modified: convertDate(jsonMessage.message.update_time) || conversationModified, + tags: $tw.utils.stringifyList([conversationTitle]), + role: jsonMessage.message.author.role, + "message-type": jsonMessage.message.content.content_type + } + switch(jsonMessage.message.content.content_type) { + case "code": + messageFields.text = jsonMessage.message.content.text; + messageFields.type = "text/plain"; + break; + case "execution_output": + messageFields.text = jsonMessage.message.content.text; + messageFields.type = "text/plain"; + break; + case "system_error": + messageFields.text = jsonMessage.message.content.text; + messageFields.type = "text/plain"; + break; + case "text": + messageFields.text = jsonMessage.message.content.parts.join(""); + messageFields.type = "text/markdown"; + break; + default: + messageFields.text = JSON.stringify(jsonMessage.message,null,4); + messageFields.type = "text/plain"; + break; + } + tiddlers.push(messageFields); + messageIndex += 1; + } + }); + }); + // Create summary tiddler + $tw.utils.each(tiddlers,function(tidder) { + + }); + // Create the tiddlers + widget.wiki.addTiddlers(tiddlers); + // widget.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlers)}); +}; + +function convertDate(unixTimestamp) { + return $tw.utils.stringifyDate(new Date(unixTimestamp * 1000)); +} + +exports.ConversationsArchiveImporter = ConversationsArchiveImporter; + +})(); diff --git a/plugins/tiddlywiki/ai-tools/modules/startup.js b/plugins/tiddlywiki/ai-tools/modules/startup.js new file mode 100644 index 00000000000..247e06dfd4d --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/modules/startup.js @@ -0,0 +1,30 @@ +/*\ +title: $:/plugins/tiddlywiki/ai-tools/modules/startup.js +type: application/javascript +module-type: startup + +Setup the root widget event handlers + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +// Export name and synchronous status +exports.name = "ai-tools"; +exports.platforms = ["browser"]; +exports.after = ["startup"]; +exports.synchronous = true; + +// Install the root widget event handlers +exports.startup = function() { + var ConversationsArchiveImporter = require("$:/plugins/tiddlywiki/ai-tools/modules/conversations-archive-importer.js").ConversationsArchiveImporter; + $tw.conversationsArchiveImporter = new ConversationsArchiveImporter(); + $tw.rootWidget.addEventListener("tm-import-conversations",function(event) { + $tw.conversationsArchiveImporter.import(event.widget,event.param); + }); +}; + +})(); diff --git a/plugins/tiddlywiki/ai-tools/page-menu.tid b/plugins/tiddlywiki/ai-tools/page-menu.tid new file mode 100644 index 00000000000..779d01dc74f --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/page-menu.tid @@ -0,0 +1,21 @@ +title: $:/plugins/tiddlywiki/ai-tools/page-menu +tags: $:/tags/PageControls +caption: {{$:/plugins/tiddlywiki/ai-tools/icon}} AI Tools +description: Tools for interactive AI services + +\whitespace trim +<$button popup=<> tooltip="Tools for interactive AI services" aria-label="AI Tools" class=<> selectedClass="tc-selected"> + <%if [match[yes]] %> + {{$:/plugins/tiddlywiki/ai-tools/icon}} + <%endif%> + <%if [match[yes]] %> + <$text text="AI Tools"/> + <%endif%> + +<$reveal state=<> type="popup" position="belowleft" animate="yes"> +
+ <$list filter="[all[shadows+tiddlers]tag[$:/tags/AI/PageMenu]!has[draft.of]]" variable="listItem"> + <$transclude tiddler=<>/> + +
+ diff --git a/plugins/tiddlywiki/ai-tools/page-menu/import-chatgpt.tid b/plugins/tiddlywiki/ai-tools/page-menu/import-chatgpt.tid new file mode 100644 index 00000000000..fbbea8a01bd --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/page-menu/import-chatgpt.tid @@ -0,0 +1,6 @@ +title: $:/plugins/tiddlywiki/ai-tools/page-menu/import-chatgpt +tags: $:/tags/AI/PageMenu + +<$button actions=<> class="tc-btn-invisible"> +{{$:/core/images/input-button}} Import Conversations from ~ChatGPT + diff --git a/plugins/tiddlywiki/ai-tools/page-menu/new-conversation.tid b/plugins/tiddlywiki/ai-tools/page-menu/new-conversation.tid new file mode 100644 index 00000000000..5e7f3117352 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/page-menu/new-conversation.tid @@ -0,0 +1,6 @@ +title: $:/plugins/tiddlywiki/ai-tools/page-menu/new-conversation +tags: $:/tags/AI/PageMenu + +<$button actions=<> class="tc-btn-invisible"> +{{$:/core/images/new-button}} New Conversation + diff --git a/plugins/tiddlywiki/ai-tools/plugin.info b/plugins/tiddlywiki/ai-tools/plugin.info new file mode 100644 index 00000000000..77db7165bb3 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/plugin.info @@ -0,0 +1,7 @@ +{ + "title": "$:/plugins/tiddlywiki/ai-tools", + "name": "AI Tools", + "description": "AI Tools for TiddlyWiki", + "list": "readme docs tools settings", + "stability": "STABILITY_1_EXPERIMENTAL" +} diff --git a/plugins/tiddlywiki/ai-tools/readme.tid b/plugins/tiddlywiki/ai-tools/readme.tid new file mode 100644 index 00000000000..747d03a6d7c --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/readme.tid @@ -0,0 +1,12 @@ +title: $:/plugins/tiddlywiki/ai-tools/readme + +!! AI Tools for TiddlyWiki + +This plugin adds integrated LLM conversations to the TiddlyWiki platform. + +The plugin allows live conversations within TiddlyWiki and also importing ~ChatGPT conversation archives. + +The plugin supports two options for the LLM server: + +* ''Locally running Llamafile server'' - LLlamafile is an open source project that lets you distribute and run LLMs as a single file. The files are large, typically 4+ gigabytes but offer reasonable performance on modern hardware, and total privacy +* ''~OpenAI Service'' - ~OpenAI is a commercial service that offers paid APIs for accessing some of the most sophisticated LLMs that are available. ~OpenAI requires tokens to be purchased for API usage (this is entirely separate from ~ChatGPT subscriptions) diff --git a/plugins/tiddlywiki/ai-tools/servers/llamafile-llava.tid b/plugins/tiddlywiki/ai-tools/servers/llamafile-llava.tid new file mode 100644 index 00000000000..1334bea8695 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/servers/llamafile-llava.tid @@ -0,0 +1,85 @@ +title: $:/plugins/tiddlywiki/ai-tools/servers/llamafile-llava +tags: $:/tags/AI/CompletionServer +url: http://127.0.0.1:8080/completion +caption: Local Llamafile server running LLaVA models + + +\procedure json-prompt() +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline html conditional commentblock commentinline +{ + "image_data": [ + <$list filter="[all[shadows+tiddlers]tag!is[draft]sort[created]]" counter="counter"> + <%if [has[image]] %> + <%if [!match[yes]] %>,<%endif%> + { + "id": <$text text=<>/>, + "data": "<$text text={{{ [get[image]get[text]jsonstringify[]] +[join[]] }}}/>" + } + <%endif%> + + ], + "prompt": "<>", + "cache_prompt": false, + "frequency_penalty": 0, + "grammar": "", + "mirostat_eta": 0.1, + "mirostat_tau": 5, + "mirostat": 0, + "n_predict": 400, + "n_probs": 0, + "presence_penalty": 0, + "repeat_last_n": 256, + "repeat_penalty": 1.18, + "slot_id": -1, + "stop": ["", "Llama:", "User:"], + "stream" : false, + "temperature": 0.7, + "tfs_z": 1, + "top_k": 40, + "top_p": 0.5, + "typical_p": 1 +} +\end json-prompt + +\procedure json-prompt-text() +\whitespace trim +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline html conditional commentblock commentinline +<$text text={{{ [get[system-prompt]jsonstringify[]] }}}/> +\n +<$list filter="[all[shadows+tiddlers]tag!is[draft]sort[created]]" counter="counter"> +\n +<$text text={{{ [get[role]else[user]match[user]then[User:]else[Llama:]] }}}/> +<%if [has[image]] %> +[img-<$text text=<>/>] +<%endif%> +<$text text={{{ [get[text]jsonstringify[]] }}}/> + +\nLlama: +\end json-prompt-text + + +\procedure completion-callback() + <%if [compare:number:gteq[200]compare:number:lteq[299]] %> + + <$action-createtiddler + $basetitle=<> + tags=<> + type="text/markdown" + role="assistant" + text={{{ [jsonget[content]] }}} + /> + <%else%> + + <$action-createtiddler + $basetitle=<> + tags=<> + type="text/markdown" + role="error" + text={{{ [[Error:]] [] [jsonget[error],[message]] +[join[]] }}} + /> + <%endif%> +\end completion-callback diff --git a/plugins/tiddlywiki/ai-tools/servers/openai.tid b/plugins/tiddlywiki/ai-tools/servers/openai.tid new file mode 100644 index 00000000000..65ee9429a20 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/servers/openai.tid @@ -0,0 +1,69 @@ +title: $:/plugins/tiddlywiki/ai-tools/servers/openai +tags: $:/tags/AI/CompletionServer +url: https://api.openai.com/v1/chat/completions +caption: OpenAI Service + + +\procedure json-prompt() +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline html conditional commentblock commentinline +{ + "model": "gpt-4o", + "messages": [ + { + "role": "system", + "content": "<$text text={{{ [get[system-prompt]jsonstringify[]] }}}/>" + } + + <$list filter="[all[shadows+tiddlers]tag!is[draft]sort[created]]"> + , + { + + "role": "<$text text={{{ [get[role]jsonstringify[]] }}}/>", + "content": [ + { + "type": "text", + "text": "<$text text={{{ [get[text]jsonstringify[]] }}}/>" + } + <%if [get[image]else[]!match[]] %> + , + { + "type": "image_url", + "image_url": { + "url": "<$text text={{{ [[data:]] [get[image]get[type]] [[;base64,]] [get[image]get[text]jsonstringify[]] +[join[]] }}}/>" + } + } + <%endif%> + ] + + } + + ] +} +\end json-prompt + + +\procedure completion-callback() + <%if [compare:number:gteq[200]compare:number:lteq[299]] %> + + <$action-createtiddler + $basetitle=<> + tags=<> + type="text/markdown" + role={{{ [jsonget[choices],[0],[message],[role]] }}} + text={{{ [jsonget[choices],[0],[message],[content]] }}} + /> + <%else%> + + <$action-createtiddler + $basetitle=<> + tags=<> + type="text/markdown" + role="error" + text={{{ [[Error:]] [] [jsonget[error],[message]] +[join[]] }}} + /> + <%endif%> +\end completion-callback diff --git a/plugins/tiddlywiki/ai-tools/settings.tid b/plugins/tiddlywiki/ai-tools/settings.tid new file mode 100644 index 00000000000..8f2567560e8 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/settings.tid @@ -0,0 +1,23 @@ +title: $:/plugins/tiddlywiki/ai-tools/settings + +! AI Tools Settings + +!! ~OpenAI API key + +This plugin runs entirely in the browser, with no backend server component. A consequence of this design is that the API keys required to access external services must be obtained by the end user. These keys are stored in the browser and so only need to be set up once. + +# Register for an account at https://platform.openai.com/ +#* Newly registered accounts can claim a small amount of credit, thereafter payment is needed +#* Note that ~OpenAI run completely different payment systems for ~ChatGPT and the API platform. Even if you are already a subscriber to ~ChatGPT you will still need to pay for API usage after the initial free service +# Visit https://platform.openai.com/api-keys to create a new secret API key +# Copy and paste the value into the box below + +~OpenAI Secret API Key: <$password name="openai-secret-key"/> + +!! Llamafile Setup + +[[Llamafile|https://github.com/Mozilla-Ocho/llamafile]] lets you download and run LLMs as a single file. See the [[announcment blog post|https://hacks.mozilla.org/2023/11/introducing-llamafile/]] for background. + +# Download and run Llamafile as [[described in the QuickStart guide|https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart]] +# Visit http://127.0.0.1:8080 in a browser and verify that you can see the Llamafile interface. You can also try it out here +# Return to AI Tools and start a conversation, specifying Llamafile as the server diff --git a/plugins/tiddlywiki/ai-tools/styles.tid b/plugins/tiddlywiki/ai-tools/styles.tid new file mode 100644 index 00000000000..a477c4fb1f9 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/styles.tid @@ -0,0 +1,225 @@ +title: $:/plugins/tiddlywiki/ai-tools/styles +tags: [[$:/tags/Stylesheet]] + +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline + +.ai-conversation { + background: #f0eeff; + border-radius: 2em; + padding: 1em 1em; + display: flex; + flex-direction: column; + gap: 1em; + box-shadow: 2px 2px 5px rgba(0,0,0,0.2); +} + +.ai-conversation .ai-tools-message { + box-shadow: 2px 2px 5px rgba(0,0,0,0.2); + border-radius: 1em; + display: flex; + flex-direction: column; +} + +.ai-conversation .ai-tools-message .ai-tools-message-toolbar { + background: rgba(1,1,1,0.35); + color: white; + padding: 0.25em 1em 0.25em 1em; + border-top-left-radius: 1em; + border-top-right-radius: 1em; + display: flex; + justify-content: space-between; +} + +.ai-conversation .ai-tools-message .ai-tools-message-toolbar .tc-tiddlylink { + color: inherit; +} + +.ai-conversation .ai-tools-message .ai-tools-message-toolbar .ai-tools-message-toolbar-button { + background: rgba(255,255,255,0.35); + color: #333333; + cursor: pointer; + display: inline-block; + outline: 0; + overflow: hidden; + pointer-events: auto; + position: relative; + text-align: center; + touch-action: manipulation; + user-select: none; + -webkit-user-select: none; + vertical-align: top; + white-space: nowrap; + border: 0; + border-radius: 4px; +} + +.ai-conversation .ai-tools-message .ai-tools-message-toolbar .ai-tools-message-toolbar-button:hover { + color: #ffffff; + background: rgba(255,255,255,0.55); + +} + +.ai-conversation .ai-tools-message .ai-tools-message-body { + padding: 0 1em 0 1em +} + +.ai-conversation .ai-tools-message.ai-tools-message-role-system { + width: 60%; + background: #4c4c80; + color: white; +} + +.ai-conversation .ai-tools-message.ai-tools-message-role-user { + width: 60%; + margin-left: auto; + background: #ffcde0; +} + +.ai-conversation .ai-tools-message.ai-tools-message-role-assistant { + background: #dfd; +} + +.ai-conversation .ai-tools-message.ai-tools-message-role-error { + background: #fdd; +} + +.ai-conversation .ai-user-prompt { + padding: 1em; + background: #ffcde0; + border-radius: 1em; + box-shadow: inset 3px 4px 2px rgba(0, 0, 0, 0.1); +} + +.ai-conversation .ai-user-prompt button svg.tc-image-button { + fill: #000; +} + +.ai-conversation .ai-user-prompt-text { + display: flex; + align-items: flex-start; + gap: 1em; +} + +.ai-conversation .ai-user-prompt-text textarea { + color: #000; + background: #fff; +} + +.ai-conversation .ai-user-prompt button.ai-user-prompt-send { + background-color: initial; + background-image: linear-gradient(-180deg, #e0c3ce, #963057); + border-radius: 1em; + box-shadow: rgba(0, 0, 0, 0.1) 0 2px 4px; + color: #FFFFFF; + cursor: pointer; + display: inline-block; + outline: 0; + overflow: hidden; + padding: 0 20px; + pointer-events: auto; + position: relative; + text-align: center; + touch-action: manipulation; + user-select: none; + -webkit-user-select: none; + vertical-align: top; + white-space: nowrap; + border: 0; + transition: box-shadow .2s; + line-height: 2; +} + +.ai-conversation .ai-user-prompt button.ai-user-prompt-send:hover:not(:disabled) { + box-shadow: rgb(255 62 135 / 64%) 0 3px 8px; +} + +.ai-conversation .ai-user-prompt button.ai-user-prompt-send:disabled { + background: #ddd; + color: #444; +} + +.ai-conversation .ai-user-prompt textarea { + margin: 0; +} + +.ai-conversation .ai-user-prompt .ai-user-prompt-image button { + color: #000; + fill: #000; +} + +.ai-request-spinner { + animation: ai-request-spinner-animation-rotate 1s infinite; + height: 50px; + width: 50px; + margin-left: auto; + margin-right: auto; +} + +.ai-request-spinner:before, +.ai-request-spinner:after { + border-radius: 50%; + content: ""; + display: block; + height: 20px; + width: 20px; +} + +.ai-request-spinner:before { + animation: ai-request-spinner-animation-ball1 1s infinite; + background-color: #9c9ab0; + box-shadow: 30px 0 0 #fefdff; + margin-bottom: 10px; +} + +.ai-request-spinner:after { + animation: ai-request-spinner-animation-ball2 1s infinite; + background-color: #fefdff; + box-shadow: 30px 0 0 #9c9ab0; +} + +@keyframes ai-request-spinner-animation-rotate { + 0% { transform: rotate(0deg) scale(0.8) } + 50% { transform: rotate(360deg) scale(1.2) } + 100% { transform: rotate(720deg) scale(0.8) } +} + +@keyframes ai-request-spinner-animation-ball1 { + 0% { + box-shadow: 30px 0 0 #fefdff; + } + 50% { + box-shadow: 0 0 0 #fefdff; + margin-bottom: 0; + transform: translate(15px, 15px); + } + 100% { + box-shadow: 30px 0 0 #fefdff; + margin-bottom: 10px; + } +} + +@keyframes ai-request-spinner-animation-ball2 { + 0% { + box-shadow: 30px 0 0 #9c9ab0; + } + 50% { + box-shadow: 0 0 0 #9c9ab0; + margin-top: -20px; + transform: translate(15px, 15px); + } + 100% { + box-shadow: 30px 0 0 #9c9ab0; + margin-top: 0; + } +} + +.tc-ai-tools-dropzone { + background: yellow; + text-align: center; + width: 100%; + height: 4em; +} + +.tc-ai-tools-dropzone.tc-dragover { + background: red; +} \ No newline at end of file diff --git a/plugins/tiddlywiki/ai-tools/tools.tid b/plugins/tiddlywiki/ai-tools/tools.tid new file mode 100644 index 00000000000..f1bd9ee49c4 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/tools.tid @@ -0,0 +1,42 @@ +title: $:/plugins/tiddlywiki/ai-tools/tools + +! Import ~ChatGPT Export Archive + +These instructions allow you to import the conversations from a ~ChatGPT export archive. + +!! 1- Request your archive + +Visit the ~ChatGPT site to request your achive. You will be sent an email with a link to a ZIP file. Download the file and locate the file `conversations.json` within it. + +!! 2 - Import `conversations.json` as a tiddler + +It is not possible to use the normal import process to import the `conversations.json` file because TiddlyWiki will erroneously recognise it as a JSON file of tiddlers. + +Instead, drag the `conversations.json` file to the dropzone below. Then click the "Import" button to complete the import: + +<$dropzone deserializer="text/plain" autoOpenOnImport="yes" filesOnly="yes" class="tc-ai-tools-dropzone"> +Drop your file here + + +!! 3 - Import the conversations within it + +Any tiddlers containing ~ChatGPT exported `conversation.json` files will be shown here for import. + +<$list filter="[all[tiddlers+shadows]type[application/json]!has[plugin-type]sort[title]]" template="$:/plugins/tiddlywiki/ai-tools/view-templates/imported-conversations-json"/> + +!! 4 - Review Loaded Conversations + +
    + <$list filter="[all[tiddlers+shadows]tag[$:/tags/AI/Conversation]sort[title]]"> + <$list-empty> + No conversations found + + <$list-template> +
  • + <$link> + <$text text=<>/> + +
  • + + +
diff --git a/plugins/tiddlywiki/ai-tools/view-templates/conversation.tid b/plugins/tiddlywiki/ai-tools/view-templates/conversation.tid new file mode 100644 index 00000000000..db672b1910d --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/view-templates/conversation.tid @@ -0,0 +1,14 @@ +title: $:/plugins/tiddlywiki/ai-tools/view-templates/conversation +tags: $:/tags/ViewTemplate +list-after: $:/core/ui/ViewTemplate/body + +<%if [tag[$:/tags/AI/Conversation]] %> + +<$transclude + $variable="ai-tools-conversation" + $mode="block" + conversationTitle=<> +/> + +<%endif%> + diff --git a/plugins/tiddlywiki/ai-tools/view-templates/imported-conversations-json.tid b/plugins/tiddlywiki/ai-tools/view-templates/imported-conversations-json.tid new file mode 100644 index 00000000000..f7556c599b5 --- /dev/null +++ b/plugins/tiddlywiki/ai-tools/view-templates/imported-conversations-json.tid @@ -0,0 +1,45 @@ +title: $:/plugins/tiddlywiki/ai-tools/view-templates/imported-conversations-json +tags: $:/tags/ViewTemplate +list-before: $:/core/ui/ViewTemplate/body + +\whitespace trim + +\procedure importer() +

+

+ <$link> + <$text text=`$(currentTiddler)$ appears to be a ChatGPT export containing $(numberOfConversations)$ conversations`/> + +
+
+ <$button> + <$action-sendmessage $message="tm-import-conversations" $param=<>/> + {{$:/core/images/input-button}} Import + +
+

+\end importer + +<%if [type[application/json]] %> + <$let json={{{ [get[text]] }}} > + <%if [jsontype[]match[array]] %> + <$let + numberOfConversations={{{ [jsonindexes[]count[]] }}} + json={{{ [jsonextract[0]] }}} + > + <%if [jsontype[]match[object]] %> + <%if + [jsontype[title]match[string]] + :and[jsontype[create_time]match[number]] + :and[jsontype[update_time]match[number]] + :and[jsontype[mapping]match[object]] + :and[jsontype[id]match[string]] + %> + <> + <%endif%> + <%endif%> + + <%endif%> + +<%endif%> +