@saltcorn/agents 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent-view.js CHANGED
@@ -31,6 +31,7 @@ const {
31
31
  label,
32
32
  a,
33
33
  br,
34
+ img,
34
35
  } = require("@saltcorn/markup/tags");
35
36
  const { getState } = require("@saltcorn/data/db/state");
36
37
  const {
@@ -45,9 +46,12 @@ const {
45
46
  is_debug_mode,
46
47
  get_initial_interactions,
47
48
  get_skill_instances,
49
+ saveInteractions,
48
50
  } = require("./common");
49
51
  const MarkdownIt = require("markdown-it"),
50
52
  md = new MarkdownIt();
53
+ const { isWeb } = require("@saltcorn/data/utils");
54
+ const path = require("path");
51
55
 
52
56
  const configuration_workflow = (req) =>
53
57
  new Workflow({
@@ -169,6 +173,7 @@ const uploadForm = (viewname, req) =>
169
173
  type: "file",
170
174
  class: "d-none",
171
175
  accept: "image/*",
176
+ multiple: true,
172
177
  onchange: `agent_file_attach(event)`,
173
178
  }),
174
179
  span({ class: "ms-2 filename-label" }),
@@ -270,6 +275,7 @@ const run = async (
270
275
  interactMarkups.push(...run.context.html_interactions);
271
276
  } else
272
277
  for (const interact of run.context.interactions) {
278
+ //legacy
273
279
  switch (interact.role) {
274
280
  case "user":
275
281
  if (interact.content?.[0]?.type === "image_url") {
@@ -484,7 +490,7 @@ const run = async (
484
490
  ? div(
485
491
  div(
486
492
  {
487
- class: "d-flex justify-content-between align-middle mb-2",
493
+ class: "d-flex flex-wrap justify-content-between align-middle mb-2",
488
494
  },
489
495
  div(
490
496
  { class: "d-flex" },
@@ -497,7 +503,7 @@ const run = async (
497
503
  button(
498
504
  {
499
505
  type: "button",
500
- class: "btn btn-secondary btn-sm py-0",
506
+ class: "btn btn-secondary btn-sm pt-0 pb-1",
501
507
  style: "font-size: 0.9em;height:1.5em",
502
508
  onclick: "unset_state_field('run_id')",
503
509
  title: "New session",
@@ -513,7 +519,10 @@ const run = async (
513
519
  },
514
520
  div(
515
521
  { class: "d-flex justify-content-between" },
516
- localeDateTime(run.started_at),
522
+ span(
523
+ { class: "text-truncate", style: "min-width:0" },
524
+ localeDateTime(run.started_at),
525
+ ),
517
526
  i({
518
527
  class: "far fa-trash-alt",
519
528
  onclick: `delprevrun(event, ${run.id})`,
@@ -549,7 +558,15 @@ const run = async (
549
558
  `div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
550
559
  div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
551
560
  div.interaction-segment p {margin-bottom: 0px;}
552
- div.interaction-segment div.card {margin-top: 0.5rem;}
561
+ div.interaction-segment div.card {margin-top: 0.5rem;}
562
+ div.interaction-segment.to-right {
563
+ display: flex;
564
+ flex-direction: row-reverse;
565
+ }
566
+ div.interaction-segment.to-right div.badgewrap {
567
+ display: flex;
568
+ flex-direction: row-reverse;
569
+ }
553
570
  div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
554
571
  div.prevcopilotrun i.fa-trash-alt {display: none;}
555
572
  div.prevcopilotrun:hover i.fa-trash-alt {display: block;}
@@ -584,9 +601,15 @@ const run = async (
584
601
  cursor: pointer;
585
602
  }
586
603
  .copilot-entry span.attach_agent_image_wrap {
587
- position: relative;
604
+ position: relative;
588
605
  top: -1.8rem;
589
- left: 0.2rem;
606
+ left: 0.2rem;
607
+ }
608
+ .copilot-entry.dragover {
609
+ outline: 2px dashed var(--tblr-primary, #0054a6);
610
+ outline-offset: -2px;
611
+ background: var(--tblr-primary-bg-subtle, rgba(0, 84, 166, 0.05));
612
+ border-radius: 0.25rem;
590
613
  }
591
614
  .copilot-entry .explainer {
592
615
  position: relative;
@@ -618,19 +641,25 @@ const run = async (
618
641
  function get_run_id(elem) {
619
642
  return $("input[name=run_id").val()
620
643
  }
621
- function processCopilotResponse(res) {
644
+ function processCopilotResponse(res, not_final) {
622
645
  console.log("processCopilotResponse", res)
623
- const hadFile = $("input#attach_agent_image").val();
624
- let fileBadge = hadFile ? '<span class="badge text-bg-info"><i class="fas fa-image me-1"></i>'+$("input#attach_agent_image")[0].files?.item?.(0)?.name||"File"+'</span>': ""
625
- $("span.filename-label").text("");
646
+ const fileInput = $("input#attach_agent_image")[0];
647
+ let fileBadge = "";
648
+ if (fileInput?.files?.length) {
649
+ fileBadge = Array.from(fileInput.files).map(f =>
650
+ '<span class="badge text-bg-info"><i class="fas fa-image me-1"></i>'+f.name+'</span>'
651
+ ).join(" ");
652
+ }
653
+ $("span.filename-label").text("").removeClass("me-2");
654
+ _agentDT.items.clear();
626
655
  $("input#attach_agent_image").val(null);
627
- $("#sendbuttonicon").attr("class","far fa-paper-plane");
656
+ if(!not_final || (!${JSON.stringify(dyn_updates)})) $("#sendbuttonicon").attr("class","far fa-paper-plane");
628
657
  const $runidin= $("input[name=run_id")
629
658
  if(res.run_id && (!$runidin.val() || $runidin.val()=="undefined"))
630
659
  $runidin.val(res.run_id);
631
660
  const wrapSegment = (html, who) => '<div class="interaction-segment"><span class="badge bg-secondary">'+who+'</span>'+html+'</div>'
632
661
  const user_input = $("textarea[name=userinput]").val()
633
- if(user_input)
662
+ if(user_input && (!${JSON.stringify(dyn_updates)}))
634
663
  $("#copilotinteractions").append(wrapSegment('<p>'+user_input+'</p>'+fileBadge, "You"))
635
664
  $("textarea[name=userinput]").val("")
636
665
  $('form.agent-view div.next_response_scratch').html("")
@@ -639,8 +668,39 @@ const run = async (
639
668
  $("#copilotinteractions").append(res.response)
640
669
  }
641
670
  window.processCopilotResponse = processCopilotResponse;
671
+ window.final_agent_response = () => {
672
+ $("#sendbuttonicon").attr("class","far fa-paper-plane");
673
+ }
674
+ const _agentDT = new DataTransfer();
675
+ function setAgentFiles(files) {
676
+ for (const f of files) _agentDT.items.add(f);
677
+ document.getElementById('attach_agent_image').files = _agentDT.files;
678
+ updateFileLabel();
679
+ }
680
+ function updateFileLabel() {
681
+ const n = _agentDT.files.length;
682
+ const $label = $(".attach_agent_image_wrap span.filename-label");
683
+ if (n === 0) {
684
+ $label.html("").removeClass("me-2");
685
+ } else {
686
+ $label.addClass("me-2");
687
+ const text = n === 1 ? _agentDT.files[0].name : n + " files";
688
+ $label.html(${
689
+ isWeb(req)
690
+ ? `text + ' <span class="badge text-bg-secondary" style="cursor:pointer;font-size:.65em;vertical-align:middle" onclick="clearAgentFiles()" title="Remove files">&times;</span>'`
691
+ : `'<span style="max-width:8em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle">' + text + '</span> <span class="badge text-bg-secondary" style="cursor:pointer;font-size:.65em;vertical-align:middle" onclick="clearAgentFiles()" title="Remove files">&times;</span>'`
692
+ });
693
+ }
694
+ }
695
+ function clearAgentFiles() {
696
+ _agentDT.items.clear();
697
+ $("input#attach_agent_image").val(null);
698
+ updateFileLabel();
699
+ }
700
+ window.clearAgentFiles = clearAgentFiles;
642
701
  function agent_file_attach(e) {
643
- $(".attach_agent_image_wrap span.filename-label").text(e.target.files[0].name)
702
+ _agentDT.items.clear();
703
+ setAgentFiles(e.target.files);
644
704
  }
645
705
  function restore_old_button_elem(btn) {
646
706
  const oldText = $(btn).data("old-text");
@@ -692,6 +752,44 @@ const run = async (
692
752
  }
693
753
  }
694
754
  document.getElementById("inputuserinput").addEventListener("keydown", submitOnEnter);
755
+ if (document.getElementById('attach_agent_image')) {
756
+ let _dragCtr = 0;
757
+ const _copilotEntry = document.querySelector('.copilot-entry');
758
+ _copilotEntry.addEventListener('dragover', function(e) {
759
+ e.preventDefault();
760
+ });
761
+ _copilotEntry.addEventListener('dragenter', function(e) {
762
+ e.preventDefault();
763
+ _dragCtr++;
764
+ this.classList.add('dragover');
765
+ });
766
+ _copilotEntry.addEventListener('dragleave', function(e) {
767
+ _dragCtr--;
768
+ if (_dragCtr === 0) this.classList.remove('dragover');
769
+ });
770
+ _copilotEntry.addEventListener('drop', function(e) {
771
+ e.preventDefault();
772
+ _dragCtr = 0;
773
+ this.classList.remove('dragover');
774
+ const imgs = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
775
+ if (imgs.length) setAgentFiles(imgs);
776
+ });
777
+ document.getElementById('inputuserinput').addEventListener('paste', function(e) {
778
+ const items = e.clipboardData?.items;
779
+ if (!items) return;
780
+ const pastedFiles = [];
781
+ for (const item of items) {
782
+ if (item.type.startsWith('image/')) {
783
+ const file = item.getAsFile();
784
+ if (file) pastedFiles.push(file);
785
+ }
786
+ }
787
+ if (pastedFiles.length) {
788
+ e.preventDefault();
789
+ setAgentFiles(pastedFiles);
790
+ }
791
+ });
792
+ }
695
793
  function spin_send_button() {
696
794
  $("#sendbuttonicon").attr("class","fas fa-spinner fa-spin");
697
795
  };`,
@@ -753,44 +851,66 @@ const interact = async (table_id, viewname, config, body, { req, res }) => {
753
851
  }
754
852
  let fileBadges = "";
755
853
  if (config.image_upload && req.files?.file) {
756
- const file = await File.from_req_files(
757
- req.files.file,
758
- req.user ? req.user.id : null,
759
- 100,
760
- // file_field?.attributes?.folder
761
- );
762
- fileBadges = span(
763
- { class: "badge text-bg-info" },
764
- i({ class: "fas fa-image me-1" }),
765
- file.filename,
766
- );
767
- const baseUrl = getState().getConfig("base_url").replace(/\/$/, "");
768
- let imageurl;
769
- if (
770
- !config.image_base64 &&
771
- baseUrl &&
772
- !baseUrl.includes("http://localhost:")
773
- ) {
774
- imageurl = `${baseUrl}/files/serve/${file.path_to_serve}`;
775
- } else {
776
- const b64 = await file.get_contents("base64");
777
- imageurl = `data:${file.mimetype};base64,${b64}`;
854
+ const rawFiles = Array.isArray(req.files.file)
855
+ ? req.files.file
856
+ : [req.files.file];
857
+ const badges = [];
858
+ for (const rawFile of rawFiles) {
859
+ const file = await File.from_req_files(
860
+ rawFile,
861
+ req.user ? req.user.id : null,
862
+ 100,
863
+ );
864
+ badges.push(
865
+ div(
866
+ { class: "bg-secondary-subtle p-2 m-2 rounded-2" },
867
+ img({
868
+ src: `/files/resize/${50}/${50}/${file.path_to_serve}`,
869
+ class: "d-block",
870
+ onclick: `expand_thumbnail('${file.path_to_serve}', '${path.basename(file.path_to_serve)}')`,
871
+ }),
872
+ file.filename,
873
+ ),
874
+ );
875
+ const baseUrl = getState().getConfig("base_url").replace(/\/$/, "");
876
+ let imageurl;
877
+ if (
878
+ !config.image_base64 &&
879
+ baseUrl &&
880
+ !baseUrl.includes("http://localhost:")
881
+ ) {
882
+ imageurl = `${baseUrl}/files/serve/${file.path_to_serve}`;
883
+ } else {
884
+ const b64 = await file.get_contents("base64");
885
+ imageurl = `data:${file.mimetype};base64,${b64}`;
886
+ }
887
+ await getState().functions.llm_add_message.run("image", imageurl, {
888
+ chat: run.context.interactions || [],
889
+ });
778
890
  }
779
- await getState().functions.llm_add_message.run("image", imageurl, {
780
- chat: run.context.interactions || [],
781
- });
782
- await addToContext(run, {
783
- interactions: run.context.interactions || [],
784
- });
891
+ await saveInteractions(run);
892
+ fileBadges = div({ class: "d-flex" }, badges);
785
893
  }
894
+ const userInteractions = wrapSegment(p(userinput) + fileBadges, "You", true);
895
+
786
896
  await addToContext(run, {
787
897
  interactions: [
788
898
  ...(run.context.interactions || []),
789
899
  { role: "user", content: userinput },
790
900
  ],
791
- html_interactions: [wrapSegment(p(userinput) + fileBadges, "You")],
901
+ html_interactions: [userInteractions],
792
902
  });
793
903
  const dyn_updates = getState().getConfig("enable_dynamic_updates", true);
904
+ if (dyn_updates) {
905
+ getState().emitDynamicUpdate(
906
+ db.getTenantSchema(),
907
+ {
908
+ eval_js: `processCopilotResponse({response: ${JSON.stringify(userInteractions)}, run_id: ${run.id}}, true)`,
909
+ page_load_tag: req?.headers?.["page-load-tag"],
910
+ },
911
+ [req.user.id],
912
+ );
913
+ }
794
914
  const process_promise = process_interaction(
795
915
  run,
796
916
  action.configuration,
@@ -945,4 +1065,5 @@ module.exports = {
945
1065
  table_optional: true,
946
1066
  run,
947
1067
  routes: { interact, delprevrun, debug_info, skillroute, execute_user_action },
1068
+ mobile_render_server_side: true,
948
1069
  };
package/common.js CHANGED
@@ -166,14 +166,20 @@ const addToContext = async (run, newCtx) => {
166
166
  if (changed && run.update) await run.update({ context: run.context });
167
167
  };
168
168
 
169
- const wrapSegment = (html, who) =>
169
+ const saveInteractions = async (run) => {
170
+ await addToContext(run, {
171
+ interactions: run.context.interactions || [],
172
+ });
173
+ };
174
+
175
+ const wrapSegment = (html, who, to_right) =>
170
176
  who === null
171
177
  ? html
172
- : '<div class="interaction-segment"><span class="badge bg-secondary">' +
178
+ : `<div class="interaction-segment ${to_right ? "to-right" : ""}"><div><div class="badgewrap"><span class="badge bg-secondary">` +
173
179
  who +
174
- "</span>" +
180
+ "</span></div>" +
175
181
  html +
176
- "</div>";
182
+ "</div></div>";
177
183
 
178
184
  const wrapCard = (title, ...inners) =>
179
185
  span({ class: "badge bg-info ms-1" }, title) +
@@ -236,12 +242,13 @@ const process_interaction = async (
236
242
  interactions: complArgs.chat,
237
243
  });
238
244
  const responses = [];
239
- const add_response = async (resp) => {
245
+
246
+ const add_response = async (resp, not_final) => {
240
247
  if (dyn_updates)
241
248
  getState().emitDynamicUpdate(
242
249
  db.getTenantSchema(),
243
250
  {
244
- eval_js: `processCopilotResponse({response: ${JSON.stringify(resp)}, run_id: ${run.id}})`,
251
+ eval_js: `processCopilotResponse({response: ${JSON.stringify(resp)}, run_id: ${run.id}}, true)`,
245
252
  page_load_tag: req?.headers?.["page-load-tag"],
246
253
  },
247
254
  [req.user.id],
@@ -479,6 +486,15 @@ const process_interaction = async (
479
486
  ? answer
480
487
  : wrapSegment(md.render(answer), agent_label),
481
488
  );
489
+ if (dyn_updates)
490
+ getState().emitDynamicUpdate(
491
+ db.getTenantSchema(),
492
+ {
493
+ eval_js: `final_agent_response()`,
494
+ page_load_tag: req?.headers?.["page-load-tag"],
495
+ },
496
+ [req.user.id],
497
+ );
482
498
 
483
499
  return {
484
500
  json: {
@@ -497,6 +513,7 @@ module.exports = {
497
513
  get_skill_instances,
498
514
  getCompletionArguments,
499
515
  addToContext,
516
+ saveInteractions,
500
517
  wrapCard,
501
518
  wrapSegment,
502
519
  process_interaction,
package/index.js CHANGED
@@ -18,6 +18,7 @@ module.exports = {
18
18
  dependencies: ["@saltcorn/large-language-model"],
19
19
  viewtemplates: [require("./agent-view")],
20
20
  plugin_name: "agents",
21
+ ready_for_mobile: true,
21
22
  headers: [
22
23
  {
23
24
  script: `/plugins/public/agents@${
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {