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 eb34550..8f81d6c 100644 Binary files a/Blogger/build/release/Blogger.zip and b/Blogger/build/release/Blogger.zip differ diff --git a/Blogger/dialogs.ts b/Blogger/dialogs.ts index d661ec3..45cd413 100644 --- a/Blogger/dialogs.ts +++ b/Blogger/dialogs.ts @@ -212,7 +212,7 @@ namespace OS { if (emails.length === 0) { return this.notify(__("No email selected")); } // send the email const data = { - path: `${this.meta().path}/sendmail.lua`, + path: `${this.meta().path}/api/sendmail.lua`, parameters: { to: emails, title: (this.find("mail-title") as HTMLInputElement).value, @@ -222,7 +222,7 @@ namespace OS { } }; return this._api.apigateway(data, false) - .then((d: { error: any; result: { join: (arg0: string) => 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": [