1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-02-02 12:19:11 +00:00

Initial Commit

This commit is contained in:
Jeremy Ruston 2024-07-09 08:59:49 +01:00
parent eeeb9f97a9
commit 2faba2e820
15 changed files with 567 additions and 2 deletions

View File

@ -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) {

View File

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

View File

@ -17,7 +17,9 @@
"tiddlywiki/jszip",
"tiddlywiki/confetti",
"tiddlywiki/dynannotate",
"tiddlywiki/tour"
"tiddlywiki/tour",
"tiddlywiki/markdown",
"tiddlywiki/ai-tools"
],
"themes": [
"tiddlywiki/vanilla",

View File

@ -0,0 +1,2 @@
title: $:/plugins/tiddlywiki/ai-tools/docs

View File

@ -0,0 +1,60 @@
title: $:/plugins/tiddlywiki/ai-tools/globals
tags: $:/tags/Global
\function default-llm-completion-server()
[all[shadows+tiddlers]tag[$:/tags/AI/CompletionServer]sort[caption]first[]]
\end
<!--
Action procedure to retrieve an LLM completion, given the following parameters:
payload - JSON payload to be posted to the LLM
resultTitlePrefix - Prefix of the tiddler to be used for saving the result. If the tiddler already exists then a number will be added repeatedly until the resulting title is unique
resultTags - Tags to be applied to the result tiddler
statusTitle - Optional title of a tiddler to which the status of the request will be bound: "pending", "complete", "error"
completionServer - Optional URL of server
-->
\procedure get-llm-completion(payload,resultTitlePrefix,resultTags,statusTitle,completionServer)
<!--
Callback for the HTTP response from the LLM
-->
\procedure get-llm-completion-callback()
<%if [<status>compare:number:gteq[200]compare:number:lteq[299]] %>
<!-- Success -->
<$action-createtiddler
$basetitle=<<resultTitlePrefix>>
tags=<<resultTags>>
type="text/markdown"
role={{{ [<data>jsonget[choices],[0],[message],[role]] }}}
text={{{ [<data>jsonget[choices],[0],[message],[content]] }}}
/>
<%else%>
<!-- Error -->
<$action-createtiddler
$basetitle=<<resultTitlePrefix>>
tags=<<resultTags>>
type="text/markdown"
role="error"
text={{{ [[Error:]] [<statusText>] [<data>jsonget[error],[message]] +[join[]] }}}
/>
<%endif%>
\end get-llm-completion-callback
<$let
completionServer={{{ [<completionServer>!is[blank]else<default-llm-completion-server>] }}}
>
<$action-log message="get-llm-completion"/>
<$action-log/>
<$action-sendmessage
$message="tm-http-request"
url={{{ [<completionServer>get[url]addsuffix[/v1/chat/completions]] }}}
body=<<payload>>
header-content-type="application/json"
bearer-auth-token-from-store="openai-secret-key"
method="POST"
oncompletion=<<get-llm-completion-callback>>
bind-status=<<statusTitle>>
var-resultTitlePrefix=<<resultTitlePrefix>>
var-resultTags=<<resultTags>>
/>
</$let>
\end get-llm-completion

File diff suppressed because one or more lines are too long

View File

@ -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=<<qualify "$:/state/popup/ai-tools/page-menu">> tooltip="Tools for interactive AI services" aria-label="AI Tools" class=<<tv-config-toolbar-class>> selectedClass="tc-selected">
<%if [<tv-config-toolbar-icons>match[yes]] %>
{{$:/plugins/tiddlywiki/ai-tools/icon}}
<%endif%>
<%if [<tv-config-toolbar-text>match[yes]] %>
<span class="tc-btn-text"><$text text="AI Tools"/></span>
<%endif%>
</$button>
<$reveal state=<<qualify "$:/state/popup/ai-tools/page-menu">> type="popup" position="belowleft" animate="yes">
<div class="tc-drop-down">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/AI/PageMenu]!has[draft.of]]" variable="listItem">
<$transclude tiddler=<<listItem>>/>
</$list>
</div>
</$reveal>

View File

@ -0,0 +1,17 @@
title: $:/plugins/tiddlywiki/ai-tools/page-menu/new-conversation
tags: $:/tags/AI/PageMenu
\define actions()
<$action-createtiddler
$basetitle="AI Conversation"
tags="$:/tags/AI/Conversation"
system-prompt="You are a helpful assistant."
current-response-text="Please describe this picture"
>
<$action-navigate $to=<<createTiddler-title>>/>
</$action-createtiddler>
\end actions
<$button actions=<<actions>> class="tc-btn-invisible">
{{$:/core/images/new-button}} New Conversation
</$button>

View File

@ -0,0 +1,7 @@
{
"title": "$:/plugins/tiddlywiki/ai-tools",
"name": "AI Tools",
"description": "AI Tools for TiddlyWiki",
"list": "readme docs settings",
"stability": "STABILITY_1_EXPERIMENTAL"
}

View File

@ -0,0 +1,6 @@
title: $:/plugins/tiddlywiki/ai-tools/readme
AI Tools for TiddlyWiki.
This plugin adds new AI tools to the TiddlyWiki platform.

View File

@ -0,0 +1,4 @@
title: $:/plugins/tiddlywiki/ai-tools/servers/local-llamafile
tags: $:/tags/AI/CompletionServer
url: http://127.0.0.1:8080
caption: Locally running Llamafile server

View File

@ -0,0 +1,4 @@
title: $:/plugins/tiddlywiki/ai-tools/servers/openai
tags: $:/tags/AI/CompletionServer
url: https://api.openai.com
caption: OpenAI Service

View File

@ -0,0 +1,16 @@
title: $:/plugins/tiddlywiki/ai-tools/settings
! AI Tools Settings
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.
!! ~OpenAI API key
# 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"/>

View File

@ -0,0 +1,204 @@
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-message {
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
border-radius: 1em;
display: flex;
flex-direction: column;
}
.ai-conversation .ai-message .ai-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-message .ai-message-toolbar .tc-tiddlylink {
color: inherit;
}
.ai-conversation .ai-message .ai-message-toolbar .ai-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-message .ai-message-toolbar .ai-message-toolbar-button:hover {
color: #ffffff;
background: rgba(255,255,255,0.55);
}
.ai-conversation .ai-message .ai-message-body {
padding: 0 1em 0 1em
}
.ai-conversation .ai-message.ai-message-role-system {
width: 60%;
background: #4c4c80;
color: white;
}
.ai-conversation .ai-message.ai-message-role-user {
width: 60%;
margin-left: auto;
background: #ffcde0;
}
.ai-conversation .ai-message.ai-message-role-assistant {
background: #dfd;
}
.ai-conversation .ai-message.ai-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 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-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: #ddffdd;
box-shadow: 30px 0 0 #90a690;
margin-bottom: 10px;
}
.ai-request-spinner:after {
animation: ai-request-spinner-animation-ball2 1s infinite;
background-color: #90a690;
box-shadow: 30px 0 0 #ddffdd;
}
@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 #90a690;
}
50% {
box-shadow: 0 0 0 #90a690;
margin-bottom: 0;
transform: translate(15px, 15px);
}
100% {
box-shadow: 30px 0 0 #90a690;
margin-bottom: 10px;
}
}
@keyframes ai-request-spinner-animation-ball2 {
0% {
box-shadow: 30px 0 0 #ddffdd;
}
50% {
box-shadow: 0 0 0 #ddffdd;
margin-top: -20px;
transform: translate(15px, 15px);
}
100% {
box-shadow: 30px 0 0 #ddffdd;
margin-top: 0;
}
}

View File

@ -0,0 +1,211 @@
title: $:/plugins/tiddlywiki/ai-tools/view-templates/conversation
tags: $:/tags/ViewTemplate
list-after: $:/core/ui/ViewTemplate/body
<!--
-->
\function statusTitle()
[<currentTiddler>addprefix[$:/temp/ai-tools/status/]]
\end statusTitle
<!--
Procedure to display a message from an AI conversation. Current tiddler is the conversation tiddler
-->
\procedure ai-message(tiddler,field,role,makeLink:"yes")
<$qualify
name="state"
title={{{ [[$:/state/ai-message-state/]addsuffix<tiddler>] }}}
>
<$let
editStateTiddler={{{ [<state>addsuffix[-edit-state]] }}}
editState={{{ [<editStateTiddler>get[text]else[view]] }}}
>
<div class={{{ ai-message [<role>addprefix[ai-message-role-]] +[join[ ]] }}}>
<div class="ai-message-toolbar">
<div class="ai-message-toolbar-left">
<$genesis $type={{{ [<makeLink>match[yes]then[$link]else[span]] }}} to=<<tiddler>>>
<$text text=<<role>>/>
</$genesis>
</div>
<div class="ai-message-toolbar-left">
<%if [<editState>!match[edit]] %>
<$button class="ai-message-toolbar-button">
<$action-setfield $tiddler=<<editStateTiddler>> text="edit"/>
edit
</$button>
<%endif%>
<%if [<editState>!match[view]] %>
<$button class="ai-message-toolbar-button">
<$action-setfield $tiddler=<<editStateTiddler>> text="view"/>
view
</$button>
<%endif%>
<$button class="ai-message-toolbar-button">
<$action-sendmessage $message="tm-copy-to-clipboard" $param={{{ [<tiddler>get<field>else[]] }}}/>
copy
</$button>
<$button class="ai-message-toolbar-button">
<$action-deletetiddler $tiddler=<<tiddler>>/>
delete
</$button>
</div>
</div>
<div class="ai-message-body">
<%if [<editState>match[view]] %>
<$transclude $tiddler=<<tiddler>> $field=<<field>> $mode="block"/>
<%else%>
<$edit-text tiddler=<<tiddler>> field=<<field>> tag="textarea" class="tc-edit-texteditor"/>
<%endif%>
<%if [<tiddler>get[image]else[]!match[]] %>
<$image source={{{ [<tiddler>get[image]] }}}/>
<%endif%>
</div>
</div>
</$let>
</$qualify>
\end ai-message
<!--
Procedure that is wikified to generate the JSON payload for the LLM
-->
\procedure payload()
\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline html conditional commentblock commentinline
{
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "<$text text={{{ [<currentTiddler>get[system-prompt]jsonstringify[]] }}}/>"
}
<!-- Loop through the tiddlers tagged with this one to pick up all the messages in the conversation -->
<$list filter="[all[shadows+tiddlers]tag<currentTiddler>!is[draft]sort[created]]">
,
{
<!-- We use JSON stringify to escape the characters that can't be used directly in JSON -->
"role": "<$text text={{{ [<currentTiddler>get[role]jsonstringify[]] }}}/>",
"content": [
{
"type": "text",
"text": "<$text text={{{ [<currentTiddler>get[text]jsonstringify[]] }}}/>"
}
<%if [<currentTiddler>get[image]else[]!match[]] %>
,
{
"type": "image_url",
"image_url": {
"url": "<$text text={{{ [[data:]] [<currentTiddler>get[image]get[type]] [[;base64,]] [<currentTiddler>get[image]get[text]jsonstringify[]] +[join[]] }}}/>"
}
}
<%endif%>
]
}
</$list>
]
}
\end payload
<!--
Action procedure to get the next response from the LLM
-->
\procedure action-get-response()
<$let
resultTitlePrefix={{{ [<currentTiddler>addsuffix[ - Prompt]] }}}
resultTags={{{ [<currentTiddler>format:titlelist[]] }}}
>
<$action-createtiddler
$basetitle=<<resultTitlePrefix>>
tags=<<resultTags>>
type="text/markdown"
role="user"
text={{!!current-response-text}}
image={{!!current-response-image}}
>
<$action-deletefield $tiddler=<<currentTiddler>> $field="current-response-text"/>
<$action-deletefield $tiddler=<<currentTiddler>> $field="current-response-image"/>
<$wikify name="json" text=<<payload>>>
<$transclude
$variable="get-llm-completion"
payload=<<json>>
completionServer={{!!completion-server}}
resultTitlePrefix=<<resultTitlePrefix>>
resultTags=<<resultTags>>
statusTitle=<<statusTitle>>
/>
</$wikify>
</$action-createtiddler>
</$let>
\end action-get-response
<%if [<currentTiddler>tag[$:/tags/AI/Conversation]] %>
<$select tiddler=<<currentTiddler>> field="completion-server" default=<<default-llm-completion-server>>>
<$list filter="[all[shadows+tiddlers]tag[$:/tags/AI/CompletionServer]sort[caption]]">
<option value=<<currentTiddler>>><$view field='caption'/></option>
</$list>
</$select>
<div class="ai-conversation">
<$transclude
$variable="ai-message"
tiddler=<<currentTiddler>>
field="system-prompt"
role="system"
makeLink="no"
/>
<$list filter="[all[shadows+tiddlers]tag<currentTiddler>!is[draft]sort[created]]" variable="message" storyview="pop">
<$transclude
$variable="ai-message"
tiddler=<<message>>
field="text"
role={{{ [<message>get[role]] }}}
/>
</$list>
<%if [<statusTitle>get[text]else[complete]match[pending]] %>
<div class="ai-request-status">
<div class="ai-request-spinner"></div>
</div>
<%endif%>
<div class="ai-user-prompt">
<div class="ai-user-prompt-text">
<$edit-text tiddler=<<currentTiddler>> field="current-response-text" tag="textarea" class="tc-edit-texteditor"/>
<$button
class="ai-user-prompt-send"
actions=<<action-get-response>>
disabled={{{ [<statusTitle>get[text]else[complete]match[pending]then[yes]] [<currentTiddler>get[current-response-text]else[]match[]then[yes]] ~[[no]] }}}
>
Send
</$button>
</div>
<div class="ai-user-prompt-image">
<div class="tc-drop-down-wrapper">
<$let state=<<qualify "$:/state/ai-user-prompt-image-dropdown-state/">>>
<$button popup=<<state>> class="tc-btn-invisible tc-btn-dropdown">Choose an image {{$:/core/images/down-arrow}}</$button>
<$link to={{!!current-response-image}}>
<$text text={{!!current-response-image}}/>
</$link>
<$reveal state=<<state>> type="popup" position="belowleft" text="" default="" class="tc-popup-keep">
<div class="tc-drop-down" style="text-align:center;">
<$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=<<currentTiddler>>
current-response-image=<<imageTitle>>
/>
<$action-deletetiddler $tiddler=<<state>>/>
"""
/>
</div>
</$reveal>
</$let>
<$image source={{!!current-response-image}}/>
</div>
</div>
</div>
</div>
<%endif%>