cue-console 0.1.9 → 0.1.12

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/README.md CHANGED
@@ -2,11 +2,14 @@
2
2
 
3
3
  [![Repo: cue-stack](https://img.shields.io/badge/repo-cue--stack-111827)](https://github.com/nmhjklnm/cue-stack)
4
4
  [![Repo: cue-console](https://img.shields.io/badge/repo-cue--console-111827)](https://github.com/nmhjklnm/cue-console)
5
+ [![Repo: cue-command](https://img.shields.io/badge/repo-cue--command-111827)](https://github.com/nmhjklnm/cue-command)
5
6
  [![Repo: cue-mcp](https://img.shields.io/badge/repo-cue--mcp-111827)](https://github.com/nmhjklnm/cue-mcp)
6
7
 
7
8
  [![npm](https://img.shields.io/npm/v/cue-console?label=cue-console&color=CB3837)](https://www.npmjs.com/package/cue-console)
8
9
  [![npm downloads](https://img.shields.io/npm/dw/cue-console?color=CB3837)](https://www.npmjs.com/package/cue-console)
9
10
 
11
+ [Contributing](./CONTRIBUTING.md) · [Trademark](./TRADEMARK.md)
12
+
10
13
  | Mobile | Desktop |
11
14
  | --- | --- |
12
15
  | ![Mobile screenshot](./assets/iphone.png) | ![Desktop screenshot](./assets/desktop.png) |
@@ -27,7 +30,9 @@ Think of it as an “all-in-one” collaboration console for your agents and CLI
27
30
 
28
31
  ### Goal
29
32
 
30
- Run the console and pair it with `cuemcp`.
33
+ Run the console and pair it with `cueme` (recommended).
34
+
35
+ Note: `cuemcp` (MCP mode) can be blocked/flagged by some IDEs, so command mode is currently recommended.
31
36
 
32
37
  ### Step 1: Install `cue-console`
33
38
 
@@ -38,24 +43,36 @@ npm install -g cue-console
38
43
  ### Step 2: Start `cue-console`
39
44
 
40
45
  ```bash
41
- cue-console dev --port 3000
46
+ cue-console start
42
47
  ```
43
48
 
44
49
  Alternatively, you can run it without installing globally:
45
50
 
46
51
  ```bash
47
- npx cue-console dev --port 3000
52
+ npx cue-console start
48
53
  ```
49
54
 
50
55
  Open `http://localhost:3000`.
51
56
 
52
- ### Step 3: Start `cuemcp`
57
+ ### Step 3: Install `cueme` (recommended)
53
58
 
54
- Add and run the MCP server in your agent/runtime (see [`cue-mcp`](https://github.com/nmhjklnm/cue-mcp) for client-specific MCP configuration).
59
+ ```bash
60
+ npm install -g cueme
61
+ ```
62
+
63
+ ### Step 4: Configure your system prompt (HAP)
55
64
 
56
- ### Step 4: Connect your runtime
65
+ Add the contents of `cue-command/protocol.md` to your tool's system prompt / rules (see [`cue-command`](https://github.com/nmhjklnm/cue-command)).
57
66
 
58
- In the agent/runtime you want to use, type `cue` to trigger connect (or reconnect) and route the collaboration flow to `cue-console`.
67
+ ### Step 5: Connect your runtime
68
+
69
+ In the agent/runtime you want to use, call `cueme cue <agent_id> -` / `cueme pause <agent_id> -` (see `cue-command/protocol.md`).
70
+
71
+ ---
72
+
73
+ ### Optional: MCP mode (`cuemcp`)
74
+
75
+ Add and run the MCP server in your agent/runtime (see [`cue-mcp`](https://github.com/nmhjklnm/cue-mcp) for client-specific MCP configuration).
59
76
 
60
77
  ---
61
78
 
@@ -123,6 +140,7 @@ Not calling cue() means the user cannot continue the interaction.
123
140
  After installation, the `cue-console` command is available:
124
141
 
125
142
  ```bash
143
+ cue-console start
126
144
  cue-console dev --port 3000
127
145
  cue-console build
128
146
  cue-console start --host 0.0.0.0 --port 3000
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawn } = require("node:child_process");
4
+ const fs = require("node:fs");
4
5
  const path = require("node:path");
5
6
 
6
7
  function printHelp() {
7
- process.stdout.write(`cue-console - Cue Hub console launcher\n\nUsage:\n cue-console <dev|build|start> [--port <port>] [--host <host>]\n\nExamples:\n cue-console dev --port 3000\n cue-console start --host 0.0.0.0 --port 3000\n`);
8
+ process.stdout.write(
9
+ `cue-console - Cue Hub console launcher\n\nUsage:\n cue-console <dev|build|start> [--port <port>] [--host <host>]\n\nExamples:\n cue-console start --port 3000\n cue-console start --host 0.0.0.0 --port 3000\n\nNotes:\n - start will auto-build if needed (when .next is missing)\n`
10
+ );
8
11
  }
9
12
 
10
13
  function parseArgs(argv) {
@@ -70,16 +73,30 @@ async function main() {
70
73
 
71
74
  const pkgRoot = path.resolve(__dirname, "..");
72
75
 
73
- const child = spawn(process.execPath, [nextBin, command, ...passthrough], {
74
- stdio: "inherit",
75
- env,
76
- cwd: pkgRoot,
77
- });
76
+ const spawnNext = (subcmd) =>
77
+ new Promise((resolve, reject) => {
78
+ const child = spawn(process.execPath, [nextBin, subcmd, ...passthrough], {
79
+ stdio: "inherit",
80
+ env,
81
+ cwd: pkgRoot,
82
+ });
83
+ child.on("exit", (code, signal) => {
84
+ if (signal) return reject(Object.assign(new Error(`terminated: ${signal}`), { code, signal }));
85
+ if (code && code !== 0) return reject(Object.assign(new Error(`exit ${code}`), { code }));
86
+ resolve();
87
+ });
88
+ });
89
+
90
+ if (command === "start") {
91
+ const buildIdPath = path.join(pkgRoot, ".next", "BUILD_ID");
92
+ if (!fs.existsSync(buildIdPath)) {
93
+ await spawnNext("build");
94
+ }
95
+ await spawnNext("start");
96
+ return;
97
+ }
78
98
 
79
- child.on("exit", (code, signal) => {
80
- if (signal) process.kill(process.pid, signal);
81
- process.exit(code ?? 0);
82
- });
99
+ await spawnNext(command);
83
100
  }
84
101
 
85
102
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cue-console",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Cue Hub console launcher (Next.js UI)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": ["mcp", "cue", "console", "nextjs"],
@@ -33,6 +33,7 @@
33
33
  "@radix-ui/react-separator": "^1.1.8",
34
34
  "@radix-ui/react-slot": "^1.2.4",
35
35
  "@tailwindcss/postcss": "^4",
36
+ "@types/better-sqlite3": "^7.6.13",
36
37
  "@types/node": "^20",
37
38
  "@dicebear/core": "^9.2.4",
38
39
  "@dicebear/thumbs": "^9.2.4",
@@ -54,7 +55,6 @@
54
55
  "uuid": "^13.0.0"
55
56
  },
56
57
  "devDependencies": {
57
- "@types/better-sqlite3": "^7.6.13",
58
58
  "@types/react": "^19",
59
59
  "@types/react-dom": "^19",
60
60
  "@types/uuid": "^11.0.0",
package/src/app/page.tsx CHANGED
@@ -5,23 +5,115 @@ import { ConversationList } from "@/components/conversation-list";
5
5
  import { ChatView } from "@/components/chat-view";
6
6
  import { CreateGroupDialog } from "@/components/create-group-dialog";
7
7
  import { MessageCircle } from "lucide-react";
8
+ import { claimWorkerLease, processQueueTick } from "@/lib/actions";
8
9
 
9
10
  export default function Home() {
10
11
  const [selectedId, setSelectedId] = useState<string | null>(null);
11
12
  const [selectedType, setSelectedType] = useState<"agent" | "group" | null>(null);
12
13
  const [selectedName, setSelectedName] = useState<string>("");
13
14
  const [showCreateGroup, setShowCreateGroup] = useState(false);
14
- const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
15
- if (typeof window === "undefined") return false;
15
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
16
+
17
+ useEffect(() => {
18
+ let stopped = false;
19
+ const holderId =
20
+ (globalThis.crypto && "randomUUID" in globalThis.crypto
21
+ ? (globalThis.crypto as Crypto).randomUUID()
22
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`);
23
+ const leaseKey = "cue-console:global-queue-worker";
24
+ const leaseTtlMs = 12_000;
25
+ const claimEveryMs = 4_000;
26
+ const tickEveryMs = 3_000;
27
+
28
+ let isLeader = false;
29
+ let tickTimer: ReturnType<typeof setInterval> | null = null;
30
+ let claimTimer: ReturnType<typeof setInterval> | null = null;
31
+
32
+ const stopTick = () => {
33
+ if (tickTimer) {
34
+ clearInterval(tickTimer);
35
+ tickTimer = null;
36
+ }
37
+ };
38
+
39
+ const startTick = () => {
40
+ if (tickTimer) return;
41
+ tickTimer = setInterval(() => {
42
+ if (document.visibilityState !== "visible") return;
43
+ void (async () => {
44
+ try {
45
+ const res = await processQueueTick(holderId);
46
+ if ((res?.sent ?? 0) > 0 || (res?.failed ?? 0) > 0 || (res?.rescheduled ?? 0) > 0) {
47
+ const removedQueueIds = Array.isArray((res as { removedQueueIds?: unknown })?.removedQueueIds)
48
+ ? ((res as { removedQueueIds: string[] }).removedQueueIds || [])
49
+ : [];
50
+ window.dispatchEvent(
51
+ new CustomEvent("cue-console:queueUpdated", {
52
+ detail: {
53
+ removedQueueIds,
54
+ },
55
+ })
56
+ );
57
+ }
58
+ } catch {
59
+ // ignore
60
+ }
61
+ })();
62
+ }, tickEveryMs);
63
+ };
64
+
65
+ const claimOnce = async () => {
66
+ try {
67
+ const res = await claimWorkerLease({ leaseKey, holderId, ttlMs: leaseTtlMs });
68
+ const nextLeader = Boolean(res.acquired && res.holderId === holderId);
69
+ if (nextLeader !== isLeader) {
70
+ isLeader = nextLeader;
71
+ if (isLeader) {
72
+ startTick();
73
+ } else {
74
+ stopTick();
75
+ }
76
+ }
77
+ } catch {
78
+ // ignore
79
+ }
80
+ };
81
+
82
+ const boot = async () => {
83
+ await claimOnce();
84
+ if (stopped) return;
85
+ claimTimer = setInterval(() => {
86
+ if (document.visibilityState !== "visible") return;
87
+ void claimOnce();
88
+ }, claimEveryMs);
89
+ };
90
+
91
+ void boot();
92
+
93
+ const onVisibilityChange = () => {
94
+ if (document.visibilityState === "visible") {
95
+ void claimOnce();
96
+ }
97
+ };
98
+ document.addEventListener("visibilitychange", onVisibilityChange);
99
+
100
+ return () => {
101
+ stopped = true;
102
+ document.removeEventListener("visibilitychange", onVisibilityChange);
103
+ stopTick();
104
+ if (claimTimer) clearInterval(claimTimer);
105
+ };
106
+ }, []);
107
+
108
+ useEffect(() => {
16
109
  try {
17
110
  const raw = window.localStorage.getItem("cuehub.sidebarCollapsed");
18
- if (raw === "1") return true;
19
- if (raw === "0") return false;
111
+ if (raw === "1") setSidebarCollapsed(true);
112
+ if (raw === "0") setSidebarCollapsed(false);
20
113
  } catch {
21
114
  // ignore
22
115
  }
23
- return false;
24
- });
116
+ }, []);
25
117
 
26
118
  useEffect(() => {
27
119
  try {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {
4
4
  useMemo,
5
+ useState,
5
6
  type ChangeEvent,
6
7
  type ClipboardEvent,
7
8
  type Dispatch,
@@ -12,7 +13,7 @@ import {
12
13
  import { Button } from "@/components/ui/button";
13
14
  import { cn, getAgentEmoji } from "@/lib/utils";
14
15
  import { setAgentDisplayName } from "@/lib/actions";
15
- import { Plus, Send, X } from "lucide-react";
16
+ import { CornerUpLeft, GripVertical, Plus, Send, Trash2, X } from "lucide-react";
16
17
 
17
18
  type MentionDraft = {
18
19
  userId: string;
@@ -21,6 +22,13 @@ type MentionDraft = {
21
22
  display: string;
22
23
  };
23
24
 
25
+ export type QueuedMessage = {
26
+ id: string;
27
+ text: string;
28
+ images: { mime_type: string; base64_data: string }[];
29
+ createdAt: number;
30
+ };
31
+
24
32
  const shiftMentions = (from: number, delta: number, list: MentionDraft[]) => {
25
33
  return list.map((m) => {
26
34
  if (m.start >= from) return { ...m, start: m.start + delta };
@@ -61,6 +69,11 @@ export function ChatComposer({
61
69
  setNotice,
62
70
  setPreviewImage,
63
71
  handleSend,
72
+ enqueueCurrent,
73
+ queue,
74
+ removeQueued,
75
+ recallQueued,
76
+ reorderQueue,
64
77
  handlePaste,
65
78
  handleImageUpload,
66
79
  textareaRef,
@@ -91,11 +104,16 @@ export function ChatComposer({
91
104
  hasPendingRequests: boolean;
92
105
  input: string;
93
106
  setInput: Dispatch<SetStateAction<string>>;
94
- images: { mime_type: string; base64_data: string }[];
95
- setImages: Dispatch<SetStateAction<{ mime_type: string; base64_data: string }[]>>;
107
+ images: { mime_type: string; base64_data: string; file_name?: string }[];
108
+ setImages: Dispatch<SetStateAction<{ mime_type: string; base64_data: string; file_name?: string }[]>>;
96
109
  setNotice: Dispatch<SetStateAction<string | null>>;
97
110
  setPreviewImage: Dispatch<SetStateAction<{ mime_type: string; base64_data: string } | null>>;
98
111
  handleSend: () => void | Promise<void>;
112
+ enqueueCurrent: () => void;
113
+ queue: QueuedMessage[];
114
+ removeQueued: (id: string) => void;
115
+ recallQueued: (id: string) => void;
116
+ reorderQueue: (fromIndex: number, toIndex: number) => void;
99
117
  handlePaste: (e: ClipboardEvent<HTMLTextAreaElement>) => void;
100
118
  handleImageUpload: (e: ChangeEvent<HTMLInputElement>) => void;
101
119
  textareaRef: RefObject<HTMLTextAreaElement | null>;
@@ -128,6 +146,17 @@ export function ChatComposer({
128
146
  : ({ left: "var(--cuehub-sidebar-w, 0px)", right: 0 } as const);
129
147
  }, [onBack]);
130
148
 
149
+ const [dragIndex, setDragIndex] = useState<number | null>(null);
150
+
151
+ const submitOrQueue = () => {
152
+ if (busy) return;
153
+ if (canSend) {
154
+ void handleSend();
155
+ return;
156
+ }
157
+ enqueueCurrent();
158
+ };
159
+
131
160
  return (
132
161
  <>
133
162
  {/* Input */}
@@ -139,17 +168,106 @@ export function ChatComposer({
139
168
  "glass-surface glass-noise"
140
169
  )}
141
170
  >
171
+ {/* Queue Panel */}
172
+ {queue.length > 0 && (
173
+ <div className="px-1 pt-1">
174
+ <div className="flex items-center justify-between">
175
+ <p className="text-[11px] text-muted-foreground">
176
+ {queue.length} messages queued
177
+ </p>
178
+ </div>
179
+ <div className="mt-1 max-h-28 overflow-y-auto pr-1">
180
+ <div className="space-y-1">
181
+ {queue.map((q, idx) => {
182
+ const summary = (q.text || "").split(/\r?\n/)[0] || "(empty)";
183
+ const hasImages = (q.images?.length || 0) > 0;
184
+ return (
185
+ <div
186
+ key={q.id}
187
+ className={cn(
188
+ "flex items-center gap-2 rounded-2xl px-2 py-1",
189
+ "bg-white/35 ring-1 ring-white/25"
190
+ )}
191
+ draggable
192
+ onDragStart={(e) => {
193
+ setDragIndex(idx);
194
+ e.dataTransfer.setData("text/plain", String(idx));
195
+ e.dataTransfer.effectAllowed = "move";
196
+ }}
197
+ onDragOver={(e) => {
198
+ e.preventDefault();
199
+ e.dataTransfer.dropEffect = "move";
200
+ }}
201
+ onDrop={(e) => {
202
+ e.preventDefault();
203
+ const raw = e.dataTransfer.getData("text/plain");
204
+ const from = Number(raw);
205
+ if (Number.isFinite(from)) reorderQueue(from, idx);
206
+ setDragIndex(null);
207
+ }}
208
+ onDragEnd={() => setDragIndex(null)}
209
+ data-dragging={dragIndex === idx ? "true" : "false"}
210
+ >
211
+ <span className="text-muted-foreground">
212
+ <GripVertical className="h-3.5 w-3.5" />
213
+ </span>
214
+ <div className="min-w-0 flex-1">
215
+ <p className="truncate text-xs">
216
+ {summary}
217
+ {hasImages ? " [img]" : ""}
218
+ </p>
219
+ </div>
220
+ <Button
221
+ type="button"
222
+ variant="ghost"
223
+ size="icon"
224
+ className="h-7 w-7 rounded-xl hover:bg-white/40"
225
+ onClick={() => recallQueued(q.id)}
226
+ title="Recall to input"
227
+ >
228
+ <CornerUpLeft className="h-3.5 w-3.5" />
229
+ </Button>
230
+ <Button
231
+ type="button"
232
+ variant="ghost"
233
+ size="icon"
234
+ className="h-7 w-7 rounded-xl hover:bg-white/40"
235
+ onClick={() => removeQueued(q.id)}
236
+ title="Remove from queue"
237
+ >
238
+ <Trash2 className="h-3.5 w-3.5" />
239
+ </Button>
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
244
+ </div>
245
+ </div>
246
+ )}
247
+
142
248
  {/* Image Preview */}
143
249
  {images.length > 0 && (
144
250
  <div className="flex max-w-full gap-2 overflow-x-auto px-0.5 pt-0.5">
145
251
  {images.map((img, i) => (
146
252
  <div key={i} className="relative shrink-0">
147
- <img
148
- src={`data:${img.mime_type};base64,${img.base64_data}`}
149
- alt=""
150
- className="h-16 w-16 rounded-xl object-cover shadow-sm ring-1 ring-border/60 cursor-pointer"
151
- onClick={() => setPreviewImage(img)}
152
- />
253
+ {img.mime_type.startsWith("image/") ? (
254
+ <img
255
+ src={`data:${img.mime_type};base64,${img.base64_data}`}
256
+ alt=""
257
+ className="h-16 w-16 rounded-xl object-cover shadow-sm ring-1 ring-border/60 cursor-pointer"
258
+ onClick={() => setPreviewImage(img)}
259
+ />
260
+ ) : (
261
+ <div
262
+ className="h-16 w-16 rounded-xl bg-white/40 dark:bg-black/20 ring-1 ring-border/60 shadow-sm flex flex-col items-center justify-center px-1"
263
+ title={`${img.file_name || "File"}${img.mime_type ? ` (${img.mime_type})` : ""}`}
264
+ >
265
+ <div className="text-[10px] font-semibold text-muted-foreground">FILE</div>
266
+ <div className="mt-0.5 text-[11px] font-semibold text-foreground/80 truncate w-full text-center">
267
+ {(img.file_name || "File").slice(0, 10)}
268
+ </div>
269
+ </div>
270
+ )}
153
271
  <button
154
272
  className="absolute -right-1 -top-1 rounded-full bg-destructive p-0.5 text-white"
155
273
  onClick={() => setImages((prev) => prev.filter((_, j) => j !== i))}
@@ -281,16 +399,16 @@ export function ChatComposer({
281
399
  placeholder={
282
400
  hasPendingRequests
283
401
  ? type === "group"
284
- ? "Type... (Enter to send, Shift+Enter for newline, supports @)"
285
- : "Type... (Enter to send, Shift+Enter for newline)"
402
+ ? "Type... (Enter to send or queue, Shift+Enter for newline, supports @)"
403
+ : "Type... (Enter to send or queue, Shift+Enter for newline)"
286
404
  : "Waiting for new pending requests..."
287
405
  }
288
406
  title={
289
407
  !hasPendingRequests
290
408
  ? "No pending requests (PENDING/PROCESSING). Send button is disabled."
291
409
  : type === "group"
292
- ? "Type @ to mention members; ↑↓ to navigate, Enter to insert; Enter to send, Shift+Enter for newline"
293
- : "Enter to send, Shift+Enter for newline"
410
+ ? "Type @ to mention members; ↑↓ to navigate, Enter to insert; Enter to send or queue, Shift+Enter for newline"
411
+ : "Enter to send or queue, Shift+Enter for newline"
294
412
  }
295
413
  value={input}
296
414
  onPaste={handlePaste}
@@ -392,7 +510,7 @@ export function ChatComposer({
392
510
 
393
511
  if (e.key === "Enter" && !e.shiftKey) {
394
512
  e.preventDefault();
395
- if (canSend) void handleSend();
513
+ submitOrQueue();
396
514
  }
397
515
  }}
398
516
  onKeyUp={() => {
@@ -440,39 +558,45 @@ export function ChatComposer({
440
558
  )}
441
559
  onClick={() => fileInputRef.current?.click()}
442
560
  disabled={busy}
443
- title="Add image"
561
+ title="Add file"
444
562
  >
445
563
  <Plus className="h-4.5 w-4.5" />
446
564
  </Button>
565
+
566
+ <Button
567
+ type="button"
568
+ variant="ghost"
569
+ size="sm"
570
+ className={cn(
571
+ "h-8 rounded-xl px-2",
572
+ "text-muted-foreground hover:text-foreground",
573
+ "hover:bg-white/40"
574
+ )}
575
+ onClick={() => {
576
+ if (busy) return;
577
+ enqueueCurrent();
578
+ }}
579
+ disabled={busy || (!input.trim() && images.length === 0)}
580
+ title="Queue (Enter)"
581
+ >
582
+ Queue
583
+ </Button>
447
584
  </div>
448
585
 
449
586
  <Button
450
587
  type="button"
451
588
  onClick={() => {
452
- if (canSend) {
453
- void handleSend();
454
- return;
455
- }
456
- if (!hasPendingRequests) {
457
- setNotice("No pending questions to answer.");
458
- return;
459
- }
460
- if (!input.trim() && images.length === 0) {
461
- setNotice("Enter a message to send, or select an image.");
462
- return;
463
- }
464
- setNotice("Unable to send right now. Please try again later.");
589
+ submitOrQueue();
465
590
  }}
466
- disabled={busy || (!canSend && (!input.trim() && images.length === 0))}
591
+ disabled={busy || (!input.trim() && images.length === 0)}
467
592
  className={cn(
468
593
  "h-8 w-8 rounded-xl p-0",
469
594
  canSend
470
595
  ? "bg-primary text-primary-foreground hover:bg-primary/90"
471
596
  : "bg-transparent text-muted-foreground hover:bg-white/40",
472
- (busy || (!canSend && (!input.trim() && images.length === 0))) &&
473
- "opacity-40 hover:bg-transparent"
597
+ (busy || (!input.trim() && images.length === 0)) && "opacity-40 hover:bg-transparent"
474
598
  )}
475
- title={canSend ? "Send" : "Enter a message"}
599
+ title={canSend ? "Send" : "Queue (cannot send now)"}
476
600
  >
477
601
  <Send className="h-4 w-4" />
478
602
  </Button>
@@ -481,7 +605,7 @@ export function ChatComposer({
481
605
  <input
482
606
  ref={fileInputRef}
483
607
  type="file"
484
- accept="image/*"
608
+ accept="*/*"
485
609
  multiple
486
610
  className="hidden"
487
611
  onChange={handleImageUpload}