From f9f27321abcf69c324ff603490933d0a48210cfc Mon Sep 17 00:00:00 2001 From: DanyLE Date: Mon, 24 Apr 2023 16:41:26 +0200 Subject: [PATCH] feat(Blogger): support blog posts TF-IDF analyse --- Blogger/README.md | 1 + Blogger/api/ai/analyse.lua | 66 ++++ Blogger/api/ai/cluster.lua | 346 +++++++++++++++++++++ Blogger/api/ai/stopwords.txt | 151 +++++++++ Blogger/api/ai/test.lua | 50 +++ Blogger/build.json | 2 +- Blogger/build/debug/README.md | 1 + Blogger/build/debug/api/ai/analyse.lua | 66 ++++ Blogger/build/debug/api/ai/cluster.lua | 346 +++++++++++++++++++++ Blogger/build/debug/api/ai/stopwords.txt | 151 +++++++++ Blogger/build/debug/api/ai/test.lua | 50 +++ Blogger/build/debug/{ => api}/sendmail.lua | 0 Blogger/build/debug/main.js | 2 +- Blogger/build/debug/package.json | 2 +- Blogger/build/release/Blogger.zip | Bin 10426 -> 15450 bytes Blogger/dialogs.ts | 4 +- Blogger/main.ts | 30 ++ Blogger/package.json | 2 +- 18 files changed, 1264 insertions(+), 6 deletions(-) create mode 100644 Blogger/api/ai/analyse.lua create mode 100644 Blogger/api/ai/cluster.lua create mode 100644 Blogger/api/ai/stopwords.txt create mode 100644 Blogger/api/ai/test.lua create mode 100644 Blogger/build/debug/api/ai/analyse.lua create mode 100644 Blogger/build/debug/api/ai/cluster.lua create mode 100644 Blogger/build/debug/api/ai/stopwords.txt create mode 100644 Blogger/build/debug/api/ai/test.lua rename Blogger/build/debug/{ => api}/sendmail.lua (100%) diff --git a/Blogger/README.md b/Blogger/README.md index b6f783d..f956a63 100644 --- a/Blogger/README.md +++ b/Blogger/README.md @@ -6,6 +6,7 @@ Blackend for my blog at https://blog.iohub.dev ## Change logs ### v0.2.x-a +* Patch 11: Add TFIDF analyse functionality * Patch 10: Migrate code to typescript, use SQLiteDB lib for database access * Patch 9: Update to use the new MDE library * Patch 8: Support for antOS 2.0.x diff --git a/Blogger/api/ai/analyse.lua b/Blogger/api/ai/analyse.lua new file mode 100644 index 0000000..d748e84 --- /dev/null +++ b/Blogger/api/ai/analyse.lua @@ -0,0 +1,66 @@ +local args = ... + +local ret = { + error = false, + result = nil +} +local __dir__ = debug.getinfo(1).source:match("@?(.*/)") +LOG_DEBUG("CURRENT PATH:%s", __dir__) +local cluster = loadfile(__dir__.."/cluster.lua")() +local dbpath = require("vfs").ospath(args.dbpath) +LOG_DEBUG("DB PATH:%s", dbpath) + +local gettext = {} +gettext.get = function(file) + local db = DBModel:new{db=file} + db:open() + if not db then return nil end + local data, sort = db:find("blogs", { + where = { publish = 1 }, + fields = {"id", "content"} + }) + db:close() + if not data or #data == 0 then return nil end + return data +end + +gettext.stopwords = function(ospath) + local words = {} + for line in io.lines(ospath) do + words[line] = true + end + return words +end + +local data = gettext.get(dbpath) +local documents = {} +if data then + local sw = gettext.stopwords(__dir__.."/stopwords.txt") + for k, v in pairs(data) do + local bag = cluster.bow(data[k].content, sw) + documents[data[k].id] = bag + end + + cluster.tfidf(documents) + --local v = cluster.search("arm", documents) + --echo(JSON.encode(v)) + local vectors, maxv, size = cluster.get_vectors(documents) + local analytical = DBModel:new{db=dbpath} + analytical:open() + -- purge the table + analytical:delete("st_similarity", nil) + -- get similarity and put to the table + for id, v in pairs(vectors) do + local top = cluster.top_similarity(id, vectors, args.top, 0.1) + for a, b in pairs(top) do + local record = {pid = id, sid = a, score = b} + analytical:insert("st_similarity", record) + end + end + analytical:close() + ret.result = "Analyse complete" +else + ret.error = "Unable to query database for post" +end + +return ret \ No newline at end of file diff --git a/Blogger/api/ai/cluster.lua b/Blogger/api/ai/cluster.lua new file mode 100644 index 0000000..13ed37c --- /dev/null +++ b/Blogger/api/ai/cluster.lua @@ -0,0 +1,346 @@ +local doclassify = {} +local st = require("stmr") +doclassify.bow = function(data, stopwords) + -- first step get a table of worlds that contain + -- world: occurences + local bag = {} + for w in data:gmatch('%w+') do + local word = w:lower() + if not stopwords[word] then + word = st.stmr(word) + if bag[word] then + bag[word].count = bag[word].count + 1 + else + bag[word] = {count=0, tf=0, tfidf=0.0} + bag[word].count = 1 + end + end + end + -- now calculate the tf of the bag + for k,v in pairs(bag) do + bag[k].tf = math.log(1 + bag[k].count) + end + return bag +end +doclassify.len = function(table) + local cnt = 0 + for k,v in pairs(table) do cnt = cnt+1 end + return cnt +end +doclassify.tfidf = function(documents) + -- now for each term in a bag, calculate + -- the inverse document frequency, which + -- is a measure of how much information + -- the word provides, that is, whether the + -- term is common or rare across all documents + local ndoc = doclassify.len(documents) + for k,bag in pairs(documents) do + -- for eacht term in bag + -- calculate its idf across all documents + for term,b in pairs(bag) do + local n = 0 + for id,doc in pairs(documents) do + if doc[term] then n = n+1 end + end + --echo("term:"..term.." appears in"..n.." documents") + b.tfidf = b.tf*math.log(ndoc/n) + end + end + +end + +doclassify.search = function(term, documents) + local r = {} + for id, doc in pairs(documents) do + if doc[term:lower()] then + r[id] = doc[term].tfidf + end + end + return r +end + +doclassify.get_vectors = function(documents) + -- get a list of vector from documents + local index = 0 + local vectors = {} + local maps = {} + local terms = {} + local maxv = 0 + + for id in pairs(documents) do + maps[id] = {} + vectors[id] = {} + end + -- first loop, get the term + for id, doc in pairs(documents) do + for k,v in pairs(doc) do + -- get max value + if v.tfidf > maxv then + maxv = v.tfidf + end + -- get the term + if not terms[k] then + index = index + 1 + terms[k] = index + end + for pid in pairs(documents) do + if not maps[pid][k] then + if id == pid then + maps[pid][k] = v.tfidf + else + maps[pid][k] = 0 + end + else + if maps[pid][k] == 0 and id == pid then + maps[pid][k] = v.tfidf + end + end + end + end + end + -- reindexing the vectors + for id in pairs(documents) do + for k,v in pairs(maps[id]) do + vectors[id][terms[k]] = v + end + end + --echo("Max tfidf "..maxv.." in document #"..maxid.." of term "..term) + return vectors, maxv, index, terms +end + +doclassify.similarity = function(va, vb) + -- using cosin similarity + local dotp = 0 + local maga = 0 + local magb = 0 + for k = 1,#va do + dotp = dotp + va[k]*vb[k] + maga = maga + va[k]*va[k] + magb = magb + vb[k]*vb[k] + end + maga = math.sqrt(maga) + magb = math.sqrt(magb) + local d = 0 + if maga ~= 0 and magb ~= 0 then + d = dotp/ (magb*maga) + end + return d +end +doclassify.similarities = function(v1, collection) + local similarities = {} + assert(#v1 == #(collection[1]), "Incorrect vectors size") + for i=1,#collection do + similarities[i] = doclassify.similarity(v1, collection[i]) + end + return similarities +end + +doclassify.mean_similarity = function(v1, v2) + assert(#v1 == #v2, "Incorrect vectors size") + local similarities = {} + for i = 1,#v1 do similarities[i] = doclassify.similarity(v1[i], v2[i]) end + return doclassify.mean(similarities) +end +doclassify.similarity_chart = function(id, vectors) + local vs = {} + local cnt = 0 + local lut = {} + for k,v in pairs(vectors) do + if k ~= id then + cnt = cnt + 1 + vs[cnt] = v + lut[cnt] = k + end + end + if not vs[1] then return {} end + return doclassify.similarities(vectors[id], vs), lut +end + +doclassify.top_similarity = function(id, vectors, n, th) + local chart,lut = doclassify.similarity_chart(id,vectors) + --echo(JSON.encode(chart)) + --echo(JSON.encode(lut)) + if not lut or #lut <= 0 then return nil end + local top = {} + + local j=0 + local goon = true + if not th then + goon = false + th = 0 + end + + while j < n or goon + do + local i,maxv = doclassify.argmax(chart) + top[lut[i]] = maxv + chart[i] = 0.0 + j=j+1 + if maxv < th and goon then + goon = false + end + end + + --for j=1,n do + -- local i,maxv = doclassify.argmax(chart) + -- top[lut[i]] = maxv + -- chart[i] = 0.0 + --end + return top + +end +doclassify.save_vectors = function(vectors, name) + local f = io.open(name,"w") + if f == nil then return false end + for id, v in pairs(vectors) do + f:write(id) + for i=1,#v do f:write(","..v[i]) end + f:write("\n") + end + f:close() + return true +end +doclassify.save_topchart = function(vectors, name,n) + local f = io.open(name,"w") + if f == nil then return false end + for k,v in pairs(vectors) do + local top = doclassify.top_similarity(k,vectors,n, 0.1) + for a,b in pairs(top) do + f:write(k.." "..a.." "..b.."\n") + end + end + f:close() + return true +end +doclassify.kmean = function(nclass, documents, maxstep, ids) + -- now + local vectors, maxv, size = doclassify.get_vectors(documents) + -- random centroids + local centroids = {} + local old_centroids = {} + local clusters = {} + --for pid in pairs(documents) do clusters[pid] = 0 end + -- add noise to mean_vector + for i = 1,nclass do + if ids == nil then + centroids[i] = doclassify.random(size,math.floor(maxv)) + else + centroids[i] = vectors[ids[i]] + end + old_centroids[i] = doclassify.zeros(size) + end + + -- loop until convergence or maxstep reached + local similarity = doclassify.mean_similarity(centroids, old_centroids) + local step = maxstep + while 1.0-similarity > 1e-9 and step > 0 do + clusters = {} + --echo(JSON.encode(centroids)) + for id,v in pairs(vectors) do + local similarities = doclassify.similarities(v, centroids) + --echo(JSON.encode(similarities)) + local cluster, maxvalue = doclassify.argmax(similarities) + --echo("doc #"..id.." is in clusters #"..cluster.." max value is "..maxvalue) + clusters[id] = cluster + end + -- storing the old centroids + old_centroids = centroids + -- calculate new centroids + local new_centroids = {} + for class in pairs(centroids) do + local cnt = 0 + local cvectors = {} + for id,v in pairs(vectors) do + if clusters[id] == class then + cnt = cnt + 1 + cvectors[cnt] = v + end + end + new_centroids[class] = doclassify.mean_vector(cvectors, size) + end + centroids = new_centroids + --echo(JSON.encode(centroids)) + --echo(JSON.encode(old_centroids)) + similarity = doclassify.mean_similarity(centroids, old_centroids) + echo("step #"..step..", similarity "..similarity) + step = step - 1 + end + local results = {} + for i = 1,nclass do + local list = {} + local cnt = 0 + for id,c in pairs(clusters) do + if c == i then + cnt = cnt + 1 + list[cnt] = id + end + end + results[i] = list + end + return results, clusters, centroids +end + +doclassify.zeros = function(n) + local vector = {} + for i = 1,n do vector[i] = 0.0 end + return vector +end + +doclassify.random = function(n,maxv) + local vector = {} + for i=1,n do + vector[i] = math.random() + math.random(0, maxv) + end + return vector +end + +doclassify.sum = function(v) + local sum = 0.0 + for i=1,#v do sum = sum + v[i] end + return sum +end + +doclassify.mean = function(v) + return doclassify.sum(v)/#v + +end + +doclassify.mean_vector = function(vectors, size) + local means = doclassify.zeros(size) + if not vectors or #vectors == 0 then return means end + --local size = #(vectors[1]) + local times = 0 + for k,v in pairs(vectors) do + for i=1,#v do means[i] = means[i] + v[i] end + times = times + 1 + end + for i = 1,size do means[i] = means[i]/times end + return means +end + +doclassify.argmin = function(v) + local minv = 0.0 + local mini = 0.0 + for i = 1,#v do + if v[i] <= minv then + mini = i + minv = v[i] + end + end + --echo("min index"..mini.." val "..minv) + return mini, minv +end + +doclassify.argmax = function(v) + local maxv = 0.0 + local maxi = 0.0 + for i = 1,#v do + if v[i] >= maxv then + maxi = i + maxv = v[i] + end + end + return maxi,maxv +end + +return doclassify diff --git a/Blogger/api/ai/stopwords.txt b/Blogger/api/ai/stopwords.txt new file mode 100644 index 0000000..abe0cef --- /dev/null +++ b/Blogger/api/ai/stopwords.txt @@ -0,0 +1,151 @@ +i +me +my +myself +we +our +ours +ourselves +you +your +yours +yourself +yourselves +he +him +his +himself +she +her +hers +herself +it +its +itself +they +them +their +theirs +themselves +what +which +who +whom +this +that +these +those +am +is +are +was +were +be +been +being +have +has +had +having +do +does +did +doing +a +an +the +and +but +if +or +because +as +until +while +of +at +by +for +with +about +against +between +into +through +during +before +after +above +below +to +from +up +down +in +out +on +off +over +under +again +further +then +once +here +there +when +where +why +how +all +any +both +each +few +more +most +other +some +such +no +nor +not +only +own +same +so +than +too +very +s +t +can +will +just +don +should +now +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +w +r +s +t +x +y +z \ No newline at end of file diff --git a/Blogger/api/ai/test.lua b/Blogger/api/ai/test.lua new file mode 100644 index 0000000..21573a6 --- /dev/null +++ b/Blogger/api/ai/test.lua @@ -0,0 +1,50 @@ +local path = require("fs/vfs").ospath("home://aiws/blog-clustering") +local gettext = loadfile(path.."/gettext.lua")() +local cluster = loadfile(path.."/cluster.lua")() + +local refresh = false + +local file = "/home/mrsang/test.csv" +if refresh then + local data = gettext.get({publish=1}) + local documents = {} + if data then + local sw = gettext.stopwords("home://aiws/blog-clustering/stopwords.txt") + for k,v in pairs(data) do + local bag = cluster.bow(data[k].content, sw) + documents[data[k].id] = bag + end + cluster.tfidf(documents) + --local v = cluster.search("arm", documents) + --echo(JSON.encode(v)) + local vectors, maxv, size = cluster.get_vectors(documents) + local s = cluster.save_topchart(vectors,file, 3) + if s then echo("file saved") else echo("error save file") end + --echo(JSON.encode(r)) + --r = cluster.similarity(vectors["14"],vectors["16"]) + --echo("Similarity "..r) + + --local c,l = cluster.kmean(3, documents, 10) + --echo(JSON.encode(c)) + --echo(JSON.encode(l)) + else + echo("Data missing") + end +else + local f = io.open(file,"r") + local result = {} + for line in f:lines() do + local arr = {} + local cnt = 0 + for i in line:gmatch( "%S+") do + cnt = cnt + 1 + arr[cnt] = i + end + if not result[arr[1]] then result[arr[1]] = {} end + result[arr[1]][arr[2]] = tonumber(arr[3]) + end + f:close() + echo(JSON.encode(result)) + --local r = cluster.top_similarity("2",vectors, 3) + --echo(JSON.encode(r)) +end \ No newline at end of file diff --git a/Blogger/build.json b/Blogger/build.json index 566d9f8..d204b72 100644 --- a/Blogger/build.json +++ b/Blogger/build.json @@ -55,7 +55,7 @@ "data": { "src": [ "scheme.html", - "api/sendmail.lua", + "api", "package.json", "README.md", "main.css" diff --git a/Blogger/build/debug/README.md b/Blogger/build/debug/README.md index b6f783d..f956a63 100644 --- a/Blogger/build/debug/README.md +++ b/Blogger/build/debug/README.md @@ -6,6 +6,7 @@ Blackend for my blog at https://blog.iohub.dev ## Change logs ### v0.2.x-a +* Patch 11: Add TFIDF analyse functionality * Patch 10: Migrate code to typescript, use SQLiteDB lib for database access * Patch 9: Update to use the new MDE library * Patch 8: Support for antOS 2.0.x diff --git a/Blogger/build/debug/api/ai/analyse.lua b/Blogger/build/debug/api/ai/analyse.lua new file mode 100644 index 0000000..d748e84 --- /dev/null +++ b/Blogger/build/debug/api/ai/analyse.lua @@ -0,0 +1,66 @@ +local args = ... + +local ret = { + error = false, + result = nil +} +local __dir__ = debug.getinfo(1).source:match("@?(.*/)") +LOG_DEBUG("CURRENT PATH:%s", __dir__) +local cluster = loadfile(__dir__.."/cluster.lua")() +local dbpath = require("vfs").ospath(args.dbpath) +LOG_DEBUG("DB PATH:%s", dbpath) + +local gettext = {} +gettext.get = function(file) + local db = DBModel:new{db=file} + db:open() + if not db then return nil end + local data, sort = db:find("blogs", { + where = { publish = 1 }, + fields = {"id", "content"} + }) + db:close() + if not data or #data == 0 then return nil end + return data +end + +gettext.stopwords = function(ospath) + local words = {} + for line in io.lines(ospath) do + words[line] = true + end + return words +end + +local data = gettext.get(dbpath) +local documents = {} +if data then + local sw = gettext.stopwords(__dir__.."/stopwords.txt") + for k, v in pairs(data) do + local bag = cluster.bow(data[k].content, sw) + documents[data[k].id] = bag + end + + cluster.tfidf(documents) + --local v = cluster.search("arm", documents) + --echo(JSON.encode(v)) + local vectors, maxv, size = cluster.get_vectors(documents) + local analytical = DBModel:new{db=dbpath} + analytical:open() + -- purge the table + analytical:delete("st_similarity", nil) + -- get similarity and put to the table + for id, v in pairs(vectors) do + local top = cluster.top_similarity(id, vectors, args.top, 0.1) + for a, b in pairs(top) do + local record = {pid = id, sid = a, score = b} + analytical:insert("st_similarity", record) + end + end + analytical:close() + ret.result = "Analyse complete" +else + ret.error = "Unable to query database for post" +end + +return ret \ No newline at end of file diff --git a/Blogger/build/debug/api/ai/cluster.lua b/Blogger/build/debug/api/ai/cluster.lua new file mode 100644 index 0000000..13ed37c --- /dev/null +++ b/Blogger/build/debug/api/ai/cluster.lua @@ -0,0 +1,346 @@ +local doclassify = {} +local st = require("stmr") +doclassify.bow = function(data, stopwords) + -- first step get a table of worlds that contain + -- world: occurences + local bag = {} + for w in data:gmatch('%w+') do + local word = w:lower() + if not stopwords[word] then + word = st.stmr(word) + if bag[word] then + bag[word].count = bag[word].count + 1 + else + bag[word] = {count=0, tf=0, tfidf=0.0} + bag[word].count = 1 + end + end + end + -- now calculate the tf of the bag + for k,v in pairs(bag) do + bag[k].tf = math.log(1 + bag[k].count) + end + return bag +end +doclassify.len = function(table) + local cnt = 0 + for k,v in pairs(table) do cnt = cnt+1 end + return cnt +end +doclassify.tfidf = function(documents) + -- now for each term in a bag, calculate + -- the inverse document frequency, which + -- is a measure of how much information + -- the word provides, that is, whether the + -- term is common or rare across all documents + local ndoc = doclassify.len(documents) + for k,bag in pairs(documents) do + -- for eacht term in bag + -- calculate its idf across all documents + for term,b in pairs(bag) do + local n = 0 + for id,doc in pairs(documents) do + if doc[term] then n = n+1 end + end + --echo("term:"..term.." appears in"..n.." documents") + b.tfidf = b.tf*math.log(ndoc/n) + end + end + +end + +doclassify.search = function(term, documents) + local r = {} + for id, doc in pairs(documents) do + if doc[term:lower()] then + r[id] = doc[term].tfidf + end + end + return r +end + +doclassify.get_vectors = function(documents) + -- get a list of vector from documents + local index = 0 + local vectors = {} + local maps = {} + local terms = {} + local maxv = 0 + + for id in pairs(documents) do + maps[id] = {} + vectors[id] = {} + end + -- first loop, get the term + for id, doc in pairs(documents) do + for k,v in pairs(doc) do + -- get max value + if v.tfidf > maxv then + maxv = v.tfidf + end + -- get the term + if not terms[k] then + index = index + 1 + terms[k] = index + end + for pid in pairs(documents) do + if not maps[pid][k] then + if id == pid then + maps[pid][k] = v.tfidf + else + maps[pid][k] = 0 + end + else + if maps[pid][k] == 0 and id == pid then + maps[pid][k] = v.tfidf + end + end + end + end + end + -- reindexing the vectors + for id in pairs(documents) do + for k,v in pairs(maps[id]) do + vectors[id][terms[k]] = v + end + end + --echo("Max tfidf "..maxv.." in document #"..maxid.." of term "..term) + return vectors, maxv, index, terms +end + +doclassify.similarity = function(va, vb) + -- using cosin similarity + local dotp = 0 + local maga = 0 + local magb = 0 + for k = 1,#va do + dotp = dotp + va[k]*vb[k] + maga = maga + va[k]*va[k] + magb = magb + vb[k]*vb[k] + end + maga = math.sqrt(maga) + magb = math.sqrt(magb) + local d = 0 + if maga ~= 0 and magb ~= 0 then + d = dotp/ (magb*maga) + end + return d +end +doclassify.similarities = function(v1, collection) + local similarities = {} + assert(#v1 == #(collection[1]), "Incorrect vectors size") + for i=1,#collection do + similarities[i] = doclassify.similarity(v1, collection[i]) + end + return similarities +end + +doclassify.mean_similarity = function(v1, v2) + assert(#v1 == #v2, "Incorrect vectors size") + local similarities = {} + for i = 1,#v1 do similarities[i] = doclassify.similarity(v1[i], v2[i]) end + return doclassify.mean(similarities) +end +doclassify.similarity_chart = function(id, vectors) + local vs = {} + local cnt = 0 + local lut = {} + for k,v in pairs(vectors) do + if k ~= id then + cnt = cnt + 1 + vs[cnt] = v + lut[cnt] = k + end + end + if not vs[1] then return {} end + return doclassify.similarities(vectors[id], vs), lut +end + +doclassify.top_similarity = function(id, vectors, n, th) + local chart,lut = doclassify.similarity_chart(id,vectors) + --echo(JSON.encode(chart)) + --echo(JSON.encode(lut)) + if not lut or #lut <= 0 then return nil end + local top = {} + + local j=0 + local goon = true + if not th then + goon = false + th = 0 + end + + while j < n or goon + do + local i,maxv = doclassify.argmax(chart) + top[lut[i]] = maxv + chart[i] = 0.0 + j=j+1 + if maxv < th and goon then + goon = false + end + end + + --for j=1,n do + -- local i,maxv = doclassify.argmax(chart) + -- top[lut[i]] = maxv + -- chart[i] = 0.0 + --end + return top + +end +doclassify.save_vectors = function(vectors, name) + local f = io.open(name,"w") + if f == nil then return false end + for id, v in pairs(vectors) do + f:write(id) + for i=1,#v do f:write(","..v[i]) end + f:write("\n") + end + f:close() + return true +end +doclassify.save_topchart = function(vectors, name,n) + local f = io.open(name,"w") + if f == nil then return false end + for k,v in pairs(vectors) do + local top = doclassify.top_similarity(k,vectors,n, 0.1) + for a,b in pairs(top) do + f:write(k.." "..a.." "..b.."\n") + end + end + f:close() + return true +end +doclassify.kmean = function(nclass, documents, maxstep, ids) + -- now + local vectors, maxv, size = doclassify.get_vectors(documents) + -- random centroids + local centroids = {} + local old_centroids = {} + local clusters = {} + --for pid in pairs(documents) do clusters[pid] = 0 end + -- add noise to mean_vector + for i = 1,nclass do + if ids == nil then + centroids[i] = doclassify.random(size,math.floor(maxv)) + else + centroids[i] = vectors[ids[i]] + end + old_centroids[i] = doclassify.zeros(size) + end + + -- loop until convergence or maxstep reached + local similarity = doclassify.mean_similarity(centroids, old_centroids) + local step = maxstep + while 1.0-similarity > 1e-9 and step > 0 do + clusters = {} + --echo(JSON.encode(centroids)) + for id,v in pairs(vectors) do + local similarities = doclassify.similarities(v, centroids) + --echo(JSON.encode(similarities)) + local cluster, maxvalue = doclassify.argmax(similarities) + --echo("doc #"..id.." is in clusters #"..cluster.." max value is "..maxvalue) + clusters[id] = cluster + end + -- storing the old centroids + old_centroids = centroids + -- calculate new centroids + local new_centroids = {} + for class in pairs(centroids) do + local cnt = 0 + local cvectors = {} + for id,v in pairs(vectors) do + if clusters[id] == class then + cnt = cnt + 1 + cvectors[cnt] = v + end + end + new_centroids[class] = doclassify.mean_vector(cvectors, size) + end + centroids = new_centroids + --echo(JSON.encode(centroids)) + --echo(JSON.encode(old_centroids)) + similarity = doclassify.mean_similarity(centroids, old_centroids) + echo("step #"..step..", similarity "..similarity) + step = step - 1 + end + local results = {} + for i = 1,nclass do + local list = {} + local cnt = 0 + for id,c in pairs(clusters) do + if c == i then + cnt = cnt + 1 + list[cnt] = id + end + end + results[i] = list + end + return results, clusters, centroids +end + +doclassify.zeros = function(n) + local vector = {} + for i = 1,n do vector[i] = 0.0 end + return vector +end + +doclassify.random = function(n,maxv) + local vector = {} + for i=1,n do + vector[i] = math.random() + math.random(0, maxv) + end + return vector +end + +doclassify.sum = function(v) + local sum = 0.0 + for i=1,#v do sum = sum + v[i] end + return sum +end + +doclassify.mean = function(v) + return doclassify.sum(v)/#v + +end + +doclassify.mean_vector = function(vectors, size) + local means = doclassify.zeros(size) + if not vectors or #vectors == 0 then return means end + --local size = #(vectors[1]) + local times = 0 + for k,v in pairs(vectors) do + for i=1,#v do means[i] = means[i] + v[i] end + times = times + 1 + end + for i = 1,size do means[i] = means[i]/times end + return means +end + +doclassify.argmin = function(v) + local minv = 0.0 + local mini = 0.0 + for i = 1,#v do + if v[i] <= minv then + mini = i + minv = v[i] + end + end + --echo("min index"..mini.." val "..minv) + return mini, minv +end + +doclassify.argmax = function(v) + local maxv = 0.0 + local maxi = 0.0 + for i = 1,#v do + if v[i] >= maxv then + maxi = i + maxv = v[i] + end + end + return maxi,maxv +end + +return doclassify diff --git a/Blogger/build/debug/api/ai/stopwords.txt b/Blogger/build/debug/api/ai/stopwords.txt new file mode 100644 index 0000000..abe0cef --- /dev/null +++ b/Blogger/build/debug/api/ai/stopwords.txt @@ -0,0 +1,151 @@ +i +me +my +myself +we +our +ours +ourselves +you +your +yours +yourself +yourselves +he +him +his +himself +she +her +hers +herself +it +its +itself +they +them +their +theirs +themselves +what +which +who +whom +this +that +these +those +am +is +are +was +were +be +been +being +have +has +had +having +do +does +did +doing +a +an +the +and +but +if +or +because +as +until +while +of +at +by +for +with +about +against +between +into +through +during +before +after +above +below +to +from +up +down +in +out +on +off +over +under +again +further +then +once +here +there +when +where +why +how +all +any +both +each +few +more +most +other +some +such +no +nor +not +only +own +same +so +than +too +very +s +t +can +will +just +don +should +now +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +w +r +s +t +x +y +z \ No newline at end of file diff --git a/Blogger/build/debug/api/ai/test.lua b/Blogger/build/debug/api/ai/test.lua new file mode 100644 index 0000000..21573a6 --- /dev/null +++ b/Blogger/build/debug/api/ai/test.lua @@ -0,0 +1,50 @@ +local path = require("fs/vfs").ospath("home://aiws/blog-clustering") +local gettext = loadfile(path.."/gettext.lua")() +local cluster = loadfile(path.."/cluster.lua")() + +local refresh = false + +local file = "/home/mrsang/test.csv" +if refresh then + local data = gettext.get({publish=1}) + local documents = {} + if data then + local sw = gettext.stopwords("home://aiws/blog-clustering/stopwords.txt") + for k,v in pairs(data) do + local bag = cluster.bow(data[k].content, sw) + documents[data[k].id] = bag + end + cluster.tfidf(documents) + --local v = cluster.search("arm", documents) + --echo(JSON.encode(v)) + local vectors, maxv, size = cluster.get_vectors(documents) + local s = cluster.save_topchart(vectors,file, 3) + if s then echo("file saved") else echo("error save file") end + --echo(JSON.encode(r)) + --r = cluster.similarity(vectors["14"],vectors["16"]) + --echo("Similarity "..r) + + --local c,l = cluster.kmean(3, documents, 10) + --echo(JSON.encode(c)) + --echo(JSON.encode(l)) + else + echo("Data missing") + end +else + local f = io.open(file,"r") + local result = {} + for line in f:lines() do + local arr = {} + local cnt = 0 + for i in line:gmatch( "%S+") do + cnt = cnt + 1 + arr[cnt] = i + end + if not result[arr[1]] then result[arr[1]] = {} end + result[arr[1]][arr[2]] = tonumber(arr[3]) + end + f:close() + echo(JSON.encode(result)) + --local r = cluster.top_similarity("2",vectors, 3) + --echo(JSON.encode(r)) +end \ No newline at end of file diff --git a/Blogger/build/debug/sendmail.lua b/Blogger/build/debug/api/sendmail.lua similarity index 100% rename from Blogger/build/debug/sendmail.lua rename to Blogger/build/debug/api/sendmail.lua diff --git a/Blogger/build/debug/main.js b/Blogger/build/debug/main.js index b7ca0e6..0d4b35d 100644 --- a/Blogger/build/debug/main.js +++ b/Blogger/build/debug/main.js @@ -1 +1 @@ -var OS;!function(t){let e;!function(t){class e extends t.BaseApplication{constructor(t){super("Blogger",t),this.previewOn=!1}async init_db(){try{const e=await this.openDialog("FileDialog",{title:__("Open/create new database"),file:"Untitled.db"});var t=e.file.path.asFileHandle();"file"===e.file.type&&(t=t.parent());const i=`${t.path}/${e.name}`.asFileHandle();this.dbhandle=("sqlite://"+i.genealogy.join("/")).asFileHandle();const a=await this.dbhandle.read();if(!a.user){this.dbhandle.cache={address:"TEXT",Phone:"TEXT",shortbiblio:"TEXT",fullname:"TEXT",email:"TEXT",url:"TEXT",photo:"TEXT"};const t=await this.dbhandle.write("user");if(t.error)throw new Error(t.error)}if(!a.cv_cat){this.dbhandle.cache={publish:"NUMERIC",name:"TEXT",pid:"NUMERIC"};const t=await this.dbhandle.write("cv_cat");if(t.error)throw new Error(t.error)}if(!a.cv_sections){this.dbhandle.cache={title:"TEXT",start:"NUMERIC",location:"TEXT",end:"NUMERIC",content:"TEXT",subtitle:"TEXT",publish:"NUMERIC",cid:"NUMERIC"};const t=await this.dbhandle.write("cv_sections");if(t.error)throw new Error(t.error)}if(!a.blogs){this.dbhandle.cache={tags:"TEXT",content:"TEXT",utime:"NUMERIC",rendered:"TEXT",title:"TEXT",utimestr:"TEXT",ctime:"NUMERIC",ctimestr:"TEXT",publish:"INTEGER DEFAULT 0"};const t=await this.dbhandle.write("blogs");if(t.error)throw new Error(t.error)}if(!a.st_similarity){this.dbhandle.cache={pid:"NUMERIC",sid:"NUMERIC",score:"NUMERIC"};const t=await this.dbhandle.write("st_similarity");if(t.error)throw new Error(t.error)}if(!a.subscribers){this.dbhandle.cache={name:"TEXT",email:"TEXT"};const t=await this.dbhandle.write("subscribers");if(t.error)throw new Error(t.error)}this.userdb=(this.dbhandle.path+"@user").asFileHandle(),this.cvcatdb=(this.dbhandle.path+"@cv_cat").asFileHandle(),this.cvsecdb=(this.dbhandle.path+"@cv_sections").asFileHandle(),this.blogdb=(this.dbhandle.path+"@blogs").asFileHandle(),this.subdb=(this.dbhandle.path+"@subscribers").asFileHandle(),this.last_ctime=0,this.bloglist.data=[],this.loadBlogs()}catch(t){this.error(__("Unable to init database file: {0}",t.toString()),t),this.dbhandle=void 0}}menu(){return[{text:"__(Open/Create database)",onmenuselect:t=>{this.init_db()}}]}main(){this.user={},this.cvlist=this.find("cv-list"),this.cvlist.ontreeselect=t=>{if(!t)return;const{data:e}=t.data.item;return this.CVSectionByCID(Number(e.id))},this.inputtags=this.find("input-tags"),this.bloglist=this.find("blog-list"),this.seclist=this.find("cv-sec-list");let e=this.find("photo");return $(e).on("click",async t=>{try{const t=await this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]});return e.value=t.file.path}catch(t){return this.error(__("Unable to get file"),t)}}),this.tabcontainer=this.find("tabcontainer"),this.tabcontainer.ontabselect=t=>this.fetchData(t.data.container.aid),this.find("bt-user-save").onbtclick=t=>this.saveUser(),this.find("blog-load-more").onbtclick=t=>{this.loadBlogs()},this.find("cv-cat-add").onbtclick=async e=>{try{const e=await this.fetchCVCat(),i=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Add category"),tree:e});this.cvcatdb.cache={name:i.value,pid:i.p.id,publish:1};const a=await this.cvcatdb.write(void 0);if(a.error)throw new Error(a.error);await this.refreshCVCat()}catch(e){this.error(__("cv-cat-add: {0}",e.toString()),e)}},this.find("cv-cat-edit").onbtclick=async e=>{try{const e=this.cvlist.selectedItem;if(!e)return;const i=e.data;if(!i)return;const a=await this.fetchCVCat(),s=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Edit category"),tree:a,cat:i}),n=i.$vfs;n.cache={id:i.id,publish:i.publish,pid:s.p.id,name:s.value};const r=await n.write(void 0);if(r.error)throw new Error(r.error);await this.refreshCVCat()}catch(e){this.error(__("cv-cat-edit: {0}",e.toString()),e)}},this.find("cv-cat-del").onbtclick=async t=>{try{const t=this.cvlist.selectedItem;if(!t)return;const e=t.data;if(!e)return;if(!await this.openDialog("YesNoDialog",{title:__("Delete category"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.name)}))return;await this.deleteCVCat(e)}catch(t){this.error(__("cv-cat-del: {0}",t.toString()),t)}},this.find("cv-sec-add").onbtclick=async e=>{try{const e=this.cvlist.selectedItem;if(!e)return;const i=e.data;if(!i||"0"===i.id)return this.toast(__("Please select a category"));const a=await this.openDialog(new t.blogger.BloggerCVSectionDiaglog,{title:__("New section entry for {0}",i.name)});a.cid=Number(i.id),a.start=Number(a.start),a.end=Number(a.end),this.cvsecdb.cache=a;const s=await this.cvsecdb.write(void 0);if(s.error)throw new Error(s.error);await this.CVSectionByCID(Number(i.id))}catch(e){this.error(__("cv-sec-add: {0}",e.toString()),e)}},this.find("cv-sec-move").onbtclick=async e=>{try{const e=this.seclist.selectedItem;if(!e)return this.toast(__("Please select a section to move"));const i=e.data,a=i.$vfs;console.log(a);const s=await this.fetchCVCat(),n=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Move to"),tree:s,selonly:!0});a.cache={id:i.id,cid:n.p.id};const r=await a.write(void 0);if(r.error)throw new Error(r.error);await this.CVSectionByCID(i.cid),this.seclist.unselect()}catch(e){this.error(__("cv-sec-move: {0}",e.toString()),e)}},this.find("cv-sec-edit").onbtclick=async e=>{try{const e=this.seclist.selectedItem;if(!e)return this.toast(__("Please select a section to edit"));const i=e.data,a=await this.openDialog(new t.blogger.BloggerCVSectionDiaglog,{title:__("Modify section entry"),section:i});a.cid=Number(i.cid),a.start=Number(a.start),a.end=Number(a.end);const s=i.$vfs;s.cache=a;const n=await s.write(void 0);if(n.error)throw new Error(n.error);await this.CVSectionByCID(Number(i.cid))}catch(e){this.error(__("cv-sec-edit: {0}",e.toString()),e)}},this.seclist.onitemclose=t=>{if(!t)return;const e=t.data.item.data;return this.openDialog("YesNoDialog",{iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.title)}).then(async i=>{if(i)try{const i=await this.cvsecdb.remove({where:{id:e.id}});if(i.error)throw new Error(i.error);return this.seclist.delete(t.data.item)}catch(t){return this.error(__("Cannot delete the section: {0}",t.toString()),t)}}),!1},this.editor=new EasyMDE({element:this.find("markarea"),autoDownloadFontAwesome:!1,autofocus:!0,tabSize:4,indentWithTabs:!0,toolbar:[{name:__("New"),className:"fa fa-file",action:t=>(this.bloglist.unselect(),this.clearEditor())},{name:__("Save"),className:"fa fa-save",action:t=>this.saveBlog()},"|","bold","italic","heading","|","quote","code","unordered-list","ordered-list","|","link","image","table","horizontal-rule",{name:"image",className:"fa fa-file-image-o",action:t=>this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]}).then(t=>t.file.path.asFileHandle().publish().then(t=>this.editor.codemirror.getDoc().replaceSelection(`![](${this._api.handle.shared}/${t.result})`)).catch(t=>this.error(__("Cannot export file for embedding to text"),t)))},{name:"Youtube",className:"fa fa-youtube",action:t=>this.editor.codemirror.getDoc().replaceSelection("[[youtube:]]")},"|",{name:__("Preview"),className:"fa fa-eye no-disable",action:t=>{this.previewOn=!this.previewOn,EasyMDE.togglePreview(t),renderMathInElement(this.find("editor-container"))}},"|",{name:__("Send mail"),className:"fa fa-paper-plane",action:async e=>{try{const e=await this.subdb.read(),i=this.bloglist.selectedItem;if(!i)return this.error(__("No post selected"));const a=i.data;await this.openDialog(new t.blogger.BloggerSendmailDiaglog,{title:__("Send mail"),content:this.editor.value(),mails:e,id:a.id}),this.toast(__("Emails sent"))}catch(e){this.error(__("Error sending mails: {0}",e.toString()),e)}}}]}),this.bloglist.onlistselect=async t=>{const e=this.bloglist.selectedItem;if(!e)return;const i=e.data;if(i)try{const t=await this.blogdb.read({where:{id:Number(i.id)}});if(!t||0==t.length)throw new Error(__("No record found for ID {}",i.id).__());const e=t[0];return this.editor.value(e.content),this.inputtags.value=e.tags,this.find("blog-publish").swon=!!Number(e.publish)}catch(t){return this.error(__("Cannot fetch the entry content"),t)}},this.bloglist.onitemclose=t=>{if(!t)return;const e=t.data.item,i=e.data;return this.openDialog("YesNoDialog",{title:__("Delete a post"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete this post ?")}).then(async t=>{if(!t)return;const a=await this.blogdb.remove({where:{id:Number(i.id)}});if(a.error)throw new Error(a.error);return this.bloglist.delete(e),this.bloglist.unselect(),this.clearEditor()}),!1},this.bindKey("CTRL-S",()=>{const t=this.tabcontainer.selectedTab;if(t&&"blog-container"===t.container.aid)return this.saveBlog()}),this.on("resize",()=>this.resizeContent()),this.resizeContent(),this.init_db()}fetchData(t){switch(t){case"user-container":return this.userdb.read().then(t=>{if(t&&0!=t.length)return this.user=t[0],this.select("[input-class='user-input']").map((t,e)=>$(e).val(this.user[e.name]))}).catch(t=>this.error(__("Cannot fetch user data"),t));case"cv-container":return this.refreshCVCat();default:return this.last_ctime=0,this.bloglist.data=[],this.loadBlogs()}}async saveUser(){try{const t=this.select("[input-class='user-input']");for(let e of t)this.user[e.name]=$(e).val();if(!this.user.fullname||""===this.user.fullname)return this.toast(__("Full name must be entered"));let e=this.userdb;this.user&&this.user.id&&(e=`${this.userdb.path}@${this.user.id}`.asFileHandle()),e.cache=this.user;const i=await e.write(void 0);if(i.error)throw new Error(i.error);this.user.id||(this.user.id=i.result),this.toast(__("User data updated"))}catch(t){this.error(__("Cannot save user data: {0}",t.toString()),t)}}refreshCVCat(){return this.fetchCVCat().then(t=>(this.cvlist.data=t,this.cvlist.expandAll())).catch(t=>this.error(__("Unable to load categories"),t))}fetchCVCat(){return new Promise(async(t,e)=>{try{const e={text:"Porfolio",id:0,nodes:[]},i={order:["name$asc"]},a=await this.cvcatdb.read(i);this.catListToTree(a,e,0),t(e)}catch(t){e(__e(t))}})}catListToTree(t,e,i){const a=t.filter(t=>t.pid==i);if(0===a.length)return e.nodes=null;for(let i of a)i.nodes=[],i.text=i.name,this.catListToTree(t,i,i.id),e.nodes.push(i)}deleteCVCat(t){return new Promise(async(e,i)=>{try{const e=[];var a=function(t){e.push(t.id),t.nodes&&t.nodes.map(t=>a(t))};a(t);let i=await this.cvsecdb.remove({where:{$or:{cid:e}}});if(i.error)throw new Error(i.error);if(i=await this.cvcatdb.remove({where:{$or:{id:e}}}),i.error)throw new Error(i.error);await this.refreshCVCat(),this.seclist.data=[]}catch(t){i(__e(t))}})}CVSectionByCID(t){return new Promise(async(e,i)=>{try{const e=await this.cvsecdb.read({where:{cid:t},order:["start$desc"]}),i=[];this.find("cv-sec-status").text=__("Found {0} sections",e.length);for(let t of e)t.closable=!0,t.tag="afx-blogger-cvsection-item",t.start=Number(t.start),t.end=Number(t.end),t.start<1e3&&(t.start=void 0),t.end<1e3&&(t.end=void 0),i.push(t);this.seclist.data=i}catch(t){i(__e(t))}})}async saveBlog(){try{let t=void 0;const e=this.bloglist.selectedItem;e&&(t=e.data);const i=this.inputtags.value,a=this.editor.value(),s=new RegExp("^#+(.*)\n","g").exec(a);if(!s||2!==s.length)return this.toast(__("Please insert a title in the text: beginning with heading"));if(""===i)return this.toast(__("Please enter tags"));const n=new Date,r={content:a,title:s[1].trim(),tags:i,ctime:t?t.ctime:n.timestamp(),ctimestr:t?t.ctimestr:n.toString(),utime:n.timestamp(),utimestr:n.toString(),rendered:this.process(this.editor.options.previewRender(a)),publish:this.find("blog-publish").swon?1:0};let o=this.blogdb;t&&(r.id=t.id,o=t.$vfs),o.cache=r;const l=await o.write(void 0);if(l.error)throw new Error(l.error);t?e.data=r:(this.last_ctime=0,this.bloglist.data=[],await this.loadBlogs())}catch(t){this.error(__("Cannot save blog: {0}",t.toString()),t)}}process(t){let e;const i=/\[\[youtube:([^\]]*)\]\]/g,a=[];for(;null!==(e=i.exec(t));)a.push(e);if(!(a.length>0))return t;let s="",n=0;for(let e of a)s+=t.substring(n,e.index),s+=``,n=e.index+e[0].length;return s+=t.substring(n,t.length),s}clearEditor(){return this.editor.value(""),this.inputtags.value="",this.find("blog-publish").swon=!1}loadBlogs(){return new Promise(async(t,e)=>{try{const t={order:["ctime$desc"],fields:["id","title","ctimestr","ctime","utime","utimestr"],limit:10};this.last_ctime&&(t.where={ctime$lt:this.last_ctime});const e=await this.blogdb.read(t);if(0==e.length)return void this.toast(__("No more record to load"));this.last_ctime=e[e.length-1].ctime;for(let t of e)t.tag="afx-blogger-post-item",this.bloglist.push(t);return this.clearEditor(),this.bloglist.selected=-1}catch(t){e(__e(t))}})}resizeContent(){const t=this.find("editor-container"),e=$(".EasyMDEContainer",t).children(),i=$(this.scheme).find(".afx-window-top")[0],a=e[0],s=e[3],n=$(this.scheme).height()-$(i).height()-$(a).height()-$(s).height()-90;return $(e[1]).css("height",n+"px")}}t.Blogger=e,e.singleton=!0,e.dependencies=["pkg://SimpleMDE/main.js","pkg://SimpleMDE/main.css","pkg://Katex/main.js","pkg://Katex/main.css","pkg://SQLiteDB/libsqlite.js"]}(e=t.application||(t.application={}))}(OS||(OS={})),function(t){let e;!function(e){let i;!function(e){class i extends t.GUI.BasicDialog{constructor(){super("BloggerCategoryDialog",i.scheme)}main(){if(super.main(),this.tree=this.find("tree"),this.txtinput=this.find("txtinput"),this.find("bt-ok").onbtclick=t=>{const e=this.tree.selectedItem;if(!e)return this.notify(__("Please select a parent category"));const i=e.data,a=this.txtinput.value;return""!==a||this.data.selonly?this.data.cat&&this.data.cat.id===i.id?this.notify(__("Parent can not be the category itself")):(this.handle&&this.handle({p:i,value:a}),this.quit()):this.notify(__("Please enter category name"))},this.find("bt-cancel").onbtclick=t=>this.quit(),this.data&&this.data.tree){if(this.data&&this.data.cat){let t;this.txtinput.value=this.data.cat.name,t="0"===this.data.cat.pid?this.data.tree:this.findDataByID(this.data.cat.pid,this.data.tree.nodes),t&&(t.selected=!0)}return this.tree.data=this.data.tree,this.tree.expandAll()}}findDataByID(t,e){for(let i of e){if(i.id===t)return i;i.nodes&&this.findDataByID(t,i.nodes)}}}e.BloggerCategoryDialog=i,i.scheme='\n \n \n \n \n \n \n
\n \n \n
\n
\n
\n
s\n ';class a extends t.GUI.BasicDialog{constructor(){super("BloggerCVSectionDiaglog")}main(){super.main(),this.editor=new EasyMDE({autoDownloadFontAwesome:!1,element:this.find("contentarea"),status:!1,toolbar:!1}),$(this.select('[class = "CodeMirror-scroll"]')[0]).css("min-height","50px"),$(this.select('[class="CodeMirror cm-s-paper CodeMirror-wrap"]')[0]).css("min-height","50px");const t=this.select("[input-class='user-input']");if(this.data&&this.data.section)for(let e of t)$(e).val(this.data.section[e.name]);return this.data&&this.data.section&&this.editor.value(this.data.section.content),this.find("section-publish").swon=!!(this.data&&this.data.section&&Number(this.data.section.publish)),this.find("bt-cv-sec-save").onbtclick=e=>{const i={};for(let e of t)i[e.name]=$(e).val();if(i.content=this.editor.value(),""===i.title&&""===i.content)return this.notify(__("Title or content must not be blank"));this.data&&this.data.section&&(i.id=this.data.section.id);const a=this.find("section-publish").swon;return i.publish=!0===a?1:0,this.handle&&this.handle(i),this.quit()},this.on("resize",()=>this.resizeContent()),this.resizeContent()}resizeContent(){const t=this.find("editor-container"),e=$(".EasyMDEContainer",t).children(),i=$(t).height()-30;return $(e[0]).css("height",i+"px")}}e.BloggerCVSectionDiaglog=a,a.scheme='\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n
\n \n \n
\n
\n
';class s extends t.GUI.BasicDialog{constructor(){super("BloggerSendmailDiaglog")}main(){super.main(),this.maillinglist=this.find("email-list");const t=new RegExp("^#+(.*)\n","g").exec(this.data.content);this.find("mail-title").value=t[1];const e=this.data.content.substring(0,500)+"...";this.find("contentarea").value=s.template.format(this.data.id,e);const i=this.data.mails.map(t=>({text:t.name,email:t.email,switch:!0,checked:!0}));return console.log(i),this.maillinglist.items=i,this.find("bt-sendmail").onbtclick=t=>{const e=this.maillinglist.items,i=[];for(let t of e)!0===t.checked&&i.push(t);if(0===i.length)return this.notify(__("No email selected"));const a={path:this.meta().path+"/sendmail.lua",parameters:{to:i,title:this.find("mail-title").value,content:this.find("contentarea").value,user:this.find("mail-user").value,password:this.find("mail-password").value}};return this._api.apigateway(a,!1).then(t=>{if(t.error){const e=t.result.join(",");return this.notify(__("Unable to send mail to: {0}",e))}return this.quit()}).catch(t=>this.error(__("Error sending mail: {0}",t.toString()),t))}}}e.BloggerSendmailDiaglog=s,s.scheme='\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n
\n
\n
',s.template="Hello,\n\nDany LE has just published a new post on his blog: https://blog.iohub.dev/post/id/{0}\n\n==========\n{1}\n==========\n\n\nRead the full article via:\nhttps://blog.iohub.dev/post/id/{0}\n\nYou receive this email because you have been subscribed to his blog.\n\nHave a nice day,\n\nSent from Blogger, an AntOS application"}(i=e.blogger||(e.blogger={}))}(e=t.application||(t.application={}))}(OS||(OS={})),function(t){let e;!function(e){let i;!function(e){class i extends t.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){if(!this.data)return;const t=this.data,e=["content","start","end"];return this.closable=t.closable,(()=>{const i=[];for(let a in this.refs){const s=this.refs[a];t[a]&&""!==t[a]?e.includes(a)?i.push($(s).text(t[a])):i.push(s.text=t[a]):i.push(void 0)}return i})()}reload(){}init(){}itemlayout(){return{el:"div",children:[{el:"afx-label",ref:"title",class:"afx-cv-sec-title"},{el:"afx-label",ref:"subtitle",class:"afx-cv-sec-subtitle"},{el:"p",ref:"content",class:"afx-cv-sec-content"},{el:"p",class:"afx-cv-sec-period",children:[{el:"i",ref:"start"},{el:"i",ref:"end",class:"period-end"}]},{el:"afx-label",ref:"location",class:"afx-cv-sec-loc"}]}}}t.GUI.tag.define("afx-blogger-cvsection-item",i);class a extends t.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){if(!this.data)return;const t=this.data;t.closable=!0,this.closable=t.closable,this.refs.title.text=t.title,this.refs.ctimestr.text=__("Created: {0}",t.ctimestr),this.refs.utimestr.text=__("Updated: {0}",t.utimestr)}reload(){}init(){}itemlayout(){return{el:"div",children:[{el:"afx-label",ref:"title",class:"afx-blogpost-title"},{el:"afx-label",ref:"ctimestr",class:"blog-dates"},{el:"afx-label",ref:"utimestr",class:"blog-dates"}]}}}t.GUI.tag.define("afx-blogger-post-item",a)}(i=e.blogger||(e.blogger={}))}(e=t.application||(t.application={}))}(OS||(OS={})); \ No newline at end of file +var OS;!function(t){let e;!function(t){class e extends t.BaseApplication{constructor(t){super("Blogger",t),this.previewOn=!1}async init_db(){try{const e=await this.openDialog("FileDialog",{title:__("Open/create new database"),file:"Untitled.db"});var t=e.file.path.asFileHandle();"file"===e.file.type&&(t=t.parent());const i=`${t.path}/${e.name}`.asFileHandle();this.dbhandle=("sqlite://"+i.genealogy.join("/")).asFileHandle();const a=await this.dbhandle.read();if(!a.user){this.dbhandle.cache={address:"TEXT",Phone:"TEXT",shortbiblio:"TEXT",fullname:"TEXT",email:"TEXT",url:"TEXT",photo:"TEXT"};const t=await this.dbhandle.write("user");if(t.error)throw new Error(t.error)}if(!a.cv_cat){this.dbhandle.cache={publish:"NUMERIC",name:"TEXT",pid:"NUMERIC"};const t=await this.dbhandle.write("cv_cat");if(t.error)throw new Error(t.error)}if(!a.cv_sections){this.dbhandle.cache={title:"TEXT",start:"NUMERIC",location:"TEXT",end:"NUMERIC",content:"TEXT",subtitle:"TEXT",publish:"NUMERIC",cid:"NUMERIC"};const t=await this.dbhandle.write("cv_sections");if(t.error)throw new Error(t.error)}if(!a.blogs){this.dbhandle.cache={tags:"TEXT",content:"TEXT",utime:"NUMERIC",rendered:"TEXT",title:"TEXT",utimestr:"TEXT",ctime:"NUMERIC",ctimestr:"TEXT",publish:"INTEGER DEFAULT 0"};const t=await this.dbhandle.write("blogs");if(t.error)throw new Error(t.error)}if(!a.st_similarity){this.dbhandle.cache={pid:"NUMERIC",sid:"NUMERIC",score:"NUMERIC"};const t=await this.dbhandle.write("st_similarity");if(t.error)throw new Error(t.error)}if(!a.subscribers){this.dbhandle.cache={name:"TEXT",email:"TEXT"};const t=await this.dbhandle.write("subscribers");if(t.error)throw new Error(t.error)}this.userdb=(this.dbhandle.path+"@user").asFileHandle(),this.cvcatdb=(this.dbhandle.path+"@cv_cat").asFileHandle(),this.cvsecdb=(this.dbhandle.path+"@cv_sections").asFileHandle(),this.blogdb=(this.dbhandle.path+"@blogs").asFileHandle(),this.subdb=(this.dbhandle.path+"@subscribers").asFileHandle(),this.last_ctime=0,this.bloglist.data=[],this.loadBlogs()}catch(t){this.error(__("Unable to init database file: {0}",t.toString()),t),this.dbhandle=void 0}}menu(){return[{text:"__(Open/Create database)",onmenuselect:t=>{this.init_db()}}]}main(){this.user={},this.cvlist=this.find("cv-list"),this.cvlist.ontreeselect=t=>{if(!t)return;const{data:e}=t.data.item;return this.CVSectionByCID(Number(e.id))},this.inputtags=this.find("input-tags"),this.bloglist=this.find("blog-list"),this.seclist=this.find("cv-sec-list");let e=this.find("photo");return $(e).on("click",async t=>{try{const t=await this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]});return e.value=t.file.path}catch(t){return this.error(__("Unable to get file"),t)}}),this.tabcontainer=this.find("tabcontainer"),this.tabcontainer.ontabselect=t=>this.fetchData(t.data.container.aid),this.find("bt-user-save").onbtclick=t=>this.saveUser(),this.find("blog-load-more").onbtclick=t=>{this.loadBlogs()},this.find("cv-cat-add").onbtclick=async e=>{try{const e=await this.fetchCVCat(),i=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Add category"),tree:e});this.cvcatdb.cache={name:i.value,pid:i.p.id,publish:1};const a=await this.cvcatdb.write(void 0);if(a.error)throw new Error(a.error);await this.refreshCVCat()}catch(e){this.error(__("cv-cat-add: {0}",e.toString()),e)}},this.find("cv-cat-edit").onbtclick=async e=>{try{const e=this.cvlist.selectedItem;if(!e)return;const i=e.data;if(!i)return;const a=await this.fetchCVCat(),s=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Edit category"),tree:a,cat:i}),n=i.$vfs;n.cache={id:i.id,publish:i.publish,pid:s.p.id,name:s.value};const r=await n.write(void 0);if(r.error)throw new Error(r.error);await this.refreshCVCat()}catch(e){this.error(__("cv-cat-edit: {0}",e.toString()),e)}},this.find("cv-cat-del").onbtclick=async t=>{try{const t=this.cvlist.selectedItem;if(!t)return;const e=t.data;if(!e)return;if(!await this.openDialog("YesNoDialog",{title:__("Delete category"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.name)}))return;await this.deleteCVCat(e)}catch(t){this.error(__("cv-cat-del: {0}",t.toString()),t)}},this.find("cv-sec-add").onbtclick=async e=>{try{const e=this.cvlist.selectedItem;if(!e)return;const i=e.data;if(!i||"0"===i.id)return this.toast(__("Please select a category"));const a=await this.openDialog(new t.blogger.BloggerCVSectionDiaglog,{title:__("New section entry for {0}",i.name)});a.cid=Number(i.id),a.start=Number(a.start),a.end=Number(a.end),this.cvsecdb.cache=a;const s=await this.cvsecdb.write(void 0);if(s.error)throw new Error(s.error);await this.CVSectionByCID(Number(i.id))}catch(e){this.error(__("cv-sec-add: {0}",e.toString()),e)}},this.find("cv-sec-move").onbtclick=async e=>{try{const e=this.seclist.selectedItem;if(!e)return this.toast(__("Please select a section to move"));const i=e.data,a=i.$vfs;console.log(a);const s=await this.fetchCVCat(),n=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Move to"),tree:s,selonly:!0});a.cache={id:i.id,cid:n.p.id};const r=await a.write(void 0);if(r.error)throw new Error(r.error);await this.CVSectionByCID(i.cid),this.seclist.unselect()}catch(e){this.error(__("cv-sec-move: {0}",e.toString()),e)}},this.find("cv-sec-edit").onbtclick=async e=>{try{const e=this.seclist.selectedItem;if(!e)return this.toast(__("Please select a section to edit"));const i=e.data,a=await this.openDialog(new t.blogger.BloggerCVSectionDiaglog,{title:__("Modify section entry"),section:i});a.cid=Number(i.cid),a.start=Number(a.start),a.end=Number(a.end);const s=i.$vfs;s.cache=a;const n=await s.write(void 0);if(n.error)throw new Error(n.error);await this.CVSectionByCID(Number(i.cid))}catch(e){this.error(__("cv-sec-edit: {0}",e.toString()),e)}},this.seclist.onitemclose=t=>{if(!t)return;const e=t.data.item.data;return this.openDialog("YesNoDialog",{iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.title)}).then(async i=>{if(i)try{const i=await this.cvsecdb.remove({where:{id:e.id}});if(i.error)throw new Error(i.error);return this.seclist.delete(t.data.item)}catch(t){return this.error(__("Cannot delete the section: {0}",t.toString()),t)}}),!1},this.editor=new EasyMDE({element:this.find("markarea"),autoDownloadFontAwesome:!1,autofocus:!0,tabSize:4,indentWithTabs:!0,toolbar:[{name:__("New"),className:"fa fa-file",action:t=>(this.bloglist.unselect(),this.clearEditor())},{name:__("Save"),className:"fa fa-save",action:t=>this.saveBlog()},"|","bold","italic","heading","|","quote","code","unordered-list","ordered-list","|","link","image","table","horizontal-rule",{name:"image",className:"fa fa-file-image-o",action:t=>this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]}).then(t=>t.file.path.asFileHandle().publish().then(t=>this.editor.codemirror.getDoc().replaceSelection(`![](${this._api.handle.shared}/${t.result})`)).catch(t=>this.error(__("Cannot export file for embedding to text"),t)))},{name:"Youtube",className:"fa fa-youtube",action:t=>this.editor.codemirror.getDoc().replaceSelection("[[youtube:]]")},"|",{name:__("Preview"),className:"fa fa-eye no-disable",action:t=>{this.previewOn=!this.previewOn,EasyMDE.togglePreview(t),renderMathInElement(this.find("editor-container"))}},"|",{name:__("Send mail"),className:"fa fa-paper-plane",action:async e=>{try{const e=await this.subdb.read(),i=this.bloglist.selectedItem;if(!i)return this.error(__("No post selected"));const a=i.data;await this.openDialog(new t.blogger.BloggerSendmailDiaglog,{title:__("Send mail"),content:this.editor.value(),mails:e,id:a.id}),this.toast(__("Emails sent"))}catch(e){this.error(__("Error sending mails: {0}",e.toString()),e)}}},"|",{name:__("TFIDF analyse"),className:"fa fa-area-chart",action:async t=>{try{const t=await this.openDialog("PromptDialog",{title:__("TFIDF Analyse"),text:__("Max number of related posts to keep per post?"),value:"5"}),e={path:this.meta().path+"/api/ai/analyse.lua",parameters:{dbpath:this.dbhandle.info.file.path,top:parseInt(t)}},i=await this._api.apigateway(e,!1);if(i.error)throw new Error(i.error);this.toast(i.result)}catch(t){this.error(__("Error analysing posts: {0}",t.toString()),t)}}}]}),this.bloglist.onlistselect=async t=>{const e=this.bloglist.selectedItem;if(!e)return;const i=e.data;if(i)try{const t=await this.blogdb.read({where:{id:Number(i.id)}});if(!t||0==t.length)throw new Error(__("No record found for ID {}",i.id).__());const e=t[0];return this.editor.value(e.content),this.inputtags.value=e.tags,this.find("blog-publish").swon=!!Number(e.publish)}catch(t){return this.error(__("Cannot fetch the entry content"),t)}},this.bloglist.onitemclose=t=>{if(!t)return;const e=t.data.item,i=e.data;return this.openDialog("YesNoDialog",{title:__("Delete a post"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete this post ?")}).then(async t=>{if(!t)return;const a=await this.blogdb.remove({where:{id:Number(i.id)}});if(a.error)throw new Error(a.error);return this.bloglist.delete(e),this.bloglist.unselect(),this.clearEditor()}),!1},this.bindKey("CTRL-S",()=>{const t=this.tabcontainer.selectedTab;if(t&&"blog-container"===t.container.aid)return this.saveBlog()}),this.on("resize",()=>this.resizeContent()),this.resizeContent(),this.init_db()}fetchData(t){switch(t){case"user-container":return this.userdb.read().then(t=>{if(t&&0!=t.length)return this.user=t[0],this.select("[input-class='user-input']").map((t,e)=>$(e).val(this.user[e.name]))}).catch(t=>this.error(__("Cannot fetch user data"),t));case"cv-container":return this.refreshCVCat();default:return this.last_ctime=0,this.bloglist.data=[],this.loadBlogs()}}async saveUser(){try{const t=this.select("[input-class='user-input']");for(let e of t)this.user[e.name]=$(e).val();if(!this.user.fullname||""===this.user.fullname)return this.toast(__("Full name must be entered"));let e=this.userdb;this.user&&this.user.id&&(e=`${this.userdb.path}@${this.user.id}`.asFileHandle()),e.cache=this.user;const i=await e.write(void 0);if(i.error)throw new Error(i.error);this.user.id||(this.user.id=i.result),this.toast(__("User data updated"))}catch(t){this.error(__("Cannot save user data: {0}",t.toString()),t)}}refreshCVCat(){return this.fetchCVCat().then(t=>(this.cvlist.data=t,this.cvlist.expandAll())).catch(t=>this.error(__("Unable to load categories"),t))}fetchCVCat(){return new Promise(async(t,e)=>{try{const e={text:"Porfolio",id:0,nodes:[]},i={order:["name$asc"]},a=await this.cvcatdb.read(i);this.catListToTree(a,e,0),t(e)}catch(t){e(__e(t))}})}catListToTree(t,e,i){const a=t.filter(t=>t.pid==i);if(0===a.length)return e.nodes=null;for(let i of a)i.nodes=[],i.text=i.name,this.catListToTree(t,i,i.id),e.nodes.push(i)}deleteCVCat(t){return new Promise(async(e,i)=>{try{const e=[];var a=function(t){e.push(t.id),t.nodes&&t.nodes.map(t=>a(t))};a(t);let i=await this.cvsecdb.remove({where:{$or:{cid:e}}});if(i.error)throw new Error(i.error);if(i=await this.cvcatdb.remove({where:{$or:{id:e}}}),i.error)throw new Error(i.error);await this.refreshCVCat(),this.seclist.data=[]}catch(t){i(__e(t))}})}CVSectionByCID(t){return new Promise(async(e,i)=>{try{const e=await this.cvsecdb.read({where:{cid:t},order:["start$desc"]}),i=[];this.find("cv-sec-status").text=__("Found {0} sections",e.length);for(let t of e)t.closable=!0,t.tag="afx-blogger-cvsection-item",t.start=Number(t.start),t.end=Number(t.end),t.start<1e3&&(t.start=void 0),t.end<1e3&&(t.end=void 0),i.push(t);this.seclist.data=i}catch(t){i(__e(t))}})}async saveBlog(){try{let t=void 0;const e=this.bloglist.selectedItem;e&&(t=e.data);const i=this.inputtags.value,a=this.editor.value(),s=new RegExp("^#+(.*)\n","g").exec(a);if(!s||2!==s.length)return this.toast(__("Please insert a title in the text: beginning with heading"));if(""===i)return this.toast(__("Please enter tags"));const n=new Date,r={content:a,title:s[1].trim(),tags:i,ctime:t?t.ctime:n.timestamp(),ctimestr:t?t.ctimestr:n.toString(),utime:n.timestamp(),utimestr:n.toString(),rendered:this.process(this.editor.options.previewRender(a)),publish:this.find("blog-publish").swon?1:0};let o=this.blogdb;t&&(r.id=t.id,o=t.$vfs),o.cache=r;const l=await o.write(void 0);if(l.error)throw new Error(l.error);t?e.data=r:(this.last_ctime=0,this.bloglist.data=[],await this.loadBlogs())}catch(t){this.error(__("Cannot save blog: {0}",t.toString()),t)}}process(t){let e;const i=/\[\[youtube:([^\]]*)\]\]/g,a=[];for(;null!==(e=i.exec(t));)a.push(e);if(!(a.length>0))return t;let s="",n=0;for(let e of a)s+=t.substring(n,e.index),s+=``,n=e.index+e[0].length;return s+=t.substring(n,t.length),s}clearEditor(){return this.editor.value(""),this.inputtags.value="",this.find("blog-publish").swon=!1}loadBlogs(){return new Promise(async(t,e)=>{try{const t={order:["ctime$desc"],fields:["id","title","ctimestr","ctime","utime","utimestr"],limit:10};this.last_ctime&&(t.where={ctime$lt:this.last_ctime});const e=await this.blogdb.read(t);if(0==e.length)return void this.toast(__("No more record to load"));this.last_ctime=e[e.length-1].ctime;for(let t of e)t.tag="afx-blogger-post-item",this.bloglist.push(t);return this.clearEditor(),this.bloglist.selected=-1}catch(t){e(__e(t))}})}resizeContent(){const t=this.find("editor-container"),e=$(".EasyMDEContainer",t).children(),i=$(this.scheme).find(".afx-window-top")[0],a=e[0],s=e[3],n=$(this.scheme).height()-$(i).height()-$(a).height()-$(s).height()-90;return $(e[1]).css("height",n+"px")}}t.Blogger=e,e.singleton=!0,e.dependencies=["pkg://SimpleMDE/main.js","pkg://SimpleMDE/main.css","pkg://Katex/main.js","pkg://Katex/main.css","pkg://SQLiteDB/libsqlite.js"]}(e=t.application||(t.application={}))}(OS||(OS={})),function(t){let e;!function(e){let i;!function(e){class i extends t.GUI.BasicDialog{constructor(){super("BloggerCategoryDialog",i.scheme)}main(){if(super.main(),this.tree=this.find("tree"),this.txtinput=this.find("txtinput"),this.find("bt-ok").onbtclick=t=>{const e=this.tree.selectedItem;if(!e)return this.notify(__("Please select a parent category"));const i=e.data,a=this.txtinput.value;return""!==a||this.data.selonly?this.data.cat&&this.data.cat.id===i.id?this.notify(__("Parent can not be the category itself")):(this.handle&&this.handle({p:i,value:a}),this.quit()):this.notify(__("Please enter category name"))},this.find("bt-cancel").onbtclick=t=>this.quit(),this.data&&this.data.tree){if(this.data&&this.data.cat){let t;this.txtinput.value=this.data.cat.name,t="0"===this.data.cat.pid?this.data.tree:this.findDataByID(this.data.cat.pid,this.data.tree.nodes),t&&(t.selected=!0)}return this.tree.data=this.data.tree,this.tree.expandAll()}}findDataByID(t,e){for(let i of e){if(i.id===t)return i;i.nodes&&this.findDataByID(t,i.nodes)}}}e.BloggerCategoryDialog=i,i.scheme='\n \n \n \n \n \n \n
\n \n \n
\n
\n
\n
';class a extends t.GUI.BasicDialog{constructor(){super("BloggerCVSectionDiaglog")}main(){super.main(),this.editor=new EasyMDE({autoDownloadFontAwesome:!1,element:this.find("contentarea"),status:!1,toolbar:!1}),$(this.select('[class = "CodeMirror-scroll"]')[0]).css("min-height","50px"),$(this.select('[class="CodeMirror cm-s-paper CodeMirror-wrap"]')[0]).css("min-height","50px");const t=this.select("[input-class='user-input']");if(this.data&&this.data.section)for(let e of t)$(e).val(this.data.section[e.name]);return this.data&&this.data.section&&this.editor.value(this.data.section.content),this.find("section-publish").swon=!!(this.data&&this.data.section&&Number(this.data.section.publish)),this.find("bt-cv-sec-save").onbtclick=e=>{const i={};for(let e of t)i[e.name]=$(e).val();if(i.content=this.editor.value(),""===i.title&&""===i.content)return this.notify(__("Title or content must not be blank"));this.data&&this.data.section&&(i.id=this.data.section.id);const a=this.find("section-publish").swon;return i.publish=!0===a?1:0,this.handle&&this.handle(i),this.quit()},this.on("resize",()=>this.resizeContent()),this.resizeContent()}resizeContent(){const t=this.find("editor-container"),e=$(".EasyMDEContainer",t).children(),i=$(t).height()-30;return $(e[0]).css("height",i+"px")}}e.BloggerCVSectionDiaglog=a,a.scheme='\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n
\n \n \n
\n
\n
';class s extends t.GUI.BasicDialog{constructor(){super("BloggerSendmailDiaglog")}main(){super.main(),this.maillinglist=this.find("email-list");const t=new RegExp("^#+(.*)\n","g").exec(this.data.content);this.find("mail-title").value=t[1];const e=this.data.content.substring(0,500)+"...";this.find("contentarea").value=s.template.format(this.data.id,e);const i=this.data.mails.map(t=>({text:t.name,email:t.email,switch:!0,checked:!0}));return console.log(i),this.maillinglist.items=i,this.find("bt-sendmail").onbtclick=t=>{const e=this.maillinglist.items,i=[];for(let t of e)!0===t.checked&&i.push(t);if(0===i.length)return this.notify(__("No email selected"));const a={path:this.meta().path+"/api/sendmail.lua",parameters:{to:i,title:this.find("mail-title").value,content:this.find("contentarea").value,user:this.find("mail-user").value,password:this.find("mail-password").value}};return this._api.apigateway(a,!1).then(t=>{if(t.error){const e=t.result.join(",");return this.notify(__("Unable to send mail to: {0}",e))}return this.quit()}).catch(t=>this.error(__("Error sending mail: {0}",t.toString()),t))}}}e.BloggerSendmailDiaglog=s,s.scheme='\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n
\n
\n
',s.template="Hello,\n\nDany LE has just published a new post on his blog: https://blog.iohub.dev/post/id/{0}\n\n==========\n{1}\n==========\n\n\nRead the full article via:\nhttps://blog.iohub.dev/post/id/{0}\n\nYou receive this email because you have been subscribed to his blog.\n\nHave a nice day,\n\nSent from Blogger, an AntOS application"}(i=e.blogger||(e.blogger={}))}(e=t.application||(t.application={}))}(OS||(OS={})),function(t){let e;!function(e){let i;!function(e){class i extends t.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){if(!this.data)return;const t=this.data,e=["content","start","end"];return this.closable=t.closable,(()=>{const i=[];for(let a in this.refs){const s=this.refs[a];t[a]&&""!==t[a]?e.includes(a)?i.push($(s).text(t[a])):i.push(s.text=t[a]):i.push(void 0)}return i})()}reload(){}init(){}itemlayout(){return{el:"div",children:[{el:"afx-label",ref:"title",class:"afx-cv-sec-title"},{el:"afx-label",ref:"subtitle",class:"afx-cv-sec-subtitle"},{el:"p",ref:"content",class:"afx-cv-sec-content"},{el:"p",class:"afx-cv-sec-period",children:[{el:"i",ref:"start"},{el:"i",ref:"end",class:"period-end"}]},{el:"afx-label",ref:"location",class:"afx-cv-sec-loc"}]}}}t.GUI.tag.define("afx-blogger-cvsection-item",i);class a extends t.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){if(!this.data)return;const t=this.data;t.closable=!0,this.closable=t.closable,this.refs.title.text=t.title,this.refs.ctimestr.text=__("Created: {0}",t.ctimestr),this.refs.utimestr.text=__("Updated: {0}",t.utimestr)}reload(){}init(){}itemlayout(){return{el:"div",children:[{el:"afx-label",ref:"title",class:"afx-blogpost-title"},{el:"afx-label",ref:"ctimestr",class:"blog-dates"},{el:"afx-label",ref:"utimestr",class:"blog-dates"}]}}}t.GUI.tag.define("afx-blogger-post-item",a)}(i=e.blogger||(e.blogger={}))}(e=t.application||(t.application={}))}(OS||(OS={})); \ No newline at end of file diff --git a/Blogger/build/debug/package.json b/Blogger/build/debug/package.json index 8baf217..5e1ed75 100644 --- a/Blogger/build/debug/package.json +++ b/Blogger/build/debug/package.json @@ -6,7 +6,7 @@ "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version": "0.2.10-a", + "version": "0.2.11-a", "category": "Internet", "iconclass": "fa fa-book", "dependencies": [ diff --git a/Blogger/build/release/Blogger.zip b/Blogger/build/release/Blogger.zip index eb345506d399a23743ddc703537f90df090f5342..8f81d6c5d038dd386272a71c928920723f696b02 100644 GIT binary patch delta 13437 zcmaKTWmFwqvi8B<-3bHvr6~H+6f+_7eB4 z(p90N$;yfT-Wz5Aq@$Q7OGFCWViJ{7Y$S53tj9>cu#lQ?SlG=SGO;#;HY#mNHkl4? zCo)8yCvu6S`S!pPaF{Agd~Z7l7}8iUHf)`fg9OFr&DvIgGHW(sZy>~wRBI#IF)C@*)!A3>r7T-t5mQMoNgz3 zqLwNa2c3UoLnpU3n*-;x?(!kq>}LN2e0@n9nM)4fgN0}Y7L!61*RprCYLde<@D(+C zP(M126#Cx_!<{@|E<(hS1K7v0-&L>B6KYKf(Kk@0IX);>4EucW%^``#zHueARQM#p zc%d5bE{1#D@C>!hKObriI#1Ilc9a;fwv}P(N=zYJy^^xNieIz^N`K)i^$nJ3p!dC! zKj+J)jEkrOYALd~=_pVKQ}qn2$wu{{69meX+x|E74O6&wa8=)cxrXL}C%6vCP)kTH zX!??l3p|GT*%Sq7^ImlQ15joN)6vvK>B|RwIuYEan7K{R=;_Gko6xg)+FRfPl<1|^VN_;AW}-i^gVpnE=NKkby`yA+&+Ca4t(FE&<(fw2#$jTX$j{Tk z>F7+}bh|-Ru6kAQhIc7eC(Q6!gE!0@Msxw_MS-=MF{q(Km}bfl3?5=pw!tF5{c`#w z^&a#!Yhe?IUek>I(ecmV*!e1B8xJ%VvZCUSh)ln!=ZXkx3y=okADM|BU_%({>-)!* z1ejCYTsP=+?%V7S%K}pvml`FMLuiYt_C3wKi zEDrgFYqq{6qVSi(Pf1slG-D%Z{qcfT>w#*u-0MK{=*1ZEwpbPAWtV=U+%6r@^s>~! z(y&1d!(S5LhkCl_XdJd%x{PfPw@x5gy{UMZj(c<}t9KhXQpU-5UgO#%?puvXUxIZ* zfhN&?h|VQun57ns6}ah+*!*}WLhjWC#n#%fXkJO&WzVnDtjEvGC8kck0=n+f5zo5Y zM{8UXrz``Ro*jh;=MZOI^(HBw(HRouy_DW%d4(f?N#ss_Sp}_|Lfh!DFOzM2+q&9S zSq=cO54w@Ntsj219=@Yyr4Jx&s^x!J1JF!uy1fm*_-`@~u%(@%r)eU7K!(EKmEPYV zT_j^wg?lS+)tIR#ISW2-(&&3qU9A;wQR2>e#k0J>EY+Lh_o&I_K?}u1{=78U6(}OS zA#JkdFC$OD@y@(rb%Z1HV~w%)+0oF2BM9-~Zm*NNO`)yJHrMpMI@aY70Q(b@;QtOv z^$*bN5Wrv2>G$^L$O#w-qzwTA!TpI@ief^dGGYwYMk?{+Hmgji0hjJ!V4k%2gU&Mz z^Yl%I-D0yJ?N)ijsy>pje2Sr6b$@0sR+o(q&wNd_9fnJatfqn2XtGN1rZSa(^6`om zMwgFBYl-}QZXH&mp55G9PX@Az*bnhFSHF7 zvqWsDHEq&5TohBS(l}S!t>bhv@2Vyrieg~3%%~M9KJe&jyg!d4g3z~ut(uS9*4Nuo z9?6)pyYLX5hQD5}UproUW=A6WT+KQdo!_Qd$R4X5tuPE@xi zNV5AAJsCJ6vjUWF!5PrS-??v_x9`xS=rM>5P20cOqt(MFzeVkv5BS@i7)ED*G>K@} zy?%Waq(l!&=1msDm7UiWyBYP5EKq5JL1E{eSdOy=%&R57NSY$J%Yt`3(PPBPAtS$h zA?w`Y8WhnP?7}8Pd4Ui>Ttz5pW=>}@X~C7v`ic6T_zb9Z)wxCwIYvyW|LsRbkW@rb zT^9QOb_8rbPospB9UkOrVisnEYEZ(@I(AO5@J6~FNKvxf)-aq3sD%rsNosW{EVoUU zqazbN{t(N4pf%J@62lu2)6hn{jJj&E*(_rnXB6$)QM zn5B(n=L1|;GToQGeyM#|Ot#RE|Ei8uNkLoKxLF}@p^U-Z{6N@~By?`X=f>xNeM$_x zc|=(~0M!nYf@UysV+Qdw!F)O^GG{4Ms6JD|i9X8y$)~ihC z+c8;nk-ytW99}lnn*8Rm2~QG=kC}+;*1@PeA%C|W(QTm1yP>|7lF#f36;FgI@*BHM zTPYE}^u$c)Io1qwrveX!KIGav%|z-{_x_kGH!v@djJqPV;*Rd9Cy{l5vekHQR^oNA$?gYM&*Kg) zueK?YPgw+(C4As9NOXm&eh1o*oBW=N4kM&e=7m6)GrCc>t{4DcJzvI#WJI}(QUJw` zFI*v^q)%-glF3bbM%E0YoP#khWyCq$M*)zVzBh~_jbaod1Zy(^wise#4mQ+)=>kVHC&VyhR>BQrb5EeH+DNm`}rz8Iv}JH-6RsuwCJ zW}~n(af!bSMD93-0Yll|VnWJUrV>!Z4Tsgvr*)Habdtur>x;zwt;lAY@gWLNvKz;<@>*nrQzxWj)hNoXI z4mqcc?BND?hWvRkeH-x+e;F~39J52ApTXcu&5AU-!D(a7E<^jpWa1}Qp@Bdb%~(oZ~?M;t~NS{Cs8;`Q^~ z(4V)m6HzXDi@lc~nHq%kkj61qTgbT1j zXwF?OKsq%mAmb*1Mn%Gc*%A&uIvTq$S@Z7ty1w>k7_p&XBeJ?X1m2Bd{Sq8OKBaK# zK9*;1TjEvTiI)NLXVAp|g-vNjeJq5ktm|=r-N>Z&a!l~~8yc-D~65*NpeCgt4bm@Ylh8ATzt8PUFQj78C%)O37 zm26MbY7An}D0`JM^%Y=_Uqc7^)}4|qxn|PVT{JBP_5VT(edgR_{Dgke!AEu-IM$a= zhIP1DRS1$jHFcb@CTH#{PAutHM)*EohbjKGyvlJ8VkUO8mv`*)8_C617m`vqVQ-_G zsB}@%Uf2>2~gq!i;6n;z>a(lh@ z5hvS|s0Y4g?Hzz^aYgw+a*rj(Zn(cE3PrVo)4S8R+?=qlxKqw~gW#fE$tDgvut1F`6K>c0$B_l5M1%n|(f%}D)c?1}63RiqV@ZYdfuBsI)e!2Uh zdlOOOpzJ7n4aExkmCJ6sryLK%6LMio{O7lVamAde0UYg4x&x-Fz4D0hmKDeiv)4Ko zdu(2L=p%BiQ1#-iGALNRux3tlN|jS-n_j)2ObclS8*_iMUN@Bl8cU&}DE|(rSX(og zDQS6z{8#|v{qP(8Vm-gGUV0XG zjMcZ<&JKrc^2#}Va0UjJDm}0!QPYP_5Q*O5m#^CpQrq(_;;P6nmF;cYhU!0i=^Qtc z)U5hP(d0>OA3Q9}0&c3GX6VpE%TI|)NPl|!KQ;kZs!**;d_7Tw0XJ;F1fgBO@*1Hq z*}>*8Dq5B1%pCD=oU=ZJmb#YBC55CZ< z{#2k~EFY%KWMe)3*flg6KG5LnuPLK1+pm|enRCSRmTiUcvI%we7^VY5NO-dD+1c-! zE3<5eSp8Uu>_MOghToqVFsWQ6bUDx*&y0O|0h0^l zr}4P>PiGoolOI3Xm(s2H)b-}HF|-qNqMl+Eu>ZV3E7Ve&VV6nc;eu*((=vDd_LYEV zQc&Wpr`osY5_s`;Jo_S~PMbU-&8;5=j9{ z9j$x$oyO}DsDeXt>(Rp^sC|6wP-iMAvO=57(uw*MNNOVuFn}mxQL34liT8uc%mpq{nP0(YNurH(-)(Np@g>({Sz zwGc-@U`x5kI22t+>TTRYWN40I6&y0h;p2TxDM87tP1T;whDO)T0c8zI{EAL<&ngVL z4$i|WCrp(Y?gAU)<2}iW>Q6MS7jf)D<&lbB-Wo8yI6KY>ZRj(C8Cgplj`GxHTIixn$SlLXfaA zEWbh>ag7CXkLjETqa^f~6aqQQuHf4a#*Jjc4U%g!L@lSPFPwytFh5=a`bN!NgW_0N zjsZA@5QMynpX*pP{AeHfeN@pGSWI*De!PB!aVlw)&%v&fKjN47MBAOla4JW=KhqEw z7D|(-hCKrRW{zfa{!HBBzu_8$-61zok`qVaJWQ=dE9IW5%m`69XDddFaA+c0`sqz% z#54J9(LXQ4im86Q*=5`Z*iP|2a8_eS_|ja(?J9O`@~Kh5MpSf(Y-S{JBmSmiu8x#B zPSS>){5%2A8+7-@ZEy&Ob8)=7m%3$Ohdhpgm2wX(ejk`qG**zHaMFoj=Y)zZ!4hmWyc=CG+iz^bi3D2J_ z?|wf7c;R=t0*YHme&ckLy`@fGzrkf%J2~9W>&7X2Nt8JBd;obRx%LRb5)ps;M%oJ} z`yjl>CGO`liJc`02nXHa+i!mRwX>JUr}Bci;DX?aEa=Tk6CWvQ$fR?kkx7`MTp5l!;(WJEL@?SzSIqE`F3#;ktf!4o0@bjAV{autGw2vSw=^TOHd zz<3hp&304Ky9zQOUBqIN3~KH6)~}!$*kRc$cG4~pBfb)5WMwS4jc)B_7n_=r9-0=L z!r-9UgpQ42zLE@a=5Xa2kuWijM?2T7|962_cG7wINhpKY7sAPp3?Z||w zXZsl<6^Zo1S|hTzl0xf?`tC|jLXqDKHnsW{^h4o5enDUdmHVl- zQ`}X-#b-R^5FxfuKkt*;yI^V!d<1!4gnI{Wv^x}ltwyd<@q#Mios{$9|Gq~RJt;`5 z$(oo2y?q3hqxT%jC6{=Ar)O9`f~}0*1f4VnsJ-f|6aOxs)AlmH_|3La)B8D(iGYh~aPaiIDmFsEnS^+=V}{w3A_hB%}5W?{$-b zIjHXbVp*_ih!?xv`+n#E11#eB1+TC9o5NDU0Q!lp2xB#|-#GkMdaIZL zrT^oqrhb$iiy%@U?rF3B<4LfM&SyJtb~f8Y%d`eRO@T5wa}r!UQQ?_Ysm;XL`;3Pe zuJ6Z1J13$lIITLgB4baPNR)j6hs}$)n~lYy7noSYyVCwQ+_CuE@9CZO=XsYtB&AMuwCbRb* zuUJ?2M@bqh=uw2$^5bzb^~&=V-^GnxaC6J?!0qlBDmI9dp3)z8?O%08q*2Cxx{V^z0aHTa)UE8{#W%AQ-QW#DYy7Kf4(lXW(C(8<46LjOu z(e2E^rA-dW`U$Bq9eaY^lsUa;Rw`Y8BCw@uuXl3j3$Fk!X9i(J*&n9)-ytME9^5_gA_2hXDqWHz35CNViN9N{s-p&OfPyEV@&=l1UyD zmzkaj?E5K|>I&4AKS1(0wF~|_SfjhD^O+$Nx?IiU4TvI|6#1sflhw3q!)0qfvDc{( zjGozXMfV&?j*kEYjn0s?tu2snrZ`0zzG6T?m#7cEdYQ6zXuj`m(vu1<2yM}Q9MrvP2JcZSqpkgwR zUP@ILq(1!ElEJItm4!^?FX%+1>^RoQhprctBSSK!?6?dJ%mz8*a;AKm_@ounyy4^2 zK;=bvgE{u=S-rx-mDhvOdeWQ~JG2-s&*Y)9)Yn^wEGS&l`0gCG`zJ$|VSkP>Cw$Ft zEx{QuxHsljBG&JIPQvwdn`wR?Z_!e4x_emRR^nCJ86eY5>r1q;Q0;QQW$Uto}b5c8Wy*Qe%AHB3(9hWw~-ev;#!v}=xCq^350i7P}pU`c3ev`(N zf3C?)yqjz~%7z4CwbG0O7U_%$`&YJSJBJv~RCLGlB}Db9rcHbz;&iR4E_H38ZOt&o=AR^6ogT~S&R z%lU$UpM)SKd-)IlES%;XQxMFp9Qg|c&fowLD8*4#S*tT}vpBngP}W`K$QP7a#Ix!E zyMwYH!HrDl;vzT356j7I&H*!|)n0dJ0b)28C#jetOs+s%m|81 z=8lu4nTxbfr1cJsyv3A3)mYdbl^D}YvG4pOt6Yt5>9AW3($7@Tk;Z1LA-nF{)ld~J z{%!six4*dY>&u3p%0_hx`3z}7F>$^y#FH{e0+ghsR+e&t=ouzft3J7tphvvb4G{|v z`+yAw#y<%Z|30ugW#0mTJ-Wg+7P_%3QZsZ1rt)Mz8QI&mjvdpdA%ltj!nX<`w^zJu zR#deo=x;($>=}o;uRI3I^!l-baFeeZ>x)*f}qEz~IEk5=N1 z^4!8_`VcE7)4uq4%@T5=U*ecDS&bv)j&6oZ$GA`n9C*mkMr94M^C7Cxc}Lmsy+s(# z?1@+JOc<>&oM>;JK~TK6R+%?6`rE9LIHlNNPW0W(2AtF9iDk0|;E0$fP^iY=|_6ROxSWtCcJy!t#mt zyLOT%JF=ue+)uJx-Fn1l6=49@cK7^Db_MjT>gz>J-y(Pg`fp9xdH;FyueE94zTIp2 zm`6s8hfJk4UW`5Ebh?$8rlJ9z>g4_Ki?S>?H(PIZ)QaB#~D!UOd- ztmNKN{?%#${bM@dPgmvN3-ZsMft@+yUkKvA2M82AnD-|BwCE83JV*U6NWBNe8W`}; zp3VOj^jELuFF^+Xl=ZhLUE|Fuyg!{j(LV_Ezd;5z23Bqk#tc?Y22Lu&f3^KO2(AQB z>EPu9d)XI3;_07-!tw}K4lp61DI%@NJ|D*Tg^N|ae)E0iD%e+K z_+X*M`8{iB>uYvTzh=J`#bfX<;ZC>oDC z=&`~n1!w?cQ3`_X8{kaDS@AI=RlI2)Fa2D&6M7VJ#Obdu@Z3MVou|HjcBNQb1#k-B|C3OW9~dkHg#9`E+8>goEw;%$tdE zB=3K7_tUyNY(N}mtW`q%J5XWXr^Av zH<+d+orR4%hVJl;=@cyxI$lZgtYu<4#dJcvCYA#eMe++9D?>N4n$gc1U0s#%-x(dx z0>4bEg=MMcovV#PZ0a34^Jb1UimeLF!k0N-;1O*apo-{cWoK9#oatO3J`G)EO^{6S zJ^|=y?gDvg+uZIQ8)lz}T94ohgnm?L=UdUUB^0xeF$dlC+H4iK-PV+~$>3#^%ROoF zPb?+bv?#2=2yG1FR6rD1grco1NUT>0ZwT-YQP!$+v=ua;glsl*X2uI1ndZoKSal2t z9|j)L5!jlVviYBrdV}`OLDXGEMclPMR=|GWq26YfWDOpB=n`eVmFUx$VvNi2nKs&N8L!fsLUuAk4+zUpbXLdkb+cJrorJOg*=&ta;Yx{r+Yw%5@ny* zl%;jT;x&|Qc;hBduF!OLo?DbRdHBT=v$5u3zA}f}i&|5yDzy97qK{w6S_8PMA%FtL z`XHNyawB(bmsH@z2^lGJzs+l;o~pbrqe0)Oqm-GH(v%oD_Q|cmszld-6!I1lF?|(eb1}=1$>2< z_?_o;?$R)dlUA`wYD%oFQ&b)M6IsqLyfdP*R z95#R+OC)1}UU4k_xZWMB6k*3o#DS72KkL^V8`gH5!z&R7`mxTZ_O$_n1Oh@Tg5IAL z^vMeK>sITJhIhkeY*pg%0d*fF*FTlfnroWmlpuA&%W|ulgdcuPE-KLZRODgH-fGqiGYa5T36 zE3!3eNJg)6Vs|c9lWLnaWGHg&cglR2=zvK{?Iq+3^hq*HSE)NnqUJg#y7R7N`Y=zJGv-+PX36OVjA2R67L2D^Dw>D=8;ut(d>k1ePe zYV-vf68G}xEojW)())gswYKVQpt%y03}?9rmL{J`BTJ)y{kYB+5yI!|B(sj;RuHG0 zmzdD(9G~V+nO7DR)rfUEACP4E1w~Vua3wrS8`c)C4&cuysfg;Y?$Rdq*74J{7;eCT z@~=!j1jMz~)#%ngZ^o+*Nj}nhclsDrI9XmXL%O#JbHaP-KqL5|hHx|!k#KaB^-YoW zT@OKMz_h8)*1NI52Cg)_(S)v@%IWq-&(B6@ZxLTxoVGnSze3kH=-Y$p@bPKzPGH)n z;vNwsc*5Hg$yfHIdmf;qA)5s8Hgr{M97MWxfk72=K&QA8y64 zFDwx{{$Qp??%htLvB>((JeF^OnDf0kPCJTlXlcG3<|~!BM+lPWJ8Ziw1l<;W4Wxun z0pQbMzv@6Gu3O(Z2R7*Oaa6yxm(8XHy)oF8H`52+Y)WBExLo$GyU9Gm(_5m63Dz~h zIaFc#%J7aGLYmeSrl-IlCmxySRM(^@;6pN>ki%G*nHzbbJ;!Bo7Wahl%WFG>!Z^Xt zRTvg9x+OBDOgPN>h1+WFG4peEC(M5EW`LhjmG(uq5<4JkGQv5>3c;T~iW3go0DFZC zrC*OkJ0>@kziD(CR?Yi@#~lT+r}_mXf+}gG5vhq|V(b(C0;x+Z%T8~~V%oeP4jZn%^##>hsNUb1o94z z5LHI#UbwmAh%1^=#h{_9E|s8|W&$vLqNFfeWjTa1qzcPK@&<@!jL^GWjP`em?h}&6HmYt z9o1qYV&-uecM;vhs{aiWhWTa4(nljJSgMUJvFio*jG3!2&(^dT2w|B)Ol1^AB0AC~ zj#cMf-RN60tK`8!@pCCV9_{lsaV z-TRT%&V=!?-M+w-*}i|FLLgM%fP!o<&D4{!-1)3f3rcviG!(Q}wG~2~6EBYzRd{Cl z9v=3MsJR~5VZtwuvnp2&3y|rZD`(Vg=vBrb9TZG{{vIjht|#r$e41=5iQUzlc*Kos z-&lJH)b-C1-ykhnt{kk6o3`vT8Z>jc$$b1=G4^dsNh$+d^Ymdf;t>4)y4hHE{kmBT zU$A=IIIgG6a<`#07C^rhi~_pbM6y?5;%4aK<%d4%Np z+3sFxeEFD9Dd)sp0>E;E;k23CE{LV0f>Z9{VL*?gQ;jg7`!?oG`qH$m#?Z{Z-b(IO zR){z=yd@sNyZcj%!Cs;#*~wOz$GodON}tp$kGsUo1;`&dBQ%lIcG~wxS>2COnbJgV zmTXm}$=g;pFZJiD3Ux2xTxK`gK=kT~veb-;h90H>-4AlDX8|ukwD2tOQ4Lp z*YfRDsn(D1NB~hsJzTkK{c&Lc=6!=^u!Q2fHHm>}3YqxkY_IM87>fVC-H9riVecz^_LZL?Dp3TKmH} ztmut}M}rMw=uD65NSO(UaMYTUgoRiZXqak+PUQL#3Sf#l@oJ`)5KJG5&1-~mxap|p zRDw&+=6(2tI;`l_`79n%CFq%)wY|$~AZkZb&v_NciO3HY6G{lw$=-d9U<#sVafK2N zQuT9A?3P-Fc!Ti8>VHJ~K~fnt#^^32nQcpfK&65QkM}ub=$m1xTcUM`yZark7p3LH zBZrF09FR1ruRkeU+H3YYe_tJGS6M4UOV!(Eje#fw3VIJWZ%+S(1$6ek`U`VoFe1376hma{I;r_h< zAy-Uo^3?2wAS%K)_u!PmV?WQV6CeBdTA;IM(6rheW>%UG%;x=763726b21|06%`hD zD9B+0j+&S5DDv^Q;8jJ;8Z%Py*}P(q7I0u;z}d4m`23yQfI>|fx#bR(c^}V#1O7Ds zCtzpS&MCS?OVyjdCxN-zfD`OwrCeEoz)K`3Ws)^(IfU^1CjWSGZs2^s7|UB4uxFf(zl^@m-4 z5tCeQXyEl=aoGc;D1AS(g*5dW9PXF6G?fPeG4o+&dyA@Ccw5|y$KF&+dfaLhFU`N_ zlMNHM2jfKZj$Adx!>fbTuqum{3!!BZ*dYMQC`+GDi6D%h!lpe?r9^7T2X;Q+A{K;9 z*0l!jJ0G?@Db^%xjgo!dKuxBj(*4c=1+SqfdINgr5=h`%@(2_AU^S(B-&(aq>%g%JUI5VrO^-0W1J3d;z z>^n`x6fN-J-s17pLNKJ1_TwNBTE(*(yi{isB3b(k7VD!IpwkDcZ}wzB+OMYPWD6TMJpVc@VM}`Y?`1{ikn|Ao<-{~ zHfJjo0k8?gVg-Iq+|9Jy0)uDO%3PJ7N8*cEzD&_Y1!$&SlH1(oywpV)NF@Mss0#(F z0UU!2NqdO93k)d~bdEW#ZA)BuCE(L0n-0+z6nCS$YERy`6H`~Oi4`}H#Et!SOB8NS z@l)z6kH?g&@|_lG^|KrOeme+9#{B%3hM64hM=tsru$AeB9oMP^xe*ZTVgirMakBpD zk#Vc&F8iPADLaadHHYsJ+br-Ll&?tSrNF>3L4Q*$|NPt|hYE}LkIjPY3=Ay|OpX5x z&Dz@hCsJXye6INq8B>S_0wMeb{`bm(jK8MBBmKYTv;U0&`M}2OQRDmr1i6!UM)t>5 z;Qt&0_HV?WyIA;mUn=bXbC~yU+%n?7M|=N1faYy$p4 zh5a{{>u;_9{6^-fI~B+>p`>;I_g|5L($)}Mb%AfgAE{ELnIZ{_HJ3i!`(_qTvG e_J1GYU%^ja3hGZ({6h);xmy0b_;9Yje*Hi0#2cRg delta 8413 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 any; }; }) => { + .then((d) => { if (d.error) { const str = d.result.join(','); return this.notify(__("Unable to send mail to: {0}", str)); } diff --git a/Blogger/main.ts b/Blogger/main.ts index 955e477..daae524 100644 --- a/Blogger/main.ts +++ b/Blogger/main.ts @@ -499,6 +499,36 @@ namespace OS { this.error(__("Error sending mails: {0}", e.toString()), e); } } + }, + "|", + { + name: __("TFIDF analyse"), + className: "fa fa-area-chart", + action: async (e: any) => { + try { + const q = await this.openDialog("PromptDialog",{ + title: __("TFIDF Analyse"), + text: __("Max number of related posts to keep per post?"), + value: "5" + }); + const data = { + path: `${this.meta().path}/api/ai/analyse.lua`, + parameters: { + dbpath: this.dbhandle.info.file.path, + top: parseInt(q) + } + }; + const d = await this._api.apigateway(data, false); + if (d.error) { + throw new Error(d.error); + } + this.toast(d.result); + } + catch(e) + { + this.error(__("Error analysing posts: {0}", e.toString()), e); + } + } } ] }); diff --git a/Blogger/package.json b/Blogger/package.json index 8baf217..5e1ed75 100644 --- a/Blogger/package.json +++ b/Blogger/package.json @@ -6,7 +6,7 @@ "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version": "0.2.10-a", + "version": "0.2.11-a", "category": "Internet", "iconclass": "fa fa-book", "dependencies": [