@extrachill/chat 0.3.1 → 0.5.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/CHANGELOG.md +31 -0
- package/css/chat.css +189 -2
- package/dist/Chat.d.ts +5 -1
- package/dist/Chat.d.ts.map +1 -1
- package/dist/Chat.js +2 -2
- package/dist/api.d.ts +28 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -2
- package/dist/components/ChatInput.d.ts +11 -4
- package/dist/components/ChatInput.d.ts.map +1 -1
- package/dist/components/ChatInput.js +68 -10
- package/dist/components/ChatMessage.d.ts +1 -0
- package/dist/components/ChatMessage.d.ts.map +1 -1
- package/dist/components/ChatMessage.js +23 -3
- package/dist/hooks/useChat.d.ts +28 -4
- package/dist/hooks/useChat.d.ts.map +1 -1
- package/dist/hooks/useChat.js +70 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/normalizer.d.ts.map +1 -1
- package/dist/normalizer.js +84 -1
- package/dist/types/api.d.ts +25 -1
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/message.d.ts +23 -0
- package/dist/types/message.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Chat.tsx +8 -0
- package/src/api.ts +39 -0
- package/src/components/ChatInput.tsx +173 -28
- package/src/components/ChatMessage.tsx +102 -5
- package/src/hooks/useChat.ts +120 -10
- package/src/index.ts +3 -1
- package/src/normalizer.ts +88 -3
- package/src/types/api.ts +26 -1
- package/src/types/index.ts +2 -0
- package/src/types/message.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- v0.4.0 — metadata, onToolCalls, processingSessionId, request dedup
|
|
7
|
+
- media support — attachments in messages, image/video rendering, file input
|
|
8
|
+
- swap to react-markdown for proper rich content rendering
|
|
9
|
+
- built-in markdown rendering for chat messages
|
|
10
|
+
- remove adapter pattern, speak chat REST API natively
|
|
11
|
+
- implement adapter contract, message model, and component library
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Initial commit
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- scroll within chat container instead of hijacking page scroll
|
|
18
|
+
- extract readable error message from @wordpress/api-fetch error objects
|
|
19
|
+
- use npm run build in prepublishOnly (pnpm not available on server)
|
|
20
|
+
|
|
21
|
+
## 0.4.0
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `onToolCalls` callback on `useChat` — fires after each turn when tool calls are present, enabling consumers to react to tool executions (apply diffs, invalidate caches, update external state)
|
|
25
|
+
- `metadata` option on `useChat` — arbitrary key-value pairs forwarded to the backend with each message for context scoping (e.g. `{ post_id, context: 'editor' }` or `{ selected_pipeline_id }`)
|
|
26
|
+
- `sessionContext` option on `useChat` — filters session listing to only show sessions created in a specific context
|
|
27
|
+
- `processingSessionId` in `UseChatReturn` — tracks which session initiated the current request, preventing stale loading indicators when switching sessions mid-request
|
|
28
|
+
- `context` parameter on `listSessions` API function — optional context filter for session listing
|
|
29
|
+
- `metadata` parameter on `sendMessage` API function — forwarded to backend alongside the message
|
|
30
|
+
- `X-Request-ID` header on send requests — automatic request deduplication via `crypto.randomUUID()` with fallback
|
|
31
|
+
- `headers` field on `FetchOptions` — allows passing custom HTTP headers through the fetch function
|
|
32
|
+
- Session creation guard — prevents concurrent session creation with `isCreatingRef`
|
|
33
|
+
|
|
3
34
|
## 0.2.0
|
|
4
35
|
|
|
5
36
|
- **BREAKING:** Remove `ChatAdapter` interface and adapter pattern
|
package/css/chat.css
CHANGED
|
@@ -249,12 +249,17 @@
|
|
|
249
249
|
============================================ */
|
|
250
250
|
|
|
251
251
|
.ec-chat-input {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
border-top: 1px solid var(--ec-chat-border);
|
|
255
|
+
background: var(--ec-chat-input-bg);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.ec-chat-input__row {
|
|
252
259
|
display: flex;
|
|
253
260
|
align-items: flex-end;
|
|
254
261
|
gap: 8px;
|
|
255
262
|
padding: var(--ec-chat-padding);
|
|
256
|
-
border-top: 1px solid var(--ec-chat-border);
|
|
257
|
-
background: var(--ec-chat-input-bg);
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
.ec-chat-input__textarea {
|
|
@@ -633,3 +638,185 @@
|
|
|
633
638
|
background: var(--ec-chat-error-bg);
|
|
634
639
|
color: var(--ec-chat-error-text);
|
|
635
640
|
}
|
|
641
|
+
|
|
642
|
+
/* ============================================
|
|
643
|
+
Message Attachments
|
|
644
|
+
============================================ */
|
|
645
|
+
|
|
646
|
+
.ec-chat-message__attachments {
|
|
647
|
+
margin-top: 8px;
|
|
648
|
+
display: flex;
|
|
649
|
+
flex-direction: column;
|
|
650
|
+
gap: 8px;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.ec-chat-message__images {
|
|
654
|
+
display: flex;
|
|
655
|
+
flex-wrap: wrap;
|
|
656
|
+
gap: 6px;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.ec-chat-message__image-link {
|
|
660
|
+
display: block;
|
|
661
|
+
border-radius: 8px;
|
|
662
|
+
overflow: hidden;
|
|
663
|
+
line-height: 0;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.ec-chat-message__image {
|
|
667
|
+
max-width: 280px;
|
|
668
|
+
max-height: 280px;
|
|
669
|
+
border-radius: 8px;
|
|
670
|
+
object-fit: cover;
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
transition: opacity 0.15s;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.ec-chat-message__image:hover {
|
|
676
|
+
opacity: 0.9;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.ec-chat-message__video {
|
|
680
|
+
max-width: 100%;
|
|
681
|
+
border-radius: 8px;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.ec-chat-message__file {
|
|
685
|
+
display: inline-flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
gap: 6px;
|
|
688
|
+
padding: 8px 12px;
|
|
689
|
+
border: 1px solid var(--ec-chat-border);
|
|
690
|
+
border-radius: 8px;
|
|
691
|
+
text-decoration: none;
|
|
692
|
+
color: var(--ec-chat-text);
|
|
693
|
+
font-size: 13px;
|
|
694
|
+
transition: background 0.15s;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.ec-chat-message__file:hover {
|
|
698
|
+
background: var(--ec-chat-assistant-bg);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.ec-chat-message__file-icon {
|
|
702
|
+
flex-shrink: 0;
|
|
703
|
+
color: var(--ec-chat-text-muted);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.ec-chat-message__file-name {
|
|
707
|
+
overflow: hidden;
|
|
708
|
+
text-overflow: ellipsis;
|
|
709
|
+
white-space: nowrap;
|
|
710
|
+
max-width: 200px;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.ec-chat-message__file-size {
|
|
714
|
+
color: var(--ec-chat-text-muted);
|
|
715
|
+
white-space: nowrap;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* ============================================
|
|
719
|
+
Input Attachments & Drag-Drop
|
|
720
|
+
============================================ */
|
|
721
|
+
|
|
722
|
+
.ec-chat-input--dragging {
|
|
723
|
+
outline: 2px dashed var(--ec-chat-input-focus-border);
|
|
724
|
+
outline-offset: -2px;
|
|
725
|
+
border-radius: var(--ec-chat-border-radius);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.ec-chat-input__attachments {
|
|
729
|
+
display: flex;
|
|
730
|
+
flex-wrap: wrap;
|
|
731
|
+
gap: 6px;
|
|
732
|
+
padding: 8px var(--ec-chat-padding) 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.ec-chat-input__row {
|
|
736
|
+
display: flex;
|
|
737
|
+
align-items: flex-end;
|
|
738
|
+
gap: 4px;
|
|
739
|
+
padding: 0 var(--ec-chat-padding) var(--ec-chat-padding);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.ec-chat-input__file-input {
|
|
743
|
+
position: absolute;
|
|
744
|
+
width: 1px;
|
|
745
|
+
height: 1px;
|
|
746
|
+
overflow: hidden;
|
|
747
|
+
clip: rect(0, 0, 0, 0);
|
|
748
|
+
border: 0;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.ec-chat-input__attach {
|
|
752
|
+
display: flex;
|
|
753
|
+
align-items: center;
|
|
754
|
+
justify-content: center;
|
|
755
|
+
width: 36px;
|
|
756
|
+
height: 36px;
|
|
757
|
+
flex-shrink: 0;
|
|
758
|
+
border: none;
|
|
759
|
+
background: none;
|
|
760
|
+
color: var(--ec-chat-text-muted);
|
|
761
|
+
cursor: pointer;
|
|
762
|
+
border-radius: 8px;
|
|
763
|
+
transition: color 0.15s, background 0.15s;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.ec-chat-input__attach:hover {
|
|
767
|
+
color: var(--ec-chat-text);
|
|
768
|
+
background: var(--ec-chat-assistant-bg);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.ec-chat-input__attach:disabled {
|
|
772
|
+
opacity: var(--ec-chat-send-disabled-opacity);
|
|
773
|
+
cursor: not-allowed;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.ec-chat-input__attach-icon {
|
|
777
|
+
display: block;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.ec-chat-input__preview {
|
|
781
|
+
position: relative;
|
|
782
|
+
display: inline-flex;
|
|
783
|
+
align-items: center;
|
|
784
|
+
gap: 4px;
|
|
785
|
+
padding: 4px 8px;
|
|
786
|
+
background: var(--ec-chat-assistant-bg);
|
|
787
|
+
border-radius: 6px;
|
|
788
|
+
font-size: 12px;
|
|
789
|
+
color: var(--ec-chat-text);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.ec-chat-input__preview-image {
|
|
793
|
+
width: 48px;
|
|
794
|
+
height: 48px;
|
|
795
|
+
border-radius: 4px;
|
|
796
|
+
object-fit: cover;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.ec-chat-input__preview-name {
|
|
800
|
+
max-width: 120px;
|
|
801
|
+
overflow: hidden;
|
|
802
|
+
text-overflow: ellipsis;
|
|
803
|
+
white-space: nowrap;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.ec-chat-input__preview-remove {
|
|
807
|
+
display: flex;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: center;
|
|
810
|
+
width: 18px;
|
|
811
|
+
height: 18px;
|
|
812
|
+
border: none;
|
|
813
|
+
background: var(--ec-chat-text-muted);
|
|
814
|
+
color: #fff;
|
|
815
|
+
border-radius: 50%;
|
|
816
|
+
font-size: 12px;
|
|
817
|
+
line-height: 1;
|
|
818
|
+
cursor: pointer;
|
|
819
|
+
position: absolute;
|
|
820
|
+
top: -4px;
|
|
821
|
+
right: -4px;
|
|
822
|
+
}
|
package/dist/Chat.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export interface ChatProps {
|
|
|
45
45
|
showSessions?: boolean;
|
|
46
46
|
/** Label shown during multi-turn processing. */
|
|
47
47
|
processingLabel?: (turnCount: number) => string;
|
|
48
|
+
/** Whether to show the attachment button in the input. Defaults to true. */
|
|
49
|
+
allowAttachments?: boolean;
|
|
50
|
+
/** Accepted file types for attachments. Defaults to 'image/*,video/*'. */
|
|
51
|
+
acceptFileTypes?: string;
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
54
|
* Ready-to-use chat component.
|
|
@@ -69,5 +73,5 @@ export interface ChatProps {
|
|
|
69
73
|
* }
|
|
70
74
|
* ```
|
|
71
75
|
*/
|
|
72
|
-
export declare function Chat({ basePath, fetchFn, agentId, contentFormat, renderContent, showTools, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions, processingLabel, }: ChatProps): import("react/jsx-runtime").JSX.Element;
|
|
76
|
+
export declare function Chat({ basePath, fetchFn, agentId, contentFormat, renderContent, showTools, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions, processingLabel, allowAttachments, acceptFileTypes, }: ChatProps): import("react/jsx-runtime").JSX.Element;
|
|
73
77
|
//# sourceMappingURL=Chat.d.ts.map
|
package/dist/Chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../src/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAQlE,MAAM,WAAW,SAAS;IACzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IAC9E,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,eAAe,EAAE,CAAC;IACpC,0BAA0B;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,OAAO,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACpC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;IACxC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../src/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAQlE,MAAM,WAAW,SAAS;IACzB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IAC9E,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,eAAe,EAAE,CAAC;IACpC,0BAA0B;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kCAAkC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,OAAO,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACpC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;IACxC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,IAAI,CAAC,EACpB,QAAQ,EACR,OAAO,EACP,OAAO,EACP,aAA0B,EAC1B,aAAa,EACb,SAAgB,EAChB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,SAAS,EACT,YAAmB,EACnB,eAAe,EACf,gBAAuB,EACvB,eAAe,GACf,EAAE,SAAS,2CA6DX"}
|
package/dist/Chat.js
CHANGED
|
@@ -29,7 +29,7 @@ import { SessionSwitcher } from "./components/SessionSwitcher.js";
|
|
|
29
29
|
* }
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
-
export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', renderContent, showTools = true, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions = true, processingLabel, }) {
|
|
32
|
+
export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', renderContent, showTools = true, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions = true, processingLabel, allowAttachments = true, acceptFileTypes, }) {
|
|
33
33
|
const chat = useChat({
|
|
34
34
|
basePath,
|
|
35
35
|
fetchFn,
|
|
@@ -46,5 +46,5 @@ export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', r
|
|
|
46
46
|
? (processingLabel
|
|
47
47
|
? processingLabel(chat.turnCount)
|
|
48
48
|
: `Processing turn ${chat.turnCount}...`)
|
|
49
|
-
: undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder })] }) }) }));
|
|
49
|
+
: undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder, allowAttachments: allowAttachments, accept: acceptFileTypes })] }) }) }));
|
|
50
50
|
}
|
package/dist/api.d.ts
CHANGED
|
@@ -21,7 +21,12 @@ import type { ChatSession } from './types/session.ts';
|
|
|
21
21
|
export interface FetchOptions {
|
|
22
22
|
path: string;
|
|
23
23
|
method?: string;
|
|
24
|
+
/** JSON body (mutually exclusive with formData). */
|
|
24
25
|
data?: Record<string, unknown>;
|
|
26
|
+
/** FormData body for file uploads (mutually exclusive with data). */
|
|
27
|
+
formData?: FormData;
|
|
28
|
+
/** Additional HTTP headers. */
|
|
29
|
+
headers?: Record<string, string>;
|
|
25
30
|
}
|
|
26
31
|
export type FetchFn = (options: FetchOptions) => Promise<unknown>;
|
|
27
32
|
export interface ChatApiConfig {
|
|
@@ -45,18 +50,39 @@ export interface ContinueResult {
|
|
|
45
50
|
turnNumber: number;
|
|
46
51
|
maxTurnsReached: boolean;
|
|
47
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Attachment metadata to send with a message.
|
|
55
|
+
*/
|
|
56
|
+
export interface SendAttachment {
|
|
57
|
+
url?: string;
|
|
58
|
+
media_id?: number;
|
|
59
|
+
mime_type?: string;
|
|
60
|
+
filename?: string;
|
|
61
|
+
}
|
|
48
62
|
/**
|
|
49
63
|
* Send a user message (create or continue a session).
|
|
64
|
+
*
|
|
65
|
+
* When attachments are provided, they are included in the JSON body
|
|
66
|
+
* as structured metadata (not as file uploads — files should already
|
|
67
|
+
* be in the WordPress media library or accessible by URL).
|
|
68
|
+
*
|
|
69
|
+
* @param metadata - Arbitrary key-value pairs forwarded to the backend
|
|
70
|
+
* alongside the message (e.g. `{ selected_pipeline_id: 42 }` or
|
|
71
|
+
* `{ post_id: 100, context: 'editor' }`). The backend can use these
|
|
72
|
+
* to scope the AI's behavior. Not persisted as message content.
|
|
50
73
|
*/
|
|
51
|
-
export declare function sendMessage(config: ChatApiConfig, content: string, sessionId?: string): Promise<SendResult>;
|
|
74
|
+
export declare function sendMessage(config: ChatApiConfig, content: string, sessionId?: string, attachments?: SendAttachment[], metadata?: Record<string, unknown>): Promise<SendResult>;
|
|
52
75
|
/**
|
|
53
76
|
* Continue a multi-turn response.
|
|
54
77
|
*/
|
|
55
78
|
export declare function continueResponse(config: ChatApiConfig, sessionId: string): Promise<ContinueResult>;
|
|
56
79
|
/**
|
|
57
80
|
* List sessions for the current user.
|
|
81
|
+
*
|
|
82
|
+
* @param context - Optional context filter (e.g. 'chat', 'editor', 'pipeline').
|
|
83
|
+
* Only sessions created in the matching context are returned.
|
|
58
84
|
*/
|
|
59
|
-
export declare function listSessions(config: ChatApiConfig, limit?: number): Promise<ChatSession[]>;
|
|
85
|
+
export declare function listSessions(config: ChatApiConfig, limit?: number, context?: string): Promise<ChatSession[]>;
|
|
60
86
|
/**
|
|
61
87
|
* Load a single session's conversation.
|
|
62
88
|
*/
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUtD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUtD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAElE,MAAM,WAAW,aAAa;IAC7B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,cAAc,EAAE,EAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,aAAa,EACrB,KAAK,SAAK,EACV,OAAO,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,EAAE,CAAC,CAcxB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,EAAE,CAAC,CAUxB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAClC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CASf"}
|
package/dist/api.js
CHANGED
|
@@ -10,17 +10,35 @@
|
|
|
10
10
|
import { normalizeConversation, normalizeMessage, normalizeSession } from "./normalizer.js";
|
|
11
11
|
/**
|
|
12
12
|
* Send a user message (create or continue a session).
|
|
13
|
+
*
|
|
14
|
+
* When attachments are provided, they are included in the JSON body
|
|
15
|
+
* as structured metadata (not as file uploads — files should already
|
|
16
|
+
* be in the WordPress media library or accessible by URL).
|
|
17
|
+
*
|
|
18
|
+
* @param metadata - Arbitrary key-value pairs forwarded to the backend
|
|
19
|
+
* alongside the message (e.g. `{ selected_pipeline_id: 42 }` or
|
|
20
|
+
* `{ post_id: 100, context: 'editor' }`). The backend can use these
|
|
21
|
+
* to scope the AI's behavior. Not persisted as message content.
|
|
13
22
|
*/
|
|
14
|
-
export async function sendMessage(config, content, sessionId) {
|
|
23
|
+
export async function sendMessage(config, content, sessionId, attachments, metadata) {
|
|
15
24
|
const body = { message: content };
|
|
16
25
|
if (sessionId)
|
|
17
26
|
body.session_id = sessionId;
|
|
18
27
|
if (config.agentId)
|
|
19
28
|
body.agent_id = config.agentId;
|
|
29
|
+
if (attachments?.length)
|
|
30
|
+
body.attachments = attachments;
|
|
31
|
+
if (metadata)
|
|
32
|
+
Object.assign(body, metadata);
|
|
33
|
+
// Generate a unique request ID for idempotent request handling.
|
|
34
|
+
const requestId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
35
|
+
? crypto.randomUUID()
|
|
36
|
+
: `req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
20
37
|
const raw = await config.fetchFn({
|
|
21
38
|
path: config.basePath,
|
|
22
39
|
method: 'POST',
|
|
23
40
|
data: body,
|
|
41
|
+
headers: { 'X-Request-ID': requestId },
|
|
24
42
|
});
|
|
25
43
|
if (!raw.success) {
|
|
26
44
|
throw new Error(raw.message ?? 'Failed to send message');
|
|
@@ -54,11 +72,16 @@ export async function continueResponse(config, sessionId) {
|
|
|
54
72
|
}
|
|
55
73
|
/**
|
|
56
74
|
* List sessions for the current user.
|
|
75
|
+
*
|
|
76
|
+
* @param context - Optional context filter (e.g. 'chat', 'editor', 'pipeline').
|
|
77
|
+
* Only sessions created in the matching context are returned.
|
|
57
78
|
*/
|
|
58
|
-
export async function listSessions(config, limit = 20) {
|
|
79
|
+
export async function listSessions(config, limit = 20, context) {
|
|
59
80
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
60
81
|
if (config.agentId)
|
|
61
82
|
params.set('agent_id', String(config.agentId));
|
|
83
|
+
if (context)
|
|
84
|
+
params.set('context', context);
|
|
62
85
|
const raw = await config.fetchFn({
|
|
63
86
|
path: `${config.basePath}/sessions?${params.toString()}`,
|
|
64
87
|
});
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
export interface ChatInputProps {
|
|
2
|
-
/** Called when the user submits a message. */
|
|
3
|
-
onSend: (content: string) => void;
|
|
2
|
+
/** Called when the user submits a message (with optional file attachments). */
|
|
3
|
+
onSend: (content: string, files?: File[]) => void;
|
|
4
4
|
/** Whether input is disabled (e.g. while waiting for response). */
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
/** Placeholder text. Defaults to 'Type a message...'. */
|
|
7
7
|
placeholder?: string;
|
|
8
8
|
/** Maximum number of rows the textarea auto-grows to. Defaults to 6. */
|
|
9
9
|
maxRows?: number;
|
|
10
|
+
/** Accepted file types for the file picker. Defaults to 'image/*,video/*'. */
|
|
11
|
+
accept?: string;
|
|
12
|
+
/** Maximum number of files per message. Defaults to 5. */
|
|
13
|
+
maxFiles?: number;
|
|
14
|
+
/** Whether to show the attachment button. Defaults to true. */
|
|
15
|
+
allowAttachments?: boolean;
|
|
10
16
|
/** Additional CSS class name. */
|
|
11
17
|
className?: string;
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
|
-
* Chat input with auto-growing textarea and
|
|
20
|
+
* Chat input with auto-growing textarea, keyboard shortcuts, and file attachments.
|
|
15
21
|
*
|
|
16
22
|
* - Enter sends the message
|
|
17
23
|
* - Shift+Enter adds a newline
|
|
18
24
|
* - Textarea auto-grows up to `maxRows`
|
|
25
|
+
* - File attachment via button, drag-and-drop, or clipboard paste
|
|
19
26
|
*/
|
|
20
|
-
export declare function ChatInput({ onSend, disabled, placeholder, maxRows, className, }: ChatInputProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export declare function ChatInput({ onSend, disabled, placeholder, maxRows, accept, maxFiles, allowAttachments, className, }: ChatInputProps): import("react/jsx-runtime").JSX.Element;
|
|
21
28
|
//# sourceMappingURL=ChatInput.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatInput.d.ts","sourceRoot":"","sources":["../../src/components/ChatInput.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC9B
|
|
1
|
+
{"version":3,"file":"ChatInput.d.ts","sourceRoot":"","sources":["../../src/components/ChatInput.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC9B,+EAA+E;IAC/E,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IAClD,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACzB,MAAM,EACN,QAAgB,EAChB,WAAiC,EACjC,OAAW,EACX,MAA0B,EAC1B,QAAY,EACZ,gBAAuB,EACvB,SAAS,GACT,EAAE,cAAc,2CA4JhB"}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useCallback } from 'react';
|
|
3
3
|
/**
|
|
4
|
-
* Chat input with auto-growing textarea and
|
|
4
|
+
* Chat input with auto-growing textarea, keyboard shortcuts, and file attachments.
|
|
5
5
|
*
|
|
6
6
|
* - Enter sends the message
|
|
7
7
|
* - Shift+Enter adds a newline
|
|
8
8
|
* - Textarea auto-grows up to `maxRows`
|
|
9
|
+
* - File attachment via button, drag-and-drop, or clipboard paste
|
|
9
10
|
*/
|
|
10
|
-
export function ChatInput({ onSend, disabled = false, placeholder = 'Type a message...', maxRows = 6, className, }) {
|
|
11
|
+
export function ChatInput({ onSend, disabled = false, placeholder = 'Type a message...', maxRows = 6, accept = 'image/*,video/*', maxFiles = 5, allowAttachments = true, className, }) {
|
|
11
12
|
const [value, setValue] = useState('');
|
|
13
|
+
const [files, setFiles] = useState([]);
|
|
14
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
12
15
|
const textareaRef = useRef(null);
|
|
16
|
+
const fileInputRef = useRef(null);
|
|
13
17
|
const cooldownRef = useRef(false);
|
|
14
18
|
const resize = useCallback(() => {
|
|
15
19
|
const el = textareaRef.current;
|
|
@@ -20,33 +24,87 @@ export function ChatInput({ onSend, disabled = false, placeholder = 'Type a mess
|
|
|
20
24
|
const maxHeight = lineHeight * maxRows;
|
|
21
25
|
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
|
|
22
26
|
}, [maxRows]);
|
|
27
|
+
const addFiles = useCallback((newFiles) => {
|
|
28
|
+
const fileArray = Array.from(newFiles);
|
|
29
|
+
setFiles((prev) => {
|
|
30
|
+
const combined = [...prev, ...fileArray];
|
|
31
|
+
return combined.slice(0, maxFiles);
|
|
32
|
+
});
|
|
33
|
+
}, [maxFiles]);
|
|
34
|
+
const removeFile = useCallback((index) => {
|
|
35
|
+
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
36
|
+
}, []);
|
|
23
37
|
const handleSubmit = useCallback((e) => {
|
|
24
38
|
e?.preventDefault();
|
|
25
39
|
const trimmed = value.trim();
|
|
26
|
-
if (!trimmed || disabled || cooldownRef.current)
|
|
40
|
+
if ((!trimmed && files.length === 0) || disabled || cooldownRef.current)
|
|
27
41
|
return;
|
|
28
|
-
// Debounce to prevent double-submit
|
|
29
42
|
cooldownRef.current = true;
|
|
30
43
|
setTimeout(() => { cooldownRef.current = false; }, 300);
|
|
31
|
-
onSend(trimmed);
|
|
44
|
+
onSend(trimmed, files.length > 0 ? files : undefined);
|
|
32
45
|
setValue('');
|
|
33
|
-
|
|
46
|
+
setFiles([]);
|
|
34
47
|
requestAnimationFrame(() => {
|
|
35
48
|
const el = textareaRef.current;
|
|
36
49
|
if (el)
|
|
37
50
|
el.style.height = 'auto';
|
|
38
51
|
});
|
|
39
|
-
}, [value, disabled, onSend]);
|
|
52
|
+
}, [value, files, disabled, onSend]);
|
|
40
53
|
const handleKeyDown = useCallback((e) => {
|
|
41
54
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
42
55
|
e.preventDefault();
|
|
43
56
|
handleSubmit();
|
|
44
57
|
}
|
|
45
58
|
}, [handleSubmit]);
|
|
59
|
+
const handleDragOver = useCallback((e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
if (allowAttachments)
|
|
62
|
+
setIsDragging(true);
|
|
63
|
+
}, [allowAttachments]);
|
|
64
|
+
const handleDragLeave = useCallback((e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
setIsDragging(false);
|
|
67
|
+
}, []);
|
|
68
|
+
const handleDrop = useCallback((e) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
setIsDragging(false);
|
|
71
|
+
if (!allowAttachments || !e.dataTransfer.files.length)
|
|
72
|
+
return;
|
|
73
|
+
addFiles(e.dataTransfer.files);
|
|
74
|
+
}, [allowAttachments, addFiles]);
|
|
75
|
+
const handlePaste = useCallback((e) => {
|
|
76
|
+
if (!allowAttachments)
|
|
77
|
+
return;
|
|
78
|
+
const pastedFiles = Array.from(e.clipboardData.items)
|
|
79
|
+
.filter((item) => item.kind === 'file')
|
|
80
|
+
.map((item) => item.getAsFile())
|
|
81
|
+
.filter((f) => f !== null);
|
|
82
|
+
if (pastedFiles.length > 0) {
|
|
83
|
+
addFiles(pastedFiles);
|
|
84
|
+
}
|
|
85
|
+
}, [allowAttachments, addFiles]);
|
|
46
86
|
const baseClass = 'ec-chat-input';
|
|
47
|
-
const classes = [
|
|
48
|
-
|
|
87
|
+
const classes = [
|
|
88
|
+
baseClass,
|
|
89
|
+
isDragging ? `${baseClass}--dragging` : '',
|
|
90
|
+
className,
|
|
91
|
+
].filter(Boolean).join(' ');
|
|
92
|
+
return (_jsxs("form", { className: classes, onSubmit: handleSubmit, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [files.length > 0 && (_jsx("div", { className: `${baseClass}__attachments`, children: files.map((file, i) => (_jsx(FilePreview, { file: file, onRemove: () => removeFile(i) }, `${file.name}-${i}`))) })), _jsxs("div", { className: `${baseClass}__row`, children: [allowAttachments && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: true, className: `${baseClass}__file-input`, onChange: (e) => {
|
|
93
|
+
if (e.target.files)
|
|
94
|
+
addFiles(e.target.files);
|
|
95
|
+
e.target.value = '';
|
|
96
|
+
}, tabIndex: -1, "aria-hidden": "true" }), _jsx("button", { type: "button", className: `${baseClass}__attach`, onClick: () => fileInputRef.current?.click(), disabled: disabled, "aria-label": "Attach file", title: "Attach file", children: _jsx(AttachIcon, {}) })] })), _jsx("textarea", { ref: textareaRef, className: `${baseClass}__textarea`, value: value, onChange: (e) => { setValue(e.target.value); resize(); }, onKeyDown: handleKeyDown, onPaste: handlePaste, placeholder: placeholder, disabled: disabled, rows: 1, "aria-label": placeholder }), _jsx("button", { className: `${baseClass}__send`, type: "submit", disabled: disabled || (!value.trim() && files.length === 0), "aria-label": "Send message", children: _jsx(SendIcon, {}) })] })] }));
|
|
97
|
+
}
|
|
98
|
+
/* ---- File Preview ---- */
|
|
99
|
+
function FilePreview({ file, onRemove }) {
|
|
100
|
+
const isImage = file.type.startsWith('image/');
|
|
101
|
+
const preview = isImage ? URL.createObjectURL(file) : null;
|
|
102
|
+
return (_jsxs("div", { className: "ec-chat-input__preview", children: [preview ? (_jsx("img", { src: preview, alt: file.name, className: "ec-chat-input__preview-image" })) : (_jsx("span", { className: "ec-chat-input__preview-name", children: file.name })), _jsx("button", { type: "button", className: "ec-chat-input__preview-remove", onClick: onRemove, "aria-label": `Remove ${file.name}`, children: "\u00D7" })] }));
|
|
49
103
|
}
|
|
104
|
+
/* ---- Icons ---- */
|
|
50
105
|
function SendIcon() {
|
|
51
106
|
return (_jsxs("svg", { className: "ec-chat-input__send-icon", viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), _jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })] }));
|
|
52
107
|
}
|
|
108
|
+
function AttachIcon() {
|
|
109
|
+
return (_jsx("svg", { className: "ec-chat-input__attach-icon", viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("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" }) }));
|
|
110
|
+
}
|
|
@@ -17,6 +17,7 @@ export interface ChatMessageProps {
|
|
|
17
17
|
*
|
|
18
18
|
* User messages align right, assistant messages align left.
|
|
19
19
|
* Markdown content is rendered via react-markdown (lazy-loaded).
|
|
20
|
+
* Media attachments render inline below the text content.
|
|
20
21
|
*/
|
|
21
22
|
export declare function ChatMessage({ message, contentFormat, renderContent, className, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
|
|
22
23
|
//# sourceMappingURL=ChatMessage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatMessage.d.ts","sourceRoot":"","sources":["../../src/components/ChatMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,OAAO,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"ChatMessage.d.ts","sourceRoot":"","sources":["../../src/components/ChatMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,OAAO,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,aAAa,EAAmB,MAAM,mBAAmB,CAAC;AAKxG,MAAM,WAAW,gBAAgB;IAChC,6BAA6B;IAC7B,OAAO,EAAE,eAAe,CAAC;IACzB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;IAC9E,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAC3B,OAAO,EACP,aAA0B,EAC1B,aAAa,EACb,SAAS,GACT,EAAE,gBAAgB,2CAgClB"}
|
|
@@ -7,15 +7,18 @@ const ReactMarkdown = lazy(() => import('react-markdown'));
|
|
|
7
7
|
*
|
|
8
8
|
* User messages align right, assistant messages align left.
|
|
9
9
|
* Markdown content is rendered via react-markdown (lazy-loaded).
|
|
10
|
+
* Media attachments render inline below the text content.
|
|
10
11
|
*/
|
|
11
12
|
export function ChatMessage({ message, contentFormat = 'markdown', renderContent, className, }) {
|
|
12
13
|
const isUser = message.role === 'user';
|
|
13
14
|
const baseClass = 'ec-chat-message';
|
|
14
15
|
const roleClass = isUser ? `${baseClass}--user` : `${baseClass}--assistant`;
|
|
15
16
|
const classes = [baseClass, roleClass, className].filter(Boolean).join(' ');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const hasText = message.content.trim().length > 0;
|
|
18
|
+
const hasAttachments = message.attachments && message.attachments.length > 0;
|
|
19
|
+
return (_jsxs("div", { className: classes, "data-message-id": message.id, children: [_jsxs("div", { className: `${baseClass}__bubble`, children: [hasText && (renderContent
|
|
20
|
+
? renderContent(message.content, message.role)
|
|
21
|
+
: _jsx(DefaultContent, { content: message.content, format: contentFormat })), hasAttachments && (_jsx(MessageAttachments, { attachments: message.attachments }))] }), message.timestamp && (_jsx("time", { className: `${baseClass}__timestamp`, dateTime: message.timestamp, title: new Date(message.timestamp).toLocaleString(), children: formatTime(message.timestamp) }))] }));
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
24
|
* Markdown rendered via lazy-loaded react-markdown.
|
|
@@ -43,3 +46,20 @@ function formatTime(iso) {
|
|
|
43
46
|
return '';
|
|
44
47
|
}
|
|
45
48
|
}
|
|
49
|
+
/* ---- Media Attachments ---- */
|
|
50
|
+
function MessageAttachments({ attachments }) {
|
|
51
|
+
const images = attachments.filter((a) => a.type === 'image');
|
|
52
|
+
const videos = attachments.filter((a) => a.type === 'video');
|
|
53
|
+
const files = attachments.filter((a) => a.type === 'file');
|
|
54
|
+
return (_jsxs("div", { className: "ec-chat-message__attachments", children: [images.length > 0 && (_jsx("div", { className: "ec-chat-message__images", children: images.map((img, i) => (_jsx("a", { href: img.url, target: "_blank", rel: "noopener noreferrer", className: "ec-chat-message__image-link", children: _jsx("img", { src: img.thumbnailUrl ?? img.url, alt: img.alt ?? img.filename ?? 'Image attachment', className: "ec-chat-message__image", loading: "lazy" }) }, i))) })), videos.map((vid, i) => (_jsx("video", { src: vid.url, controls: true, className: "ec-chat-message__video", preload: "metadata", children: _jsx("track", { kind: "captions" }) }, i))), files.map((file, i) => (_jsxs("a", { href: file.url, download: file.filename, className: "ec-chat-message__file", target: "_blank", rel: "noopener noreferrer", children: [_jsx(FileIcon, {}), _jsx("span", { className: "ec-chat-message__file-name", children: file.filename ?? 'Download file' }), file.size != null && (_jsx("span", { className: "ec-chat-message__file-size", children: formatFileSize(file.size) }))] }, i)))] }));
|
|
55
|
+
}
|
|
56
|
+
function FileIcon() {
|
|
57
|
+
return (_jsxs("svg", { className: "ec-chat-message__file-icon", viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), _jsx("polyline", { points: "14 2 14 8 20 8" })] }));
|
|
58
|
+
}
|
|
59
|
+
function formatFileSize(bytes) {
|
|
60
|
+
if (bytes < 1024)
|
|
61
|
+
return `${bytes} B`;
|
|
62
|
+
if (bytes < 1024 * 1024)
|
|
63
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
64
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
65
|
+
}
|