orchid-ai 2.1.4 → 2.2.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/orchid-ai.css +297 -4
- package/package.json +1 -1
- package/src/components/ChatInput.jsx +199 -43
- package/src/components/ChatWindow.jsx +62 -3
- package/src/components/Message.jsx +213 -8
- package/src/components/visualizations/DataTable.jsx +48 -1
- package/src/hooks/useOrchidAiChat.js +236 -145
- package/src/index.d.ts +45 -2
package/orchid-ai.css
CHANGED
|
@@ -374,6 +374,39 @@
|
|
|
374
374
|
color: #ffffff;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
/* ── Attachment display inside user bubbles ── */
|
|
378
|
+
|
|
379
|
+
.ai-chat-user-attachments {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-wrap: wrap;
|
|
382
|
+
gap: 8px;
|
|
383
|
+
margin-bottom: 8px;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.ai-chat-attachment-img {
|
|
387
|
+
max-width: 220px;
|
|
388
|
+
max-height: 180px;
|
|
389
|
+
border-radius: 8px;
|
|
390
|
+
object-fit: cover;
|
|
391
|
+
display: block;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.ai-chat-attachment-file {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 6px;
|
|
398
|
+
padding: 6px 10px;
|
|
399
|
+
background: rgba(255, 255, 255, 0.18);
|
|
400
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
401
|
+
border-radius: 8px;
|
|
402
|
+
font-size: 12px;
|
|
403
|
+
color: rgba(255, 255, 255, 0.9);
|
|
404
|
+
max-width: 200px;
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
text-overflow: ellipsis;
|
|
407
|
+
white-space: nowrap;
|
|
408
|
+
}
|
|
409
|
+
|
|
377
410
|
.ai-chat-bubble.assistant {
|
|
378
411
|
position: relative;
|
|
379
412
|
background: #ffffff;
|
|
@@ -827,6 +860,39 @@
|
|
|
827
860
|
color: #1f2937;
|
|
828
861
|
}
|
|
829
862
|
|
|
863
|
+
.ai-chart-card-header {
|
|
864
|
+
display: flex;
|
|
865
|
+
align-items: center;
|
|
866
|
+
justify-content: space-between;
|
|
867
|
+
gap: 12px;
|
|
868
|
+
margin-bottom: 10px;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.ai-chart-card-header .ai-chart-title {
|
|
872
|
+
margin: 0;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.ai-chart-csv-btn {
|
|
876
|
+
display: inline-flex;
|
|
877
|
+
align-items: center;
|
|
878
|
+
gap: 4px;
|
|
879
|
+
flex-shrink: 0;
|
|
880
|
+
padding: 4px 10px;
|
|
881
|
+
font-size: 12px;
|
|
882
|
+
font-weight: 500;
|
|
883
|
+
color: #1eaaf1;
|
|
884
|
+
background: #ffffff;
|
|
885
|
+
border: 1px solid #d1d5db;
|
|
886
|
+
border-radius: 6px;
|
|
887
|
+
cursor: pointer;
|
|
888
|
+
transition: background 0.15s, border-color 0.15s;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.ai-chart-csv-btn:hover {
|
|
892
|
+
background: #f0f9ff;
|
|
893
|
+
border-color: #1eaaf1;
|
|
894
|
+
}
|
|
895
|
+
|
|
830
896
|
.ai-chart-error {
|
|
831
897
|
color: #b91c1c;
|
|
832
898
|
background: #fef2f2;
|
|
@@ -1844,8 +1910,9 @@
|
|
|
1844
1910
|
font-size: 22px;
|
|
1845
1911
|
font-weight: 700;
|
|
1846
1912
|
color: #1f2937;
|
|
1847
|
-
line-height: 1;
|
|
1913
|
+
line-height: 1.15;
|
|
1848
1914
|
margin-bottom: 5px;
|
|
1915
|
+
overflow-wrap: break-word;
|
|
1849
1916
|
}
|
|
1850
1917
|
|
|
1851
1918
|
.ai-stat-unit {
|
|
@@ -2183,6 +2250,23 @@
|
|
|
2183
2250
|
word-break: break-word;
|
|
2184
2251
|
}
|
|
2185
2252
|
|
|
2253
|
+
.ai-chat-process-url {
|
|
2254
|
+
color: var(--ai-accent, #3b82f6);
|
|
2255
|
+
text-decoration: none;
|
|
2256
|
+
}
|
|
2257
|
+
.ai-chat-process-url:hover {
|
|
2258
|
+
text-decoration: underline;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
.ai-chat-process-url-list {
|
|
2262
|
+
list-style: none;
|
|
2263
|
+
margin: 0;
|
|
2264
|
+
padding: 0;
|
|
2265
|
+
display: flex;
|
|
2266
|
+
flex-direction: column;
|
|
2267
|
+
gap: 2px;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2186
2270
|
/* ── Message Actions ── */
|
|
2187
2271
|
|
|
2188
2272
|
.ai-chat-message-actions {
|
|
@@ -2279,6 +2363,74 @@
|
|
|
2279
2363
|
height: 5px;
|
|
2280
2364
|
}
|
|
2281
2365
|
|
|
2366
|
+
/* Queued (pending) messages — shown as muted user bubbles while a prior request runs */
|
|
2367
|
+
|
|
2368
|
+
.ai-chat-message--queued {
|
|
2369
|
+
animation: none;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
.ai-chat-bubble--queued {
|
|
2373
|
+
opacity: 0.72;
|
|
2374
|
+
display: flex;
|
|
2375
|
+
flex-direction: column;
|
|
2376
|
+
gap: 8px;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
.ai-chat-pending-msg-text {
|
|
2380
|
+
white-space: pre-wrap;
|
|
2381
|
+
word-break: break-word;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
.ai-chat-pending-footer {
|
|
2385
|
+
display: flex;
|
|
2386
|
+
align-items: center;
|
|
2387
|
+
gap: 8px;
|
|
2388
|
+
flex-wrap: wrap;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
.ai-chat-pending-status {
|
|
2392
|
+
font-size: 11px;
|
|
2393
|
+
color: rgba(255, 255, 255, 0.6);
|
|
2394
|
+
font-style: italic;
|
|
2395
|
+
flex: 1;
|
|
2396
|
+
min-width: 0;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
.ai-chat-pending-actions {
|
|
2400
|
+
display: flex;
|
|
2401
|
+
gap: 6px;
|
|
2402
|
+
flex-wrap: wrap;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
.ai-chat-pending-btn {
|
|
2406
|
+
font-size: 11px;
|
|
2407
|
+
font-weight: 500;
|
|
2408
|
+
padding: 3px 10px;
|
|
2409
|
+
border-radius: 5px;
|
|
2410
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
2411
|
+
background: rgba(255, 255, 255, 0.15);
|
|
2412
|
+
color: rgba(255, 255, 255, 0.9);
|
|
2413
|
+
cursor: pointer;
|
|
2414
|
+
transition: background 0.15s, border-color 0.15s;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
.ai-chat-pending-btn:hover {
|
|
2418
|
+
background: rgba(255, 255, 255, 0.25);
|
|
2419
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
.ai-chat-pending-btn--primary {
|
|
2423
|
+
background: rgba(255, 255, 255, 0.28);
|
|
2424
|
+
border-color: rgba(255, 255, 255, 0.55);
|
|
2425
|
+
color: #ffffff;
|
|
2426
|
+
font-weight: 600;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
.ai-chat-pending-btn--primary:hover {
|
|
2430
|
+
background: rgba(255, 255, 255, 0.42);
|
|
2431
|
+
border-color: rgba(255, 255, 255, 0.75);
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2282
2434
|
.ai-chat-streaming-status {
|
|
2283
2435
|
display: flex;
|
|
2284
2436
|
align-items: center;
|
|
@@ -2318,6 +2470,19 @@
|
|
|
2318
2470
|
border: 1px solid #e5e7eb;
|
|
2319
2471
|
}
|
|
2320
2472
|
|
|
2473
|
+
.ai-building-block--stopped {
|
|
2474
|
+
background: #fafafa;
|
|
2475
|
+
border-color: #e5e7eb;
|
|
2476
|
+
opacity: 0.65;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
.ai-chat-interrupted-notice {
|
|
2480
|
+
font-size: 12px;
|
|
2481
|
+
color: #9ca3af;
|
|
2482
|
+
font-style: italic;
|
|
2483
|
+
margin-top: 8px;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2321
2486
|
.ai-building-block__label {
|
|
2322
2487
|
font-size: 13px;
|
|
2323
2488
|
font-weight: 600;
|
|
@@ -2357,18 +2522,123 @@
|
|
|
2357
2522
|
|
|
2358
2523
|
.ai-chat-input-wrapper {
|
|
2359
2524
|
display: flex;
|
|
2360
|
-
|
|
2525
|
+
flex-direction: column;
|
|
2361
2526
|
position: relative;
|
|
2362
|
-
gap: 10px;
|
|
2363
2527
|
max-width: 768px;
|
|
2364
2528
|
margin: 0 auto;
|
|
2365
2529
|
background: #f9fafb;
|
|
2366
2530
|
border: 1px solid #d1d5db;
|
|
2367
2531
|
border-radius: 14px;
|
|
2368
|
-
|
|
2532
|
+
overflow: hidden;
|
|
2369
2533
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
2370
2534
|
}
|
|
2371
2535
|
|
|
2536
|
+
/* ── Attachment preview strip ── */
|
|
2537
|
+
|
|
2538
|
+
.ai-attach-strip {
|
|
2539
|
+
display: flex;
|
|
2540
|
+
flex-wrap: wrap;
|
|
2541
|
+
gap: 6px;
|
|
2542
|
+
padding: 10px 12px 8px;
|
|
2543
|
+
border-bottom: 1px solid #e5e7eb;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
.ai-attach-chip {
|
|
2547
|
+
display: flex;
|
|
2548
|
+
align-items: center;
|
|
2549
|
+
gap: 5px;
|
|
2550
|
+
padding: 4px 6px 4px 8px;
|
|
2551
|
+
background: #e5e7eb;
|
|
2552
|
+
border-radius: 8px;
|
|
2553
|
+
font-size: 12px;
|
|
2554
|
+
color: #374151;
|
|
2555
|
+
max-width: 180px;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
.ai-attach-chip--img {
|
|
2559
|
+
padding: 3px 6px 3px 3px;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
.ai-attach-thumb {
|
|
2563
|
+
width: 42px;
|
|
2564
|
+
height: 42px;
|
|
2565
|
+
object-fit: cover;
|
|
2566
|
+
border-radius: 6px;
|
|
2567
|
+
flex-shrink: 0;
|
|
2568
|
+
display: block;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
.ai-attach-chip-icon {
|
|
2572
|
+
display: flex;
|
|
2573
|
+
align-items: center;
|
|
2574
|
+
color: #6b7280;
|
|
2575
|
+
flex-shrink: 0;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
.ai-attach-chip-name {
|
|
2579
|
+
flex: 1;
|
|
2580
|
+
overflow: hidden;
|
|
2581
|
+
text-overflow: ellipsis;
|
|
2582
|
+
white-space: nowrap;
|
|
2583
|
+
min-width: 0;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
.ai-attach-chip-remove {
|
|
2587
|
+
border: none;
|
|
2588
|
+
background: none;
|
|
2589
|
+
color: #9ca3af;
|
|
2590
|
+
cursor: pointer;
|
|
2591
|
+
padding: 0 1px;
|
|
2592
|
+
font-size: 16px;
|
|
2593
|
+
line-height: 1;
|
|
2594
|
+
flex-shrink: 0;
|
|
2595
|
+
display: flex;
|
|
2596
|
+
align-items: center;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
.ai-attach-chip-remove:hover {
|
|
2600
|
+
color: #ef4444;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/* ── Input row (attach btn + textarea + send btn) ── */
|
|
2604
|
+
|
|
2605
|
+
.ai-chat-input-row {
|
|
2606
|
+
display: flex;
|
|
2607
|
+
align-items: center;
|
|
2608
|
+
gap: 8px;
|
|
2609
|
+
padding: 8px 8px 8px 10px;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
.ai-attach-file-input {
|
|
2613
|
+
display: none;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.ai-attach-btn {
|
|
2617
|
+
display: flex;
|
|
2618
|
+
align-items: center;
|
|
2619
|
+
justify-content: center;
|
|
2620
|
+
width: 32px;
|
|
2621
|
+
height: 32px;
|
|
2622
|
+
border: none;
|
|
2623
|
+
border-radius: 8px;
|
|
2624
|
+
background: transparent;
|
|
2625
|
+
color: #9ca3af;
|
|
2626
|
+
cursor: pointer;
|
|
2627
|
+
flex-shrink: 0;
|
|
2628
|
+
padding: 0;
|
|
2629
|
+
transition: color 0.15s, background 0.15s;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
.ai-attach-btn:hover:not(:disabled) {
|
|
2633
|
+
color: #4b5563;
|
|
2634
|
+
background: #f3f4f6;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
.ai-attach-btn:disabled {
|
|
2638
|
+
opacity: 0.4;
|
|
2639
|
+
cursor: not-allowed;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2372
2642
|
.ai-chat-input-wrapper:focus-within {
|
|
2373
2643
|
border-color: #1eaaf1;
|
|
2374
2644
|
box-shadow: 0 0 0 3px rgba(30, 170, 241, 0.1);
|
|
@@ -2437,6 +2707,15 @@
|
|
|
2437
2707
|
cursor: not-allowed;
|
|
2438
2708
|
}
|
|
2439
2709
|
|
|
2710
|
+
.ai-chat-stop-btn {
|
|
2711
|
+
background: #ef4444;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
.ai-chat-stop-btn:hover {
|
|
2715
|
+
background: #dc2626;
|
|
2716
|
+
transform: scale(1.05);
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2440
2719
|
.ai-chat-disclaimer {
|
|
2441
2720
|
font-size: 11px;
|
|
2442
2721
|
color: #9ca3af;
|
|
@@ -2517,6 +2796,20 @@
|
|
|
2517
2796
|
color: #1f2937 !important;
|
|
2518
2797
|
box-shadow: none !important;
|
|
2519
2798
|
}
|
|
2799
|
+
|
|
2800
|
+
body.ai-chat-printing #ai-cortex-print-section .ai-stat-card {
|
|
2801
|
+
padding: 8px 10px !important;
|
|
2802
|
+
min-width: 80px !important;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
body.ai-chat-printing #ai-cortex-print-section .ai-stat-value {
|
|
2806
|
+
font-size: 17px !important;
|
|
2807
|
+
line-height: 1.2 !important;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
body.ai-chat-printing #ai-cortex-print-section .ai-stat-label {
|
|
2811
|
+
font-size: 9px !important;
|
|
2812
|
+
}
|
|
2520
2813
|
}
|
|
2521
2814
|
|
|
2522
2815
|
/* ── Responsive ── */
|
package/package.json
CHANGED
|
@@ -1,26 +1,127 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from "react";
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const ACCEPTED_TYPES = 'image/jpeg,image/png,image/gif,image/webp,application/pdf,text/plain,text/csv,text/markdown';
|
|
4
|
+
const IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
|
|
5
|
+
const MAX_NON_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
const MAX_IMAGE_DIMENSION = 1568;
|
|
7
|
+
let _attachId = 0;
|
|
8
|
+
|
|
9
|
+
// Resize image to fit within MAX_IMAGE_DIMENSION on its longest side.
|
|
10
|
+
// Images already within the limit are returned unchanged.
|
|
11
|
+
// Oversized images are resampled and re-encoded as JPEG.
|
|
12
|
+
function resizeImageIfNeeded(dataUrl, origMediaType) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const img = new Image();
|
|
15
|
+
img.onload = () => {
|
|
16
|
+
const { naturalWidth: w, naturalHeight: h } = img;
|
|
17
|
+
if (Math.max(w, h) <= MAX_IMAGE_DIMENSION) {
|
|
18
|
+
resolve({ dataUrl, mediaType: origMediaType });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const scale = MAX_IMAGE_DIMENSION / Math.max(w, h);
|
|
22
|
+
const nw = Math.max(1, Math.round(w * scale));
|
|
23
|
+
const nh = Math.max(1, Math.round(h * scale));
|
|
24
|
+
const canvas = document.createElement('canvas');
|
|
25
|
+
canvas.width = nw;
|
|
26
|
+
canvas.height = nh;
|
|
27
|
+
const ctx = canvas.getContext('2d');
|
|
28
|
+
ctx.fillStyle = '#ffffff';
|
|
29
|
+
ctx.fillRect(0, 0, nw, nh);
|
|
30
|
+
ctx.drawImage(img, 0, 0, nw, nh);
|
|
31
|
+
resolve({ dataUrl: canvas.toDataURL('image/jpeg', 0.85), mediaType: 'image/jpeg' });
|
|
32
|
+
};
|
|
33
|
+
img.onerror = () => resolve({ dataUrl, mediaType: origMediaType });
|
|
34
|
+
img.src = dataUrl;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function FileIcon() {
|
|
39
|
+
return (
|
|
40
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
41
|
+
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
|
42
|
+
<polyline points="13 2 13 9 20 9" />
|
|
43
|
+
</svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function ChatInput({ onSend, onStop, loading = false, disabled, disabledReason = "", onDisabledClick }) {
|
|
4
48
|
const [text, setText] = useState("");
|
|
49
|
+
const [attachments, setAttachments] = useState([]);
|
|
5
50
|
const textareaRef = useRef(null);
|
|
51
|
+
const fileInputRef = useRef(null);
|
|
6
52
|
const showDisabledReason = Boolean(disabledReason);
|
|
53
|
+
const canSend = !disabled && (text.trim().length > 0 || attachments.length > 0);
|
|
7
54
|
|
|
8
55
|
useEffect(() => {
|
|
9
|
-
if (!disabled && textareaRef.current)
|
|
10
|
-
textareaRef.current.focus();
|
|
11
|
-
}
|
|
56
|
+
if (!disabled && textareaRef.current) textareaRef.current.focus();
|
|
12
57
|
}, [disabled]);
|
|
13
58
|
|
|
59
|
+
const readFile = useCallback((file) => new Promise((resolve, reject) => {
|
|
60
|
+
if (!file) { reject(new Error('No file')); return; }
|
|
61
|
+
const isImage = IMAGE_TYPES.has(file.type);
|
|
62
|
+
if (!isImage && file.size > MAX_NON_IMAGE_BYTES) {
|
|
63
|
+
reject(new Error(`${file.name || 'File'} exceeds 5 MB`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const reader = new FileReader();
|
|
67
|
+
reader.onload = async (e) => {
|
|
68
|
+
try {
|
|
69
|
+
const rawDataUrl = e.target.result;
|
|
70
|
+
let dataUrl = rawDataUrl;
|
|
71
|
+
let mediaType = file.type || 'application/octet-stream';
|
|
72
|
+
if (isImage) {
|
|
73
|
+
const resized = await resizeImageIfNeeded(rawDataUrl, file.type);
|
|
74
|
+
dataUrl = resized.dataUrl;
|
|
75
|
+
mediaType = resized.mediaType;
|
|
76
|
+
}
|
|
77
|
+
resolve({
|
|
78
|
+
id: `att-${++_attachId}`,
|
|
79
|
+
name: file.name || (isImage ? 'pasted-image.png' : 'attachment'),
|
|
80
|
+
mediaType,
|
|
81
|
+
data: dataUrl.split(',')[1],
|
|
82
|
+
preview: isImage ? dataUrl : null,
|
|
83
|
+
size: file.size,
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
reject(err);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
reader.onerror = reject;
|
|
90
|
+
reader.readAsDataURL(file);
|
|
91
|
+
}), []);
|
|
92
|
+
|
|
93
|
+
const addFiles = useCallback(async (fileList) => {
|
|
94
|
+
const files = Array.from(fileList).filter(Boolean);
|
|
95
|
+
if (!files.length) return;
|
|
96
|
+
const results = await Promise.allSettled(files.map(readFile));
|
|
97
|
+
const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
|
98
|
+
if (ok.length) setAttachments(prev => [...prev, ...ok]);
|
|
99
|
+
}, [readFile]);
|
|
100
|
+
|
|
101
|
+
const handleFileChange = (e) => {
|
|
102
|
+
if (e.target.files?.length) addFiles(e.target.files);
|
|
103
|
+
e.target.value = '';
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handlePaste = useCallback((e) => {
|
|
107
|
+
const items = Array.from(e.clipboardData?.items ?? []);
|
|
108
|
+
const fileItems = items.filter(it => it.kind === 'file');
|
|
109
|
+
if (!fileItems.length) return;
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
addFiles(fileItems.map(it => it.getAsFile()));
|
|
112
|
+
}, [addFiles]);
|
|
113
|
+
|
|
114
|
+
const removeAttachment = useCallback((id) => {
|
|
115
|
+
setAttachments(prev => prev.filter(a => a.id !== id));
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
14
118
|
const handleSubmit = (e) => {
|
|
15
119
|
e.preventDefault();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
textareaRef.current.style.height = "auto";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
120
|
+
if (!canSend) return;
|
|
121
|
+
onSend(text.trim(), attachments.length ? attachments : undefined);
|
|
122
|
+
setText("");
|
|
123
|
+
setAttachments([]);
|
|
124
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
24
125
|
};
|
|
25
126
|
|
|
26
127
|
const handleKeyDown = (e) => {
|
|
@@ -31,17 +132,14 @@ export default function ChatInput({ onSend, disabled, disabledReason = "", onDis
|
|
|
31
132
|
};
|
|
32
133
|
|
|
33
134
|
const handleInput = (e) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
setText(
|
|
135
|
+
const ta = e.target;
|
|
136
|
+
ta.style.height = "auto";
|
|
137
|
+
ta.style.height = Math.min(ta.scrollHeight, 150) + "px";
|
|
138
|
+
setText(ta.value);
|
|
38
139
|
};
|
|
39
140
|
|
|
40
141
|
const handleDisabledInteraction = (e) => {
|
|
41
|
-
if (!disabled || !onDisabledClick)
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
142
|
+
if (!disabled || !onDisabledClick) return;
|
|
45
143
|
e.preventDefault();
|
|
46
144
|
onDisabledClick();
|
|
47
145
|
};
|
|
@@ -49,7 +147,7 @@ export default function ChatInput({ onSend, disabled, disabledReason = "", onDis
|
|
|
49
147
|
return (
|
|
50
148
|
<form className="ai-chat-input-form" onSubmit={handleSubmit}>
|
|
51
149
|
<div
|
|
52
|
-
className={`ai-chat-input-wrapper
|
|
150
|
+
className={`ai-chat-input-wrapper${showDisabledReason ? " disabled-with-reason" : ""}`}
|
|
53
151
|
onMouseDown={handleDisabledInteraction}
|
|
54
152
|
>
|
|
55
153
|
{showDisabledReason && (
|
|
@@ -61,27 +159,85 @@ export default function ChatInput({ onSend, disabled, disabledReason = "", onDis
|
|
|
61
159
|
aria-label={disabledReason}
|
|
62
160
|
/>
|
|
63
161
|
)}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
162
|
+
{attachments.length > 0 && (
|
|
163
|
+
<div className="ai-attach-strip">
|
|
164
|
+
{attachments.map((att) => (
|
|
165
|
+
<div key={att.id} className={`ai-attach-chip${att.preview ? " ai-attach-chip--img" : ""}`}>
|
|
166
|
+
{att.preview
|
|
167
|
+
? <img src={att.preview} className="ai-attach-thumb" alt={att.name} />
|
|
168
|
+
: <span className="ai-attach-chip-icon"><FileIcon /></span>
|
|
169
|
+
}
|
|
170
|
+
<span className="ai-attach-chip-name">{att.name}</span>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className="ai-attach-chip-remove"
|
|
174
|
+
onClick={() => removeAttachment(att.id)}
|
|
175
|
+
aria-label={`Remove ${att.name}`}
|
|
176
|
+
>×</button>
|
|
177
|
+
</div>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
<div className="ai-chat-input-row">
|
|
182
|
+
<input
|
|
183
|
+
ref={fileInputRef}
|
|
184
|
+
type="file"
|
|
185
|
+
accept={ACCEPTED_TYPES}
|
|
186
|
+
multiple
|
|
187
|
+
className="ai-attach-file-input"
|
|
188
|
+
onChange={handleFileChange}
|
|
189
|
+
tabIndex={-1}
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
/>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
className="ai-attach-btn"
|
|
195
|
+
onClick={() => fileInputRef.current?.click()}
|
|
196
|
+
disabled={disabled}
|
|
197
|
+
title="Attach file or image"
|
|
198
|
+
aria-label="Attach file or image"
|
|
199
|
+
>
|
|
200
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
201
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
|
202
|
+
</svg>
|
|
203
|
+
</button>
|
|
204
|
+
<textarea
|
|
205
|
+
ref={textareaRef}
|
|
206
|
+
className="ai-chat-input"
|
|
207
|
+
value={text}
|
|
208
|
+
onChange={handleInput}
|
|
209
|
+
onKeyDown={handleKeyDown}
|
|
210
|
+
onPaste={handlePaste}
|
|
211
|
+
placeholder={showDisabledReason ? "Select a client to start chatting..." : "Type your question..."}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
rows={1}
|
|
214
|
+
/>
|
|
215
|
+
{loading ? (
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
className="ai-chat-send-btn ai-chat-stop-btn"
|
|
219
|
+
onClick={onStop}
|
|
220
|
+
title="Stop generating"
|
|
221
|
+
aria-label="Stop generating"
|
|
222
|
+
>
|
|
223
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
224
|
+
<rect x="2" y="2" width="12" height="12" rx="2" />
|
|
225
|
+
</svg>
|
|
226
|
+
</button>
|
|
227
|
+
) : (
|
|
228
|
+
<button
|
|
229
|
+
type="submit"
|
|
230
|
+
className="ai-chat-send-btn"
|
|
231
|
+
disabled={!canSend}
|
|
232
|
+
title="Send message"
|
|
233
|
+
>
|
|
234
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
235
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
236
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
237
|
+
</svg>
|
|
238
|
+
</button>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
85
241
|
</div>
|
|
86
242
|
<p className="ai-chat-disclaimer">
|
|
87
243
|
Responses are generated by AI and may contain errors. Double-check anything important. Data in the app is
|