@extrachill/chat 0.3.1 → 0.5.1

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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1] - 2026-03-24
4
+
5
+ ### Changed
6
+ - add metadata prop to Chat component for client context injection
7
+
8
+ ## [0.5.0] - 2026-03-24
9
+
10
+ ### Added
11
+ - v0.4.0 — metadata, onToolCalls, processingSessionId, request dedup
12
+ - media support — attachments in messages, image/video rendering, file input
13
+ - swap to react-markdown for proper rich content rendering
14
+ - built-in markdown rendering for chat messages
15
+ - remove adapter pattern, speak chat REST API natively
16
+ - implement adapter contract, message model, and component library
17
+
18
+ ### Changed
19
+ - Initial commit
20
+
21
+ ### Fixed
22
+ - scroll within chat container instead of hijacking page scroll
23
+ - extract readable error message from @wordpress/api-fetch error objects
24
+ - use npm run build in prepublishOnly (pnpm not available on server)
25
+
26
+ ## 0.4.0
27
+
28
+ ### Added
29
+ - `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)
30
+ - `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 }`)
31
+ - `sessionContext` option on `useChat` — filters session listing to only show sessions created in a specific context
32
+ - `processingSessionId` in `UseChatReturn` — tracks which session initiated the current request, preventing stale loading indicators when switching sessions mid-request
33
+ - `context` parameter on `listSessions` API function — optional context filter for session listing
34
+ - `metadata` parameter on `sendMessage` API function — forwarded to backend alongside the message
35
+ - `X-Request-ID` header on send requests — automatic request deduplication via `crypto.randomUUID()` with fallback
36
+ - `headers` field on `FetchOptions` — allows passing custom HTTP headers through the fetch function
37
+ - Session creation guard — prevents concurrent session creation with `isCreatingRef`
38
+
3
39
  ## 0.2.0
4
40
 
5
41
  - **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,15 @@ 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;
52
+ /**
53
+ * Arbitrary metadata forwarded to the backend with each message.
54
+ * Use for client-side context injection (e.g. `{ client_context: { tab: 'compose', postId: 123 } }`).
55
+ */
56
+ metadata?: Record<string, unknown>;
48
57
  }
49
58
  /**
50
59
  * Ready-to-use chat component.
@@ -69,5 +78,5 @@ export interface ChatProps {
69
78
  * }
70
79
  * ```
71
80
  */
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;
81
+ export declare function Chat({ basePath, fetchFn, agentId, contentFormat, renderContent, showTools, toolNames, placeholder, emptyState, initialMessages, initialSessionId, maxContinueTurns, onError, onMessage, className, showSessions, processingLabel, allowAttachments, acceptFileTypes, metadata, }: ChatProps): import("react/jsx-runtime").JSX.Element;
73
82
  //# sourceMappingURL=Chat.d.ts.map
@@ -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;CAChD;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,GACf,EAAE,SAAS,2CA2DX"}
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;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;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,EACf,QAAQ,GACR,EAAE,SAAS,2CA8DX"}
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, metadata, }) {
33
33
  const chat = useChat({
34
34
  basePath,
35
35
  fetchFn,
@@ -39,6 +39,7 @@ export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', r
39
39
  maxContinueTurns,
40
40
  onError,
41
41
  onMessage,
42
+ metadata,
42
43
  });
43
44
  const baseClass = 'ec-chat';
44
45
  const classes = [baseClass, className].filter(Boolean).join(' ');
@@ -46,5 +47,5 @@ export function Chat({ basePath, fetchFn, agentId, contentFormat = 'markdown', r
46
47
  ? (processingLabel
47
48
  ? processingLabel(chat.turnCount)
48
49
  : `Processing turn ${chat.turnCount}...`)
49
- : undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder })] }) }) }));
50
+ : undefined }), _jsx(ChatInput, { onSend: chat.sendMessage, disabled: chat.isLoading, placeholder: placeholder, allowAttachments: allowAttachments, accept: acceptFileTypes })] }) }) }));
50
51
  }
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;CAC/B;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,wBAAsB,WAAW,CAChC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACrC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED;;GAEG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,aAAa,EACrB,KAAK,SAAK,GACR,OAAO,CAAC,WAAW,EAAE,CAAC,CAaxB;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"}
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 keyboard shortcuts.
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,8CAA8C;IAC9C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EACzB,MAAM,EACN,QAAgB,EAChB,WAAiC,EACjC,OAAW,EACX,SAAS,GACT,EAAE,cAAc,2CAkEhB"}
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 keyboard shortcuts.
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
- // Reset textarea height after clearing
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 = [baseClass, className].filter(Boolean).join(' ');
48
- return (_jsxs("form", { className: classes, onSubmit: handleSubmit, children: [_jsx("textarea", { ref: textareaRef, className: `${baseClass}__textarea`, value: value, onChange: (e) => { setValue(e.target.value); resize(); }, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: disabled, rows: 1, "aria-label": placeholder }), _jsx("button", { className: `${baseClass}__send`, type: "submit", disabled: disabled || !value.trim(), "aria-label": "Send message", children: _jsx(SendIcon, {}) })] }));
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,EAAE,MAAM,mBAAmB,CAAC;AAKvF,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;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC3B,OAAO,EACP,aAA0B,EAC1B,aAAa,EACb,SAAS,GACT,EAAE,gBAAgB,2CAyBlB"}
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
- return (_jsxs("div", { className: classes, "data-message-id": message.id, children: [_jsx("div", { className: `${baseClass}__bubble`, children: renderContent
17
- ? renderContent(message.content, message.role)
18
- : _jsx(DefaultContent, { content: message.content, format: contentFormat }) }), message.timestamp && (_jsx("time", { className: `${baseClass}__timestamp`, dateTime: message.timestamp, title: new Date(message.timestamp).toLocaleString(), children: formatTime(message.timestamp) }))] }));
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
+ }