@lazyingart/agintiflow 0.2.0 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -122,6 +122,7 @@ const translations = {
122
122
  artifactTunnelTitle: "Canvas & artifacts",
123
123
  artifactTunnelHelp: "Agent-selected canvas items, generated files, screenshots, diffs, and final answers.",
124
124
  artifactCanvasTab: "Canvas",
125
+ artifactPdfTab: "PDF Reader",
125
126
  artifactExplorerTab: "Explorer",
126
127
  artifactNotificationsTab: "Notifications",
127
128
  artifactListTitle: "Artifacts",
@@ -132,6 +133,10 @@ const translations = {
132
133
  artifactLoading: "Loading artifact...",
133
134
  artifactLoadFailed: "Failed to load artifact.",
134
135
  artifactSeenUpdated: "Notifications marked as seen.",
136
+ artifactDownloadLabel: "Download artifact",
137
+ artifactDownloadPreparing: "Preparing download...",
138
+ artifactDownloadReady: "Download started.",
139
+ artifactDownloadFailed: "Download failed.",
135
140
  },
136
141
  ar: {
137
142
  documentTitle: "AgInTiFlow",
@@ -1030,9 +1035,28 @@ function artifactLabel(item) {
1030
1035
  return [item.kind, source, when].filter(Boolean).join(" · ");
1031
1036
  }
1032
1037
 
1038
+ function isPdfArtifact(item) {
1039
+ return item?.kind === "pdf" || item?.mime === "application/pdf" || /\.pdf$/i.test(item?.path || "");
1040
+ }
1041
+
1042
+ function artifactVisibleInCurrentTab(item) {
1043
+ if (currentArtifactTab === "pdf") return isPdfArtifact(item);
1044
+ return item.tab === currentArtifactTab;
1045
+ }
1046
+
1047
+ function preferredArtifactForCurrentTab(fallbackId = "") {
1048
+ const current = artifactItems.find((item) => item.id === selectedArtifactId && artifactVisibleInCurrentTab(item));
1049
+ if (current) return current;
1050
+
1051
+ const fallback = artifactItems.find((item) => item.id === fallbackId && artifactVisibleInCurrentTab(item));
1052
+ if (fallback) return fallback;
1053
+
1054
+ return artifactItems.find(artifactVisibleInCurrentTab) || null;
1055
+ }
1056
+
1033
1057
  function renderArtifactList() {
1034
1058
  if (!artifactListEl) return;
1035
- const visible = artifactItems.filter((item) => item.tab === currentArtifactTab);
1059
+ const visible = artifactItems.filter(artifactVisibleInCurrentTab);
1036
1060
  const readIds = getArtifactReadIds();
1037
1061
 
1038
1062
  document.querySelectorAll(".artifact-tab").forEach((tab) => {
@@ -1054,14 +1078,23 @@ function renderArtifactList() {
1054
1078
  (item) => {
1055
1079
  const read = readIds.has(item.id);
1056
1080
  return `
1057
- <button class="artifact-row" type="button" data-artifact-id="${escapeHtml(item.id)}" data-selected="${item.id === selectedArtifactId}" data-read="${read}">
1058
- <span class="artifact-row-top">
1059
- <strong>${escapeHtml(item.title || item.path || item.kind)}</strong>
1060
- <span>${read ? escapeHtml(item.kind) : `New · ${escapeHtml(item.kind)}`}</span>
1061
- </span>
1062
- <span class="artifact-row-meta">${escapeHtml(artifactLabel(item))}</span>
1063
- <span class="artifact-row-preview">${escapeHtml(item.preview || item.path || "")}</span>
1064
- </button>
1081
+ <div class="artifact-row" data-selected="${item.id === selectedArtifactId}" data-read="${read}">
1082
+ <button class="artifact-row-main" type="button" data-artifact-id="${escapeHtml(item.id)}">
1083
+ <span class="artifact-row-top">
1084
+ <strong>${escapeHtml(item.title || item.path || item.kind)}</strong>
1085
+ <span>${read ? escapeHtml(item.kind) : `New · ${escapeHtml(item.kind)}`}</span>
1086
+ </span>
1087
+ <span class="artifact-row-meta">${escapeHtml(artifactLabel(item))}</span>
1088
+ <span class="artifact-row-preview">${escapeHtml(item.preview || item.path || "")}</span>
1089
+ </button>
1090
+ <button
1091
+ class="artifact-menu-button"
1092
+ type="button"
1093
+ data-artifact-download-id="${escapeHtml(item.id)}"
1094
+ aria-label="${escapeHtml(t("artifactDownloadLabel"))}"
1095
+ title="${escapeHtml(t("artifactDownloadLabel"))}"
1096
+ >...</button>
1097
+ </div>
1065
1098
  `;
1066
1099
  }
1067
1100
  )
@@ -1078,7 +1111,8 @@ function resetArtifactViewer() {
1078
1111
  function renderArtifactShell() {
1079
1112
  renderArtifactBadge();
1080
1113
  renderArtifactList();
1081
- if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
1114
+ const selected = artifactItems.find((item) => item.id === selectedArtifactId);
1115
+ if (!selected || !artifactVisibleInCurrentTab(selected)) {
1082
1116
  resetArtifactViewer();
1083
1117
  }
1084
1118
  }
@@ -1111,6 +1145,79 @@ function renderArtifactContent(content) {
1111
1145
  `;
1112
1146
  }
1113
1147
 
1148
+ function artifactDownloadName(item, content) {
1149
+ const sourceName = content.path || item?.path || content.title || item?.title || `artifact-${content.id || "download"}`;
1150
+ const fallbackExt =
1151
+ content.mime === "application/pdf" || content.kind === "pdf"
1152
+ ? ".pdf"
1153
+ : content.mime?.startsWith("image/png")
1154
+ ? ".png"
1155
+ : content.mime?.startsWith("image/svg")
1156
+ ? ".svg"
1157
+ : content.kind === "json"
1158
+ ? ".json"
1159
+ : content.kind === "diff"
1160
+ ? ".diff"
1161
+ : content.kind === "markdown"
1162
+ ? ".md"
1163
+ : ".txt";
1164
+ const base = sourceName
1165
+ .split("/")
1166
+ .filter(Boolean)
1167
+ .pop()
1168
+ ?.replace(/[^A-Za-z0-9._-]+/g, "-")
1169
+ .replace(/^-+|-+$/g, "");
1170
+ const name = base || `artifact-${content.id || Date.now()}`;
1171
+ return /\.[A-Za-z0-9]{1,8}$/.test(name) ? name : `${name}${fallbackExt}`;
1172
+ }
1173
+
1174
+ function blobFromDataUrl(dataUrl) {
1175
+ const [meta = "", payload = ""] = String(dataUrl || "").split(",", 2);
1176
+ const mime = meta.match(/^data:([^;,]+)/)?.[1] || "application/octet-stream";
1177
+ if (meta.includes(";base64")) {
1178
+ const binary = atob(payload);
1179
+ const bytes = new Uint8Array(binary.length);
1180
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
1181
+ return new Blob([bytes], { type: mime });
1182
+ }
1183
+ return new Blob([decodeURIComponent(payload)], { type: mime });
1184
+ }
1185
+
1186
+ async function downloadArtifact(artifactId) {
1187
+ if (!currentSessionId || !artifactId) return;
1188
+ const item = artifactItems.find((candidate) => candidate.id === artifactId);
1189
+ artifactStatusEl.textContent = t("artifactDownloadPreparing");
1190
+
1191
+ try {
1192
+ const response = await fetch(
1193
+ `/api/sessions/${encodeURIComponent(currentSessionId)}/artifacts/${encodeURIComponent(artifactId)}`
1194
+ );
1195
+ const data = await response.json().catch(() => ({}));
1196
+ if (!response.ok) throw new Error(data.error || t("artifactDownloadFailed"));
1197
+
1198
+ const blob = data.dataUrl
1199
+ ? blobFromDataUrl(data.dataUrl)
1200
+ : new Blob([typeof data.text === "string" ? data.text : JSON.stringify(data, null, 2)], {
1201
+ type: data.mime || "text/plain;charset=utf-8",
1202
+ });
1203
+ const url = URL.createObjectURL(blob);
1204
+ const link = document.createElement("a");
1205
+ link.href = url;
1206
+ link.download = artifactDownloadName(item, data);
1207
+ document.body.append(link);
1208
+ link.click();
1209
+ link.remove();
1210
+ URL.revokeObjectURL(url);
1211
+
1212
+ markArtifactRead(artifactId);
1213
+ renderArtifactBadge();
1214
+ renderArtifactList();
1215
+ artifactStatusEl.textContent = t("artifactDownloadReady");
1216
+ } catch (error) {
1217
+ artifactStatusEl.textContent = error instanceof Error ? error.message : t("artifactDownloadFailed");
1218
+ }
1219
+ }
1220
+
1114
1221
  async function refreshArtifacts({ loadSelected = false } = {}) {
1115
1222
  if (!currentSessionId) {
1116
1223
  artifactItems = [];
@@ -1125,9 +1232,27 @@ async function refreshArtifacts({ loadSelected = false } = {}) {
1125
1232
  const data = await response.json();
1126
1233
  artifactItems = data.items || [];
1127
1234
  artifactUnreadCount = computeUnreadArtifactCount();
1235
+
1236
+ if (loadSelected) {
1237
+ const backendSelected = artifactItems.find((item) => item.id === data.selectedItemId);
1238
+ const pdfCandidate = artifactItems.find((item) => item.id === selectedArtifactId && isPdfArtifact(item))
1239
+ || (backendSelected && isPdfArtifact(backendSelected) ? backendSelected : null)
1240
+ || artifactItems.find(isPdfArtifact);
1241
+ if (pdfCandidate) {
1242
+ currentArtifactTab = "pdf";
1243
+ selectedArtifactId = pdfCandidate.id;
1244
+ }
1245
+ }
1246
+
1128
1247
  if (!selectedArtifactId || !artifactItems.some((item) => item.id === selectedArtifactId)) {
1129
1248
  selectedArtifactId = data.selectedItemId || artifactItems[0]?.id || "";
1130
1249
  }
1250
+
1251
+ const visiblePreferred = preferredArtifactForCurrentTab(data.selectedItemId);
1252
+ if (visiblePreferred) {
1253
+ selectedArtifactId = visiblePreferred.id;
1254
+ }
1255
+
1131
1256
  renderArtifactShell();
1132
1257
  if (loadSelected && selectedArtifactId) await selectArtifact(selectedArtifactId, { persist: false });
1133
1258
  }
@@ -1466,10 +1591,26 @@ artifactTabsEl.addEventListener("click", (event) => {
1466
1591
  const tab = event.target.closest("[data-artifact-tab]");
1467
1592
  if (!tab) return;
1468
1593
  currentArtifactTab = tab.dataset.artifactTab || "canvas";
1594
+ const preferred = preferredArtifactForCurrentTab();
1595
+ if (preferred) selectedArtifactId = preferred.id;
1469
1596
  renderArtifactShell();
1597
+ if (selectedArtifactId) {
1598
+ selectArtifact(selectedArtifactId, { persist: false }).catch((error) => {
1599
+ artifactStatusEl.textContent = String(error);
1600
+ });
1601
+ }
1470
1602
  });
1471
1603
 
1472
1604
  artifactListEl.addEventListener("click", (event) => {
1605
+ const downloadButton = event.target.closest("[data-artifact-download-id]");
1606
+ if (downloadButton) {
1607
+ event.stopPropagation();
1608
+ downloadArtifact(downloadButton.dataset.artifactDownloadId || "").catch((error) => {
1609
+ artifactStatusEl.textContent = String(error);
1610
+ });
1611
+ return;
1612
+ }
1613
+
1473
1614
  const row = event.target.closest("[data-artifact-id]");
1474
1615
  if (!row) return;
1475
1616
  selectArtifact(row.dataset.artifactId || "").catch((error) => {
package/public/index.html CHANGED
@@ -308,6 +308,7 @@
308
308
  </div>
309
309
  <div class="artifact-tabs" role="tablist" aria-label="Artifact tunnel tabs">
310
310
  <button id="artifact-tab-canvas" class="artifact-tab" type="button" data-artifact-tab="canvas" data-selected="true" data-i18n="artifactCanvasTab">Canvas</button>
311
+ <button id="artifact-tab-pdf" class="artifact-tab" type="button" data-artifact-tab="pdf" data-i18n="artifactPdfTab">PDF Reader</button>
311
312
  <button id="artifact-tab-explorer" class="artifact-tab" type="button" data-artifact-tab="explorer" data-i18n="artifactExplorerTab">Explorer</button>
312
313
  <button id="artifact-tab-notifications" class="artifact-tab" type="button" data-artifact-tab="notifications" data-i18n="artifactNotificationsTab">Notifications</button>
313
314
  </div>
package/public/styles.css CHANGED
@@ -14,6 +14,13 @@
14
14
  box-sizing: border-box;
15
15
  }
16
16
 
17
+ html,
18
+ body {
19
+ width: 100%;
20
+ max-width: 100%;
21
+ overflow-x: hidden;
22
+ }
23
+
17
24
  body {
18
25
  margin: 0;
19
26
  font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
@@ -25,20 +32,24 @@ body {
25
32
  }
26
33
 
27
34
  .page {
28
- width: min(1240px, calc(100% - 32px));
35
+ width: min(1240px, 100%);
36
+ max-width: 100%;
29
37
  margin: 32px auto;
38
+ padding: 0 16px;
30
39
  display: grid;
31
40
  gap: 20px;
32
- grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
41
+ grid-template-columns: minmax(280px, 400px) minmax(0, 1fr);
33
42
  }
34
43
 
35
44
  .stack {
36
45
  display: grid;
46
+ min-width: 0;
37
47
  align-content: start;
38
48
  gap: 14px;
39
49
  }
40
50
 
41
51
  .panel {
52
+ min-width: 0;
42
53
  background: var(--panel);
43
54
  border: 1px solid var(--line);
44
55
  border-radius: 18px;
@@ -137,6 +148,7 @@ button {
137
148
  input,
138
149
  textarea,
139
150
  select {
151
+ min-width: 0;
140
152
  width: 100%;
141
153
  border: 1px solid var(--line);
142
154
  background: #fffdfa;
@@ -306,6 +318,8 @@ button.danger {
306
318
  .actions,
307
319
  .header-row {
308
320
  display: flex;
321
+ min-width: 0;
322
+ flex-wrap: wrap;
309
323
  gap: 12px;
310
324
  align-items: center;
311
325
  justify-content: space-between;
@@ -417,6 +431,7 @@ button.danger {
417
431
  font-size: 0.72rem;
418
432
  line-height: 1.42;
419
433
  white-space: pre-wrap;
434
+ overflow-wrap: anywhere;
420
435
  }
421
436
 
422
437
  .sandbox-card h2 {
@@ -444,6 +459,7 @@ button.danger {
444
459
  font-size: 0.78rem;
445
460
  line-height: 1.45;
446
461
  white-space: pre-wrap;
462
+ overflow-wrap: anywhere;
447
463
  }
448
464
 
449
465
  .session-select {
@@ -452,13 +468,18 @@ button.danger {
452
468
 
453
469
  .session-tools {
454
470
  display: flex;
455
- min-width: min(100%, 420px);
471
+ flex: 1 1 420px;
472
+ min-width: 0;
473
+ max-width: 100%;
474
+ flex-wrap: wrap;
456
475
  gap: 8px;
457
476
  align-items: center;
458
477
  justify-content: flex-end;
459
478
  }
460
479
 
461
480
  .session-tools .session-select {
481
+ flex: 1 1 220px;
482
+ min-width: 0;
462
483
  max-width: none;
463
484
  }
464
485
 
@@ -476,6 +497,7 @@ button.danger {
476
497
  font-size: 0.9rem;
477
498
  line-height: 1.5;
478
499
  white-space: pre-wrap;
500
+ overflow-wrap: anywhere;
479
501
  }
480
502
 
481
503
  .chat-panel {
@@ -514,6 +536,7 @@ button.danger {
514
536
 
515
537
  .chat-content {
516
538
  line-height: 1.5;
539
+ overflow-wrap: anywhere;
517
540
  }
518
541
 
519
542
  .chat-form {
@@ -522,6 +545,7 @@ button.danger {
522
545
 
523
546
  .modal {
524
547
  width: min(720px, calc(100% - 28px));
548
+ max-width: 100%;
525
549
  padding: 0;
526
550
  border: 0;
527
551
  border-radius: 22px;
@@ -604,6 +628,7 @@ button.danger {
604
628
 
605
629
  .artifact-modal {
606
630
  width: min(1120px, calc(100% - 28px));
631
+ max-width: 100%;
607
632
  }
608
633
 
609
634
  .artifact-modal-card {
@@ -613,6 +638,7 @@ button.danger {
613
638
 
614
639
  .artifact-tabs {
615
640
  display: flex;
641
+ max-width: 100%;
616
642
  flex-wrap: wrap;
617
643
  gap: 8px;
618
644
  padding: 5px;
@@ -623,7 +649,7 @@ button.danger {
623
649
 
624
650
  .artifact-tab {
625
651
  flex: 1;
626
- min-width: 130px;
652
+ min-width: min(130px, 100%);
627
653
  border: 1px solid transparent;
628
654
  background: transparent;
629
655
  color: var(--muted);
@@ -639,6 +665,7 @@ button.danger {
639
665
  .artifact-layout {
640
666
  display: grid;
641
667
  min-height: 520px;
668
+ max-width: 100%;
642
669
  gap: 14px;
643
670
  grid-template-columns: minmax(240px, 340px) minmax(0, 1fr);
644
671
  overflow: hidden;
@@ -674,6 +701,7 @@ button.danger {
674
701
  .artifact-list {
675
702
  display: grid;
676
703
  align-content: start;
704
+ min-width: 0;
677
705
  gap: 8px;
678
706
  overflow: auto;
679
707
  padding: 10px;
@@ -681,7 +709,8 @@ button.danger {
681
709
 
682
710
  .artifact-row {
683
711
  display: grid;
684
- gap: 5px;
712
+ grid-template-columns: minmax(0, 1fr) auto;
713
+ gap: 8px;
685
714
  width: 100%;
686
715
  padding: 11px 12px;
687
716
  border: 1px solid var(--line);
@@ -691,6 +720,40 @@ button.danger {
691
720
  text-align: left;
692
721
  }
693
722
 
723
+ .artifact-row-main {
724
+ display: grid;
725
+ min-width: 0;
726
+ gap: 5px;
727
+ padding: 0;
728
+ border: 0;
729
+ border-radius: 0;
730
+ background: transparent;
731
+ color: inherit;
732
+ box-shadow: none;
733
+ text-align: left;
734
+ }
735
+
736
+ .artifact-menu-button {
737
+ width: 34px;
738
+ min-width: 34px;
739
+ height: 34px;
740
+ align-self: start;
741
+ padding: 0;
742
+ border: 1px solid var(--line);
743
+ border-radius: 999px;
744
+ background: rgba(255, 253, 250, 0.9);
745
+ color: var(--muted);
746
+ font-weight: 900;
747
+ letter-spacing: 0.08em;
748
+ box-shadow: none;
749
+ }
750
+
751
+ .artifact-menu-button:hover,
752
+ .artifact-menu-button:focus-visible {
753
+ border-color: rgba(15, 118, 110, 0.34);
754
+ color: var(--accent);
755
+ }
756
+
694
757
  .artifact-row[data-selected="true"] {
695
758
  border-color: rgba(15, 118, 110, 0.62);
696
759
  box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.1);
@@ -715,6 +778,7 @@ button.danger {
715
778
  .artifact-row-top,
716
779
  .artifact-viewer-header {
717
780
  display: flex;
781
+ min-width: 0;
718
782
  gap: 10px;
719
783
  align-items: flex-start;
720
784
  justify-content: space-between;
@@ -722,6 +786,7 @@ button.danger {
722
786
 
723
787
  .artifact-row-top strong,
724
788
  .artifact-viewer-header h3 {
789
+ min-width: 0;
725
790
  overflow: hidden;
726
791
  text-overflow: ellipsis;
727
792
  }
@@ -752,6 +817,7 @@ button.danger {
752
817
  .artifact-row-preview {
753
818
  display: -webkit-box;
754
819
  overflow: hidden;
820
+ overflow-wrap: anywhere;
755
821
  -webkit-box-orient: vertical;
756
822
  -webkit-line-clamp: 2;
757
823
  }
@@ -808,6 +874,7 @@ button.danger {
808
874
 
809
875
  .artifact-pdf-frame {
810
876
  width: 100%;
877
+ max-width: 100%;
811
878
  min-height: 62vh;
812
879
  border: 1px solid var(--line);
813
880
  border-radius: 14px;
@@ -828,6 +895,8 @@ button.danger {
828
895
 
829
896
  @media (max-width: 960px) {
830
897
  .page {
898
+ margin: 16px auto;
899
+ padding: 0 12px;
831
900
  grid-template-columns: 1fr;
832
901
  }
833
902
 
@@ -876,3 +945,23 @@ button.danger {
876
945
  overflow: auto;
877
946
  }
878
947
  }
948
+
949
+ @media (max-width: 720px) {
950
+ .grid,
951
+ .checks,
952
+ .wrapper-controls {
953
+ grid-template-columns: 1fr;
954
+ }
955
+
956
+ .panel {
957
+ padding: 15px;
958
+ }
959
+
960
+ .artifact-tabs {
961
+ border-radius: 18px;
962
+ }
963
+
964
+ .artifact-tab {
965
+ min-width: 120px;
966
+ }
967
+ }