@narumitw/pi-image-drop 0.25.0 → 0.30.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.
@@ -4,112 +4,12 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <meta name="color-scheme" content="light dark" />
7
+ <meta name="csp-nonce" content="__PI_CSP_NONCE__" />
7
8
  <title>Pi Image Drop</title>
8
9
  <link rel="stylesheet" href="/styles.css" />
9
10
  </head>
10
11
  <body>
11
- <header class="page-header">
12
- <div>
13
- <p class="eyebrow">Pi local image staging</p>
14
- <h1>Image Drop</h1>
15
- <p id="session-label" class="session-label">Connecting to Pi…</p>
16
- </div>
17
- <details class="session-details">
18
- <summary>Session details</summary>
19
- <dl>
20
- <dt>Working directory</dt>
21
- <dd id="cwd">—</dd>
22
- </dl>
23
- </details>
24
- </header>
25
-
26
- <main>
27
- <section id="drop-zone" class="drop-zone" aria-labelledby="drop-title">
28
- <div class="drop-copy">
29
- <span class="drop-icon" aria-hidden="true">🖼️</span>
30
- <div>
31
- <h2 id="drop-title">Add images</h2>
32
- <p>Paste anywhere, drop files here, or choose images.</p>
33
- </div>
34
- <button id="choose-files" class="primary" type="button">Choose images</button>
35
- <input id="file-input" type="file" multiple accept="image/png,image/jpeg,image/webp,image/gif,image/bmp,image/tiff,image/heic,image/heif,image/avif,.bmp,.tif,.tiff,.heic,.heif,.avif" hidden />
36
- </div>
37
- </section>
38
-
39
- <p class="collection-note">Sensitive image metadata removed from processed images.</p>
40
-
41
- <section class="batch draft" aria-labelledby="batch-title">
42
- <div class="batch-toolbar">
43
- <div>
44
- <h2 id="batch-title">Ready for next message</h2>
45
- <p id="status" hidden></p>
46
- <p id="next-step" class="next-step" role="status" aria-live="polite">Choose images to add them to your next Pi message.</p>
47
- </div>
48
- <button id="clear-all" class="danger-secondary" type="button" hidden disabled>Clear all</button>
49
- </div>
50
- <div id="error-banner" class="banner error" role="alert" hidden></div>
51
- <div id="grid" class="image-grid" aria-live="polite"></div>
52
- </section>
53
-
54
- <section class="history" aria-labelledby="history-title">
55
- <header class="history-toolbar">
56
- <div class="history-summary">
57
- <h2 id="history-title">Previously sent</h2>
58
- <p id="history-status" role="status" aria-live="polite">No images sent yet</p>
59
- <p class="history-note"><span id="history-retention">0 images retained</span>. Images here are not attached again unless you choose <strong>Add again</strong>. The oldest are removed at the configured memory limit, and all are cleared when this Pi session ends.</p>
60
- </div>
61
- <button id="clear-history" class="danger-secondary" type="button" hidden disabled>Clear history</button>
62
- </header>
63
- <div id="history-grid" class="image-grid" aria-live="polite"></div>
64
- </section>
65
-
66
- <p class="privacy">
67
- Draft and previously sent images stay in this Pi process until removed, evicted at the retention limit, or the session ends. Sending a Pi message sends its staged images to your configured model provider; deleting local history does not retract images already sent.
68
- </p>
69
- </main>
70
-
71
- <div id="connection-overlay" class="connection-overlay" role="alert" hidden>
72
- <div>
73
- <h2 id="connection-title">Connection lost</h2>
74
- <p id="connection-message">Run <code>/image-drop</code> in Pi for a new link.</p>
75
- </div>
76
- </div>
77
-
78
- <dialog id="clear-dialog">
79
- <form method="dialog">
80
- <h2>Clear every staged image?</h2>
81
- <p>This removes the current batch from Pi memory.</p>
82
- <div class="dialog-actions">
83
- <button type="submit" value="cancel">Cancel</button>
84
- <button type="submit" class="danger" value="confirm">Clear all</button>
85
- </div>
86
- </form>
87
- </dialog>
88
-
89
- <dialog id="clear-history-dialog">
90
- <form method="dialog">
91
- <h2>Clear sent image history?</h2>
92
- <p>This releases every retained image from this Pi session. Images already sent to Pi or a model provider are unaffected.</p>
93
- <div class="dialog-actions">
94
- <button type="submit" value="cancel">Cancel</button>
95
- <button type="submit" class="danger" value="confirm">Clear history</button>
96
- </div>
97
- </form>
98
- </dialog>
99
-
100
- <dialog id="image-preview-dialog" class="image-preview-dialog" aria-labelledby="image-preview-title">
101
- <div class="image-preview-content">
102
- <header>
103
- <h2 id="image-preview-title">Image preview</h2>
104
- </header>
105
- <div class="image-preview-stage">
106
- <button id="image-preview-dismiss" class="image-preview-dismiss" type="button" aria-label="Close enlarged image">
107
- <img id="image-preview" alt="" />
108
- </button>
109
- </div>
110
- </div>
111
- </dialog>
112
-
12
+ <div id="root"></div>
113
13
  <script type="module" src="/app.js"></script>
114
14
  </body>
115
15
  </html>
package/src/web/state.js CHANGED
@@ -1,114 +1,5 @@
1
- export function summarizeBatch(batch) {
2
- const counts = { ready: 0, uploading: 0, error: 0 };
3
- for (const item of batch.items ?? []) {
4
- if (item.status === "ready") counts.ready += 1;
5
- else if (item.status === "error") counts.error += 1;
6
- else counts.uploading += 1;
7
- }
8
- return {
9
- ...counts,
10
- total: (batch.items ?? []).length,
11
- bytes: Number(batch.totalSourceBytes ?? 0),
12
- label: statusLabel(batch.phase, counts, (batch.items ?? []).length),
13
- };
14
- }
15
-
16
- export function summarizeHistory(history) {
17
- const total = (history?.items ?? []).length;
18
- const bytes = Number(history?.totalBytes ?? 0);
19
- const maxImages = Number(history?.maxImages ?? 0);
20
- const maxBytes = Number(history?.maxBytes ?? 0);
21
- return {
22
- total,
23
- bytes,
24
- maxImages,
25
- maxBytes,
26
- label: total === 0 ? "No images sent yet" : `${total} ${plural(total, "image")} · ${formatBytes(bytes)}`,
27
- usage: `${total}/${maxImages} images · ${formatBytes(bytes)} of ${formatBytes(maxBytes)}`,
28
- };
29
- }
30
-
31
- const SHARED_METADATA_NOTE = "Sensitive image metadata removed";
32
-
33
- export function draftPresentation(batch) {
34
- const summary = summarizeBatch(batch);
35
- return {
36
- status: summary.total === 0 ? "" : `${summary.label} · ${formatBytes(summary.bytes)}`,
37
- guidance: draftGuidance(batch),
38
- };
39
- }
40
-
41
- export function draftGuidance(batch) {
42
- const summary = summarizeBatch(batch);
43
- if (batch.phase === "closed") return "This Pi session is no longer accepting images.";
44
- if (batch.phase === "reserved") {
45
- return "Queued with Pi. These images will be attached when Pi sends this message.";
46
- }
47
- if (summary.total === 0) return "Choose images to add them to your next Pi message.";
48
- if (summary.error > 0) {
49
- return `Fix or delete ${summary.error} ${plural(summary.error, "image")} that ${summary.error === 1 ? "needs" : "need"} attention before sending from Pi.`;
50
- }
51
- if (summary.uploading > 0) {
52
- return `Wait for ${summary.uploading} ${plural(summary.uploading, "image")} to finish processing before sending from Pi.`;
53
- }
54
- return `Return to Pi and send a non-empty message. ${summary.ready} ready ${plural(summary.ready, "image")} will be attached automatically.`;
55
- }
56
-
57
- export function statusLabel(phase, counts, total) {
58
- if (phase === "empty" || total === 0) return "No images staged";
59
- if (phase === "reserved") return `${total} ${plural(total, "image")} queued with Pi`;
60
- const parts = [`${counts.ready}/${total} ready`];
61
- if (counts.uploading > 0) parts.push(`${counts.uploading} uploading`);
62
- if (counts.error > 0) parts.push(`${counts.error} need attention`);
63
- return parts.join(" · ");
64
- }
65
-
66
- export function visibleItemNotes(notes) {
67
- return Array.isArray(notes) ? notes.filter((note) => note !== SHARED_METADATA_NOTE) : [];
68
- }
69
-
70
- export function moveItem(ids, id, direction) {
71
- const from = ids.indexOf(id);
72
- if (from === -1) return [...ids];
73
- const to = Math.max(0, Math.min(ids.length - 1, from + direction));
74
- if (to === from) return [...ids];
75
- const next = [...ids];
76
- next.splice(from, 1);
77
- next.splice(to, 0, id);
78
- return next;
79
- }
80
-
81
- export function moveItemBefore(ids, id, targetId) {
82
- if (id === targetId || !ids.includes(id) || !ids.includes(targetId)) return [...ids];
83
- const next = ids.filter((candidate) => candidate !== id);
84
- next.splice(next.indexOf(targetId), 0, id);
85
- return next;
86
- }
87
-
88
- export function canMutate(batch) {
89
- return batch.phase !== "reserved" && batch.phase !== "closed";
90
- }
91
-
92
- export function preferNewestState(current, next) {
93
- if (!current || next.batch.revision >= current.batch.revision) return next;
94
- return current;
95
- }
96
-
97
- export async function attemptMutation(operation) {
98
- try {
99
- return { ok: true, value: await operation() };
100
- } catch (error) {
101
- return { ok: false, error };
102
- }
103
- }
104
-
105
- export function formatBytes(value) {
106
- const bytes = Math.max(0, Number(value) || 0);
107
- if (bytes < 1024) return `${bytes} B`;
108
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)} KB`;
109
- return `${(bytes / (1024 * 1024)).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
110
- }
111
-
112
- function plural(count, noun) {
113
- return count === 1 ? noun : `${noun}s`;
114
- }
1
+ /* Generated by scripts/build-web.mjs. Do not edit directly. */
2
+ const __webpack_nonce__ = typeof document === "undefined"
3
+ ? undefined
4
+ : document.querySelector('meta[name="csp-nonce"]')?.content;
5
+ function u(t){let e={ready:0,uploading:0,error:0};for(let r of t.items??[])r.status==="ready"?e.ready+=1:r.status==="error"?e.error+=1:e.uploading+=1;return{...e,total:(t.items??[]).length,bytes:Number(t.totalSourceBytes??0),label:c(t.phase,e,(t.items??[]).length)}}function f(t){let e=(t?.items??[]).length,r=Number(t?.totalBytes??0),n=Number(t?.maxImages??0),i=Number(t?.maxBytes??0);return{total:e,bytes:r,maxImages:n,maxBytes:i,label:e===0?"No images sent yet":`${e} ${s(e,"image")} \xB7 ${a(r)}`,usage:`${e}/${n} images \xB7 ${a(r)} of ${a(i)}`}}var m="Sensitive image metadata removed";function g(t){let e=u(t);return{status:e.total===0?"":`${e.label} \xB7 ${a(e.bytes)}`,guidance:l(t)}}function l(t){let e=u(t);return t.phase==="closed"?"This Pi session is no longer accepting images.":t.phase==="reserved"?"Queued with Pi. These images will be attached when Pi sends this message.":e.total===0?"Choose images to add them to your next Pi message.":e.error>0?`Fix or delete ${e.error} ${s(e.error,"image")} that ${e.error===1?"needs":"need"} attention before sending from Pi.`:e.uploading>0?`Wait for ${e.uploading} ${s(e.uploading,"image")} to finish processing before sending from Pi.`:`Return to Pi and send a non-empty message. ${e.ready} ready ${s(e.ready,"image")} will be attached automatically.`}function c(t,e,r){if(t==="empty"||r===0)return"No images staged";if(t==="reserved")return`${r} ${s(r,"image")} queued with Pi`;let n=[`${e.ready}/${r} ready`];return e.uploading>0&&n.push(`${e.uploading} uploading`),e.error>0&&n.push(`${e.error} need attention`),n.join(" \xB7 ")}function p(t){return Array.isArray(t)?t.filter(e=>typeof e=="string"&&e!==m):[]}function d(t,e,r){let n=t.indexOf(e);if(n===-1)return[...t];let i=Math.max(0,Math.min(t.length-1,n+r));if(i===n)return[...t];let o=[...t];return o.splice(n,1),o.splice(i,0,e),o}function y(t,e,r){if(e===r||!t.includes(e)||!t.includes(r))return[...t];let n=t.filter(i=>i!==e);return n.splice(n.indexOf(r),0,e),n}function h(t){return t.phase!=="reserved"&&t.phase!=="closed"}function b(t,e){return!t||e.batch.revision>=t.batch.revision?e:t}async function x(t){try{return{ok:!0,value:await t()}}catch(e){return{ok:!1,error:e}}}function a(t){let e=Math.max(0,Number(t)||0);return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(e<10*1024?1:0)} KB`:`${(e/(1024*1024)).toFixed(e<10*1024*1024?1:0)} MB`}function s(t,e){return t===1?e:`${e}s`}export{x as attemptMutation,h as canMutate,l as draftGuidance,g as draftPresentation,a as formatBytes,d as moveItem,y as moveItemBefore,b as preferNewestState,c as statusLabel,u as summarizeBatch,f as summarizeHistory,p as visibleItemNotes};