LLM (or AI chat) support in TiddlyWikiClassic?

98 views
Skip to first unread message

Pengju Yan

unread,
Feb 10, 2025, 1:46:05 AMFeb 10
to TiddlyWikiClassic
Hi all,

I was thinking about having recent trending LLM support in TWC and then I came across this link: Anything LLM in Tiddlywiki. Is anyone here interested in try the same thing for TWC?

Use scenario by oveek there:

I am running my TiddlyWiki on the NodeJS server so individual tiddlers are stored on the filesystem as *.tid files. I experimented a bit with uploading all of my Journal tiddler *.tid files into an Anything LLM workspace and asking it questions like “What was the first day I started working on project xyz, and how many days have I been working on it?” I also uploaded a datasheet PDF for an IC chip and asked for some details from the datasheet.


image

Technological feasibility (I worry more on TWC) by mklauber:

should each AI prompt/response be it’s own tiddler?, how to display/query prompts. How to retry or continue a prompt?

I'm not an expert of JavaScript or HTML. The fact that I am making a request here shows that I am a pure taker and not a contributor. Nevertheless, I guess there might be other people also interested in this topic, so I posted it anyway.

-Pengju

Yakov

unread,
Feb 18, 2025, 11:39:47 AMFeb 18
to TiddlyWikiClassic
Hi Pengju,

you're absolutely right, that would be really nice and I'm thinking about it, but implementation is not something quite clear to me (I did some preliminary research and I don't have a simple plan yet). Thanks for sharing the thread; if you have some time and can dig (ask?) on how do they do

> uploading all of my Journal tiddler *.tid files into an Anything LLM workspace

I'd be thankful as this is the key thing (the plugin they've built is seemingly just a UI; the real question is how to index data with a solution like Anything LLM). I also know that to enable LLM answering complex questions (i.e. those that can't be answered with knowledge from just 1 tiddler content) it requires non-trivial data management, but perhaps just any 3d-party solution would be a good start.

Best regards,
Yakov.

понедельник, 10 февраля 2025 г. в 09:46:05 UTC+3, yanp...@gmail.com:
Message has been deleted

sam paul

unread,
Mar 10, 2025, 10:58:05 PMMar 10
to TiddlyWikiClassic
Hello,

I made an experimental LLMPlugin that can load gguf models from urls and chat using llm.js. But I haven't implemented the interaction with toddlers. I am interested in writing plugins for TWC. But I am not an expert in JavaScript.  Any suggestion is appreciated. I will improve it if I have time and am still interested in this plugin.


Best regards,
Yanshu.

Yakov

unread,
Mar 11, 2025, 6:25:12 AMMar 11
to TiddlyWikiClassic
Hi Yanshu, thanks for sharing!

Could you update LLMPluginConfig to set a relative address? (config.options.txtllmjsPath = './js/llm.js/llm.js') For now, the demo you've shared doesn't load the JS for me.

After I've changed that in my dev console and reopened the tiddler, the Run Model button worked! It worked quite slow, though, but this is a nice start. I haven't reviewed the code yet, but if you extract it into a separate file (and maybe use TIFP to load it back into your blog), I'll be able to suggest changes directly on Github.

Best regards,
Yakov

вторник, 11 марта 2025 г. в 05:58:05 UTC+3, samp...@gmail.com:

sam paul

unread,
Mar 13, 2025, 12:26:19 AMMar 13
to TiddlyWikiClassic
I have updated All plugin config to the relative path.

By the way, do you know any painless ways to pack external files like js and wasm into the plugin tiddler? 

Best,
Yanshu

Yakov

unread,
Mar 13, 2025, 3:12:37 AMMar 13
to TiddlyWikiClassic
Right, I've created the first pull request with some minor suggestions, take a look if it works fine for you.

As for packing js files into a tiddler, I usually use TIFP + CookTiddlerPlugin or loading from a CDN (see loadJS in TwFormulaPlugin, it's very similar to what you used in LLMPlugin). As far as I can see from search, I have mentioned CookTiddlerPlugin several times but haven't shown its source anywhere yet, so let me put it here below (the docs are somewhat incomplete, though):

CookTiddlerPluginInfo
!!!Installation
is done as usual: copy/import this tiddler, keep the {{{systemConfig}}} tag, save, reload.
!!!Usage
TODO: detailed description of RecipeList, when the cooking is done

Definition of a recipe is done via the {{{defineRecipe}}} macro. In v0.1.6, it has the following params:
{{{
<<defineRecipe [tiddler:]name parts:partsLine [recipe:recipeScript]
  [tags:"tag 1" tag2 "tag 3" ...] [plugin[:true]] [autoupdate[:true]] [reinstall[:true]]
>>
}}}
Example of using {{{parts}}}:
{{{
  parts:"MyPluginMeta" "MyPluginCode" "Examples##MyPlugin"
}}}
note that those can be named separately ({{{parts:"MyPluginMeta" parts:"MyPluginCode"}}} etc) but when they go one by one, there's no need to add param name each time.

Example of the {{{recipe}}} param:
{{{
recipe:"
parts['MyPluginMeta'][0] = '/***\n!!Code\n'+parts['MyPluginMeta'][0]+'\n***/';

tid.tags.push('systemConfig');
"
}}}

Tags can be added via the {{{tags}}} param, so the last line in the {{{recipe}}} example above may be substituted with {{{tags:systemConfig}}} param or with just the {{{plugin[:true]}}} param.

When {{{autoupdate[:true]}}} param is used, the tiddler is cooked each time any of the tiddlers from {{{parts}}} is edited.

The {{{[reinstall[:true]]}}} param is meant to be used with plugins: when enabled, the cooked plugin is evaluated afterwards (use this carefully: any hijacking without "only once" assured will turn into an infinite loop; pushing new stuff into arrays will duplicate them and some other issues may rise if the plugin is not written in an manner not appropriate for re{{{eval}}}ualtion each time).

The {{{reinstallOnly[:true]}}} param can be used for reinstalling plugins that are not actually cooked from other tiddlers: just {{{<<defineRecipe tiddler:ReinstallablePlugin autoupdate:true reinstallOnly>>}}} is enough for that (keep in mind warnings for the {{{reinstall}}} param though).

!!!Adding CSS into a plugin
To easily add CSS into a cooked plugin, CTP introduces the {{{wrapAsCssAdder}}} helper. It doesn't just apply the CSS: instead, it
* creates a shadow tiddler from which CSS is applied, so that a user can overwrite it;
* makes sure that on ColorPalette change, the CSS is updated, too (especially important for the [[dark mode|https://yakovl.github.io/TiddlyWiki_DarkModePlugin/]]).
Use it like this:
# create a tiddler like MyPluginStyleSheet, put CSS there (optionally wrap it into <html><code>/*{{{*/.../*}}}*/</code></html> to format it as code);
# in RecipeList, add it to {{{parts}}} and inside {{{recipe}}} add {{{parts['MyPluginStyleSheet'][0] = wrapAsCssAdder(parts['MyPluginStyleSheet'][0], tid.title, 'MyPluginStyleSheet')}}}.
Notes:
* in your dev TW, MyPluginStyleSheet tiddler will be both the source of CSS inside the plugin and the shadow //and// "overwrite" the shadow so that the styles are updated instantly during development, even without the {{{reinstall}}} option enabled.
* If you are developing a theme which is a plugin as well, you may want not to apply the CSS until the theme is used. For this case, you can reference the shadow in the StyleSheet slice and use the 4th argument like this: {{{wrapAsCssAdder(.., .., .., { dontApply: true })}}}.

!!!The {{{<<cook>>}}} macro
This macro creates a button which cooks the tiddler "on demand". It has the following syntax:
{{{
<<cook [tiddler] [label:"button label text"] [tooltip:"button tooltip text"]>>
}}}
If the {{{tiddler}}} param is omitted, the tiddler in which the button is wikified is cooked. If there's no recipe for that tiddler yet, the button will notify about it:
{{{<<cook SomeRandomTiddler label:"cook SomeRandomTiddler" tooltip:burnTooltips>>}}}
<<cook SomeRandomTiddler label:"cook SomeRandomTiddler" tooltip:burnTooltips>>

CookTiddlerPlugin
/***
|Description|Allows to "cook" tiddlers using various "recipes" – automatically (once a "part" is updated) and using a button|
|Version|0.2.5|
|Author|Yakov Litvin|
***/
//{{{
// creates a hidden section with lines commented by "//"
// to prevent css comments from being treated as end of js comments;
// adds js that grabs that section (of nameOfTiddlerToCook tiddler), removes "//"
// in each line and creates a shadow tiddler treated as css (..)
function wrapAsCssAdder(css, nameOfTiddlerToCook, cssName, options) {

return '// /%\n' +
'/***\n!' + cssName + '\n***/\n' +
css.replace(/^/gm, '//') +
'\n/***\n!end of '+ cssName + '\n***/\n'+
'// %/ //\n' +
'//{{{\n'+
';(function() {\n' +
'var cssName = ' + JSON.stringify(cssName) + ',\n' +
'    css = store.getTiddlerText(' + JSON.stringify(nameOfTiddlerToCook) + ' + \"##\" + cssName).replace(/^\\/\\//gm, \"\");\n' +
'css = css.substring(5, css.length - 5); // cut leading \\n***/ and trailing /***\\n of the section\n' +
'config.shadowTiddlers[cssName] = css;\n' +
(options && options.dontApply ? '' :
'store.addNotification(cssName, refreshStyles);\n' +
'store.addNotification("ColorPalette", function(smth, doc) { refreshStyles(cssName, doc) })\n'
) +
'})();\n' +
'//}}}'
}

config.extensions.cookBook = {

// hashmap by tiddlerName of
// - parts: Array of tiddlerTextExpr (tName, tName::slNmae, tName##seName, ::slNmae,)
// - steps: JavaScript expression containing parts[i] to be evaled that returns text,
//          null means join("\n") of parts[i] ~contents
// - title: equal to tiddlerName for usage flexibility
recipes: {},
addRecipe: function(title, parts, steps, tags, autoupdate, reinstall, minify, reinstallOnly) {

this.recipes[title] = { title: title, parts: parts, steps: steps, tags: tags, autoupdate: autoupdate, reinstall: reinstall, reinstallOnly: reinstallOnly }
},
getRecipeFor: function(title) { return this.recipes[title] },
getRecipesContaining: function(title) {

var goodPartRegExp = new RegExp("^" + title.escapeRegExp() + "(?:$|::.+|##.+)")
var demandedRecipes = [], t, recipeParts, i;
for(t in this.recipes) {
recipeParts = this.recipes[t].parts;
for(i = 0; i < recipeParts.length; i++)
if(goodPartRegExp.exec(recipeParts[i])) {
demandedRecipes.push(this.recipes[t]);
break;
}
}
return demandedRecipes;
}
};

config.macros.defineRecipe = {

readRecipeList: function() {

if (!window.store) return setTimeout(readRecipeList, 100);

var recipeList = store.fetchTiddler("RecipeList");
if(recipeList && recipeList.text)
wikify(recipeList.text, document.createElement("div"), null, recipeList);
},
handler: function(place, macroName, params, wikifier, paramString, tiddler) {

var pParams = paramString.parseParams("tiddler", null, true, false, true),
    checkFlag  = function(name, default_val) {
return getParam(pParams, name, default_val) ||
!!((new RegExp("\\s"+name+"(?:\\s|$)")).exec(paramString))
    },
    tiddlerNames = pParams[0]["tiddler"],
    partNames  = pParams[0]["parts"],
    tags  = pParams[0]["tags"] || [],
    plugin  = checkFlag("plugin"),
    recipe  = getParam(pParams, "recipe", ""),
    autoupdate  = checkFlag("autoupdate"),
    reinstallOnly= checkFlag("reinstallOnly"),
    reinstall  = checkFlag("reinstall") || reinstallOnly,
    minify  = checkFlag("minify");
if(plugin) tags.push("systemConfig");

if(!tiddlerNames) {
createTiddlyError(place,
  "Macro error: no 'tiddler' param (click this for details)",
  "Put it after the macro name as either 'tiddler:\"tiddlerName\"' or just '\"tiddlerName\"'");
return;
}
if(reinstallOnly) partNames = tiddlerNames;
if(!partNames) {
createTiddlyError(place,"Macro error: no 'parts' param (click this for details)","Put them after the 'tiddler' param as 'parts:\"partName1\" \"partName2\" ...'");
return;
}
var tiddlerName = tiddlerNames[0],
    sameTiddlerPartRegExp = /^(::|##).+/, i;
for(i = 0; i < partNames.length; i++)
if(sameTiddlerPartRegExp.exec(partNames[i]))
partNames[i] = tiddlerName + partNames[i];

// show the written macro code:
var w = wikifier,
    macroTWcode = w.source.substring(w.matchStart, w.nextMatch),
    hide = params.contains('hide');
if (!hide)
createTiddlyText(createTiddlyElement(place, "pre"), macroTWcode);

config.extensions.cookBook.addRecipe(tiddlerName, partNames,
recipe, tags, autoupdate, reinstall, minify, reinstallOnly);
}
};

config.macros.cook = {

cookRecipe: function(recipe, force) {

if(!force && !recipe.autoupdate) return;

var partNames = recipe.parts, parts = {}, i;
for(i = 0; i < partNames.length; i++)
parts[partNames[i]] = [ store.getTiddlerText(partNames[i], "") ];
//# for filtering, add extra parsing here (fill the array with several parts)

var script = "config.macros.cook.f = function(title, partNames, parts) {" +
"var text = '', tid = new Tiddler(title)," +

"    oldTid  = store.fetchTiddler(title);" +
"tid.creator = oldTid ? oldTid.creator : config.options.txtUserName;" +
"tid.created = oldTid ? oldTid.created : new Date();" +
"tid.modifier = config.options.txtUserName;" +
"tid.modified = new Date();" +
"var oldChangeCount = oldTid ? oldTid.fields.changecount : null;" +
"tid.fields.changecount = oldChangeCount ? (parseInt(oldChangeCount)+1) : 0;\n" +

recipe.steps +

"\nvar partName, namedParts, i;" +
"for(partName in parts) {" +
"    namedParts = parts[partName];" +
"    for(i = 0; i < namedParts.length; i++)" +
"        text += (namedParts[i]+'\\n');" +
"};" +
"if(text) text = text.substr(0,text.length-1);" +
"tid.text = tid.text || text;" +
"return tid;" +
"};"
console.log("in cookRecipe, script is",script);
eval(script);

var tid      = this.f(recipe.title, partNames, parts),
    modifier = config.options.txtUserName,
    modified = new Date();
tid.tags = tid.tags.concat(recipe.tags);
if(recipe.minify) {
tid.text = tid.text.replace(/\r/gm,'\n');
//# implement minification here, use some external lib
}

console.log("in cookRecipe, tid is",tid);
if(!recipe.reinstallOnly) {
store.saveTiddler(tid, recipe.title, null, modifier, modified);
displayMessage("Tiddler \""+ recipe.title +"\" has been cooked");
console.log("Tiddler \""+recipe.title+"\" has been cooked");
//# add msg that tells the results (test this with both ways of cooking)
//# add msg that tells if some part wasn't found
}
if(recipe.reinstall /*&& isPluginEnabled(tid)*/) {
//# do stuff from STP's installPlugin (logging)
var msg;
try {
window.eval(tid.text);
msg = '"'+ tid.title +'" was reinstalled.';
} catch(ex) {
// do react on errors in a helpful way
msg = "Error evaluating "+ tid.title +":\n"+
  exceptionText(ex)
} finally {
displayMessage(msg);
console.log(msg);
}
//# do stuff from STP's installPlugin (logging)
}
},
cookRecipeFor: function(title, force) {

var recipe = config.extensions.cookBook.getRecipeFor(title);
if(recipe)
this.cookRecipe(recipe, force);
else
return 'no recipe for "'+ title +'"';
},
cookRecipesContaining: function(title, force) {

var recipes = config.extensions.cookBook.getRecipesContaining(title), i;
for(i = 0; i < recipes.length; i++)
this.cookRecipe(recipes[i], force);
},
handler: function(place, macroName, params, wikifier, paramString, tiddler) {

var pParams = paramString.parseParams("title", null, true, false, true),
    title = getParam(pParams, "title", tiddler ? tiddler.title : ""),
    label = getParam(pParams, "label", "cook"),
    tooltip = getParam(pParams, "tooltip", "");

createTiddlyButton(place, label, tooltip, function() {
var error = config.macros.cook.cookRecipeFor(title, true);
if(error) displayMessage(error);
});
}
};

// hijack in a reinstallable fashion
if(!config.commands.saveTiddler.orig_handler_CTP)
 config.commands.saveTiddler.orig_handler_CTP = config.commands.saveTiddler.handler;
config.commands.saveTiddler.handler = function(event, src, title) {

// "pre-saving" of the tiddler's text for cooking to see it
// (code extracted from Story.saveTiddler)
var tiddlerElem = story.getTiddler(title);
if(tiddlerElem) {
var fields = {};
story.gatherSaveFields(tiddlerElem, fields);
var tiddler = store.saveTiddler(title, title, fields.text);
}

config.macros.cook.cookRecipesContaining(title);
//# add the corresponding messages

return config.commands.saveTiddler.orig_handler_CTP.apply(this, arguments);
// it's important not to use "this" here (causes conflicts with CodeMirror and may do so with others)
};

setTimeout(config.macros.defineRecipe.readRecipeList, 100);
//}}}

четверг, 13 марта 2025 г. в 07:26:19 UTC+3, samp...@gmail.com:

sam paul

unread,
Mar 14, 2025, 1:33:19 PMMar 14
to TiddlyWikiClassic
Hi, Yakov. I have merged your request.

Are there any examples of how to use CookTiddlerPlugin? For example, this version of LLMPlugin requires user to download the release file of llm.js and put the folder somewhere.

The file tree is:
-llm.js -wasm -llamacpp-cpu.js
        -llm.js
        -117.js
 
How to pack them into one or multiple tiddlers?

Best,

Yanshu
Reply all
Reply to author
Forward
0 new messages