@xinghunm/ai-chat 1.1.2 → 1.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/README.md CHANGED
@@ -55,6 +55,28 @@ const transport = createDefaultChatTransport({
55
55
  })
56
56
  ```
57
57
 
58
+ 如果后端协议大体一致,只是模型列表或 `/chat/completions` 请求体需要调整,也可以继续复用默认 transport,只覆盖局部扩展点:
59
+
60
+ ```tsx
61
+ const transport = createDefaultChatTransport({
62
+ apiBaseUrl: '/ai-api',
63
+ authToken: 'Bearer your-token-here',
64
+ resolveModels: async () => ({
65
+ data: [{ id: 'deepseek-chat', object: 'model' }],
66
+ }),
67
+ buildRequestBody: ({ model, mode, content }) => ({
68
+ model: 'deepseek-chat',
69
+ base_url: 'https://api.deepseek.com/v1',
70
+ api_key: 'sk-xxxxx',
71
+ mode,
72
+ stream: true,
73
+ messages: [{ role: 'user', content }],
74
+ }),
75
+ })
76
+ ```
77
+
78
+ 默认 transport 在检测到图片附件时,会自动把用户输入组装成多模态 `messages[].content` 数组,并将图片文件序列化为 Data URL 后发送;如果你自定义 `buildRequestBody`,也会同时收到 `attachments` 参数,可按后端协议自行处理。
79
+
58
80
  ## 完整用法
59
81
 
60
82
  如需最大灵活性,可在 `AiChatProvider` 内手动组合子组件:
@@ -74,6 +96,7 @@ const transport: ChatTransport = {
74
96
  model,
75
97
  mode,
76
98
  content,
99
+ attachments,
77
100
  onSessionId,
78
101
  onUpdate,
79
102
  onDone,
@@ -85,7 +108,7 @@ const transport: ChatTransport = {
85
108
  method: 'POST',
86
109
  signal,
87
110
  headers: { 'Content-Type': 'application/json' },
88
- body: JSON.stringify({ sessionId, model, mode, content }),
111
+ body: JSON.stringify({ sessionId, model, mode, content, attachments }),
89
112
  })
90
113
  const data = await response.json()
91
114
  onSessionId?.(data.sessionId)
@@ -193,6 +216,13 @@ export const CustomChat = () => (
193
216
  | `completions` | `"/chat/completions"` | 流式聊天接口路径。 |
194
217
  | `terminate` | `"/chat/terminate"` | 停止流式响应接口路径。 |
195
218
 
219
+ 此外还支持两个轻量扩展点:
220
+
221
+ | 键 | 类型 | 说明 |
222
+ | ------------------ | ----------------------------------- | ------------------------------------------------------------------------------ |
223
+ | `resolveModels` | `() => Promise<ChatModelsResponse>` | 覆盖默认的 `/models` 请求,直接返回模型列表。 |
224
+ | `buildRequestBody` | `(args) => unknown` | 覆盖默认的 `/chat/completions` 请求体构造逻辑;`args` 同时包含 `attachments`。 |
225
+
196
226
  ### `AiChatLabels`
197
227
 
198
228
  所有字段均为可选,未指定的字段回退到 `DEFAULT_AI_CHAT_LABELS` 中的英文默认值。
package/dist/index.d.mts CHANGED
@@ -42,6 +42,12 @@ interface ChatImageAttachment {
42
42
  mimeType: string;
43
43
  size: number;
44
44
  previewUrl: string;
45
+ /**
46
+ * Original file kept for transports that need to upload or serialize the image.
47
+ *
48
+ * This is only available for client-originated attachments selected in the composer.
49
+ */
50
+ file?: File;
45
51
  }
46
52
  /**
47
53
  * Select option metadata used by plan questionnaires.
@@ -291,6 +297,10 @@ interface ChatTransportStartStreamArgs {
291
297
  mode: ChatAgentMode;
292
298
  /** User message content that should be sent to the backend. */
293
299
  content: string;
300
+ /**
301
+ * Optional image attachments selected in the composer and sent alongside the text.
302
+ */
303
+ attachments?: ChatImageAttachment[];
294
304
  /** Abort signal controlled by the chat composer stop action. */
295
305
  signal?: AbortSignal;
296
306
  /** Emits normalized streaming patches that should update the assistant message. */
@@ -511,6 +521,25 @@ interface ChatToolExecutionPolicy {
511
521
  /** Approval timeout in seconds, forwarded when approval is enabled. */
512
522
  approvalTimeoutSec?: number;
513
523
  }
524
+ /**
525
+ * Arguments passed to a custom request body builder for the default transport.
526
+ */
527
+ interface DefaultChatTransportRequestBodyBuilderArgs {
528
+ sessionId?: string;
529
+ model: string;
530
+ mode: ChatAgentMode;
531
+ content: string;
532
+ attachments?: ChatImageAttachment[];
533
+ }
534
+ /**
535
+ * Optional builder used to customize the `/chat/completions` request body while
536
+ * reusing the default streaming transport behavior.
537
+ */
538
+ type DefaultChatTransportRequestBodyBuilder = (args: DefaultChatTransportRequestBodyBuilderArgs) => unknown;
539
+ /**
540
+ * Optional model resolver used to override the default `/models` request.
541
+ */
542
+ type DefaultChatTransportModelsResolver = () => Promise<ChatModelsResponse>;
514
543
  /**
515
544
  * Options for the built-in HTTP transport adapter.
516
545
  */
@@ -523,6 +552,10 @@ interface CreateDefaultChatTransportOptions {
523
552
  toolExecutionPolicy?: ChatToolExecutionPolicy;
524
553
  /** Optional extra headers appended to each streaming chat completion request. */
525
554
  streamHeaders?: Record<string, string>;
555
+ /** Optional resolver used to override the built-in model catalog request. */
556
+ resolveModels?: DefaultChatTransportModelsResolver;
557
+ /** Optional builder used to override the built-in chat completion request body. */
558
+ buildRequestBody?: DefaultChatTransportRequestBodyBuilder;
526
559
  /** Optional transformer used to normalize custom stream packets. */
527
560
  transformStreamPacket?: TransformChatStreamPacket;
528
561
  /** Optional endpoint overrides for backends that use different paths. */
@@ -533,7 +566,7 @@ interface CreateDefaultChatTransportOptions {
533
566
  /**
534
567
  * Creates the built-in transport backed by the current HTTP chat API.
535
568
  */
536
- declare const createDefaultChatTransport: ({ apiBaseUrl, authToken, toolExecutionPolicy, streamHeaders, transformStreamPacket, endpoints, axiosInstance, }: CreateDefaultChatTransportOptions) => ChatTransport;
569
+ declare const createDefaultChatTransport: ({ apiBaseUrl, authToken, toolExecutionPolicy, streamHeaders, resolveModels, buildRequestBody, transformStreamPacket, endpoints, axiosInstance, }: CreateDefaultChatTransportOptions) => ChatTransport;
537
570
 
538
571
  declare const ChatThread: () => _emotion_react_jsx_runtime.JSX.Element;
539
572
 
@@ -654,4 +687,4 @@ declare const useChatContext: () => ChatContextValue;
654
687
  */
655
688
  declare const useChatStore: <T>(selector: (state: ChatStore) => T) => T;
656
689
 
657
- export { AiChat, type AiChatLabels, type AiChatProps, AiChatProvider, type AiChatProviderProps, CHAT_AGENT_MODES, CHAT_MESSAGE_RENDER_ORDERS, type ChatAgentMode, ChatComposer, type ChatComposerViewProps, type ChatConfirmationSubmitHandler, ChatConversationList, type ChatImageAttachment, type ChatMessage, type ChatMessageBlock, type ChatMessageBlockRenderer, type ChatMessageBlockRendererProps, type ChatMessageRenderOrder, type ChatMessageStatus, type ChatModel, type ChatParameterSummaryItem, type ChatQuestionnaireSubmitHandler, type ChatRole, type ChatSession, type ChatStreamMessagePatch, type ChatStreamPacketTransformArgs, type ChatStreamPacketUpdate, type ChatSubmissionContext, ChatThread, type ChatToolExecutionPolicy, type ChatTransport, type ChatTransportStartStreamArgs, type CreateDefaultChatTransportOptions, DEFAULT_AI_CHAT_LABELS, DEFAULT_CHAT_AGENT_MODE, type DefaultChatTransportEndpoints, type ExecutionConfirmationSubmission, type ExecutionProposal, type PlanQuestionOption, type PlanQuestionSubmissionDetail, type PlanQuestionnaire, type PlanQuestionnaireSubmission, type ResultSummary, type TransformChatStreamPacket, createDefaultChatTransport, useChatContext, useChatStore };
690
+ export { AiChat, type AiChatLabels, type AiChatProps, AiChatProvider, type AiChatProviderProps, CHAT_AGENT_MODES, CHAT_MESSAGE_RENDER_ORDERS, type ChatAgentMode, ChatComposer, type ChatComposerViewProps, type ChatConfirmationSubmitHandler, ChatConversationList, type ChatImageAttachment, type ChatMessage, type ChatMessageBlock, type ChatMessageBlockRenderer, type ChatMessageBlockRendererProps, type ChatMessageRenderOrder, type ChatMessageStatus, type ChatModel, type ChatParameterSummaryItem, type ChatQuestionnaireSubmitHandler, type ChatRole, type ChatSession, type ChatStreamMessagePatch, type ChatStreamPacketTransformArgs, type ChatStreamPacketUpdate, type ChatSubmissionContext, ChatThread, type ChatToolExecutionPolicy, type ChatTransport, type ChatTransportStartStreamArgs, type CreateDefaultChatTransportOptions, DEFAULT_AI_CHAT_LABELS, DEFAULT_CHAT_AGENT_MODE, type DefaultChatTransportEndpoints, type DefaultChatTransportModelsResolver, type DefaultChatTransportRequestBodyBuilder, type DefaultChatTransportRequestBodyBuilderArgs, type ExecutionConfirmationSubmission, type ExecutionProposal, type PlanQuestionOption, type PlanQuestionSubmissionDetail, type PlanQuestionnaire, type PlanQuestionnaireSubmission, type ResultSummary, type TransformChatStreamPacket, createDefaultChatTransport, useChatContext, useChatStore };
package/dist/index.d.ts CHANGED
@@ -42,6 +42,12 @@ interface ChatImageAttachment {
42
42
  mimeType: string;
43
43
  size: number;
44
44
  previewUrl: string;
45
+ /**
46
+ * Original file kept for transports that need to upload or serialize the image.
47
+ *
48
+ * This is only available for client-originated attachments selected in the composer.
49
+ */
50
+ file?: File;
45
51
  }
46
52
  /**
47
53
  * Select option metadata used by plan questionnaires.
@@ -291,6 +297,10 @@ interface ChatTransportStartStreamArgs {
291
297
  mode: ChatAgentMode;
292
298
  /** User message content that should be sent to the backend. */
293
299
  content: string;
300
+ /**
301
+ * Optional image attachments selected in the composer and sent alongside the text.
302
+ */
303
+ attachments?: ChatImageAttachment[];
294
304
  /** Abort signal controlled by the chat composer stop action. */
295
305
  signal?: AbortSignal;
296
306
  /** Emits normalized streaming patches that should update the assistant message. */
@@ -511,6 +521,25 @@ interface ChatToolExecutionPolicy {
511
521
  /** Approval timeout in seconds, forwarded when approval is enabled. */
512
522
  approvalTimeoutSec?: number;
513
523
  }
524
+ /**
525
+ * Arguments passed to a custom request body builder for the default transport.
526
+ */
527
+ interface DefaultChatTransportRequestBodyBuilderArgs {
528
+ sessionId?: string;
529
+ model: string;
530
+ mode: ChatAgentMode;
531
+ content: string;
532
+ attachments?: ChatImageAttachment[];
533
+ }
534
+ /**
535
+ * Optional builder used to customize the `/chat/completions` request body while
536
+ * reusing the default streaming transport behavior.
537
+ */
538
+ type DefaultChatTransportRequestBodyBuilder = (args: DefaultChatTransportRequestBodyBuilderArgs) => unknown;
539
+ /**
540
+ * Optional model resolver used to override the default `/models` request.
541
+ */
542
+ type DefaultChatTransportModelsResolver = () => Promise<ChatModelsResponse>;
514
543
  /**
515
544
  * Options for the built-in HTTP transport adapter.
516
545
  */
@@ -523,6 +552,10 @@ interface CreateDefaultChatTransportOptions {
523
552
  toolExecutionPolicy?: ChatToolExecutionPolicy;
524
553
  /** Optional extra headers appended to each streaming chat completion request. */
525
554
  streamHeaders?: Record<string, string>;
555
+ /** Optional resolver used to override the built-in model catalog request. */
556
+ resolveModels?: DefaultChatTransportModelsResolver;
557
+ /** Optional builder used to override the built-in chat completion request body. */
558
+ buildRequestBody?: DefaultChatTransportRequestBodyBuilder;
526
559
  /** Optional transformer used to normalize custom stream packets. */
527
560
  transformStreamPacket?: TransformChatStreamPacket;
528
561
  /** Optional endpoint overrides for backends that use different paths. */
@@ -533,7 +566,7 @@ interface CreateDefaultChatTransportOptions {
533
566
  /**
534
567
  * Creates the built-in transport backed by the current HTTP chat API.
535
568
  */
536
- declare const createDefaultChatTransport: ({ apiBaseUrl, authToken, toolExecutionPolicy, streamHeaders, transformStreamPacket, endpoints, axiosInstance, }: CreateDefaultChatTransportOptions) => ChatTransport;
569
+ declare const createDefaultChatTransport: ({ apiBaseUrl, authToken, toolExecutionPolicy, streamHeaders, resolveModels, buildRequestBody, transformStreamPacket, endpoints, axiosInstance, }: CreateDefaultChatTransportOptions) => ChatTransport;
537
570
 
538
571
  declare const ChatThread: () => _emotion_react_jsx_runtime.JSX.Element;
539
572
 
@@ -654,4 +687,4 @@ declare const useChatContext: () => ChatContextValue;
654
687
  */
655
688
  declare const useChatStore: <T>(selector: (state: ChatStore) => T) => T;
656
689
 
657
- export { AiChat, type AiChatLabels, type AiChatProps, AiChatProvider, type AiChatProviderProps, CHAT_AGENT_MODES, CHAT_MESSAGE_RENDER_ORDERS, type ChatAgentMode, ChatComposer, type ChatComposerViewProps, type ChatConfirmationSubmitHandler, ChatConversationList, type ChatImageAttachment, type ChatMessage, type ChatMessageBlock, type ChatMessageBlockRenderer, type ChatMessageBlockRendererProps, type ChatMessageRenderOrder, type ChatMessageStatus, type ChatModel, type ChatParameterSummaryItem, type ChatQuestionnaireSubmitHandler, type ChatRole, type ChatSession, type ChatStreamMessagePatch, type ChatStreamPacketTransformArgs, type ChatStreamPacketUpdate, type ChatSubmissionContext, ChatThread, type ChatToolExecutionPolicy, type ChatTransport, type ChatTransportStartStreamArgs, type CreateDefaultChatTransportOptions, DEFAULT_AI_CHAT_LABELS, DEFAULT_CHAT_AGENT_MODE, type DefaultChatTransportEndpoints, type ExecutionConfirmationSubmission, type ExecutionProposal, type PlanQuestionOption, type PlanQuestionSubmissionDetail, type PlanQuestionnaire, type PlanQuestionnaireSubmission, type ResultSummary, type TransformChatStreamPacket, createDefaultChatTransport, useChatContext, useChatStore };
690
+ export { AiChat, type AiChatLabels, type AiChatProps, AiChatProvider, type AiChatProviderProps, CHAT_AGENT_MODES, CHAT_MESSAGE_RENDER_ORDERS, type ChatAgentMode, ChatComposer, type ChatComposerViewProps, type ChatConfirmationSubmitHandler, ChatConversationList, type ChatImageAttachment, type ChatMessage, type ChatMessageBlock, type ChatMessageBlockRenderer, type ChatMessageBlockRendererProps, type ChatMessageRenderOrder, type ChatMessageStatus, type ChatModel, type ChatParameterSummaryItem, type ChatQuestionnaireSubmitHandler, type ChatRole, type ChatSession, type ChatStreamMessagePatch, type ChatStreamPacketTransformArgs, type ChatStreamPacketUpdate, type ChatSubmissionContext, ChatThread, type ChatToolExecutionPolicy, type ChatTransport, type ChatTransportStartStreamArgs, type CreateDefaultChatTransportOptions, DEFAULT_AI_CHAT_LABELS, DEFAULT_CHAT_AGENT_MODE, type DefaultChatTransportEndpoints, type DefaultChatTransportModelsResolver, type DefaultChatTransportRequestBodyBuilder, type DefaultChatTransportRequestBodyBuilderArgs, type ExecutionConfirmationSubmission, type ExecutionProposal, type PlanQuestionOption, type PlanQuestionSubmissionDetail, type PlanQuestionnaire, type PlanQuestionnaireSubmission, type ResultSummary, type TransformChatStreamPacket, createDefaultChatTransport, useChatContext, useChatStore };
package/dist/index.js CHANGED
@@ -491,6 +491,7 @@ var startChatStream = async ({
491
491
  sessionId,
492
492
  authToken,
493
493
  requestHeaders,
494
+ requestBody,
494
495
  model,
495
496
  mode,
496
497
  content,
@@ -512,12 +513,14 @@ var startChatStream = async ({
512
513
  const response = await fetch(`${apiBaseUrl}${endpointPath}`, {
513
514
  method: "POST",
514
515
  headers,
515
- body: JSON.stringify({
516
- model,
517
- mode,
518
- stream: true,
519
- messages: [{ role: "user", content }]
520
- }),
516
+ body: JSON.stringify(
517
+ requestBody ?? {
518
+ model,
519
+ mode,
520
+ stream: true,
521
+ messages: [{ role: "user", content }]
522
+ }
523
+ ),
521
524
  signal
522
525
  });
523
526
  const contentType = response.headers.get("content-type") ?? "";
@@ -593,11 +596,70 @@ var createModeDefaultHeaders = (mode) => {
593
596
  }
594
597
  return {};
595
598
  };
599
+ var readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
600
+ const reader = new FileReader();
601
+ reader.onload = () => {
602
+ if (typeof reader.result === "string") {
603
+ resolve(reader.result);
604
+ return;
605
+ }
606
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
607
+ };
608
+ reader.onerror = () => {
609
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
610
+ };
611
+ reader.readAsDataURL(file);
612
+ });
613
+ var resolveAttachmentDataUrl = async (attachment) => {
614
+ if (attachment.file) {
615
+ return readFileAsDataUrl(attachment.file);
616
+ }
617
+ if (attachment.previewUrl.startsWith("data:image/")) {
618
+ return attachment.previewUrl;
619
+ }
620
+ throw new Error(`Attachment is missing file data: ${attachment.name}`);
621
+ };
622
+ var createDefaultRequestBody = async ({
623
+ model,
624
+ mode,
625
+ content,
626
+ attachments
627
+ }) => {
628
+ const hasAttachments = Boolean(attachments?.length);
629
+ if (!hasAttachments) {
630
+ return {
631
+ model,
632
+ mode,
633
+ stream: true,
634
+ messages: [{ role: "user", content }]
635
+ };
636
+ }
637
+ const imageParts = await Promise.all(
638
+ (attachments ?? []).map(async (attachment) => ({
639
+ type: "image_url",
640
+ image_url: {
641
+ url: await resolveAttachmentDataUrl(attachment)
642
+ }
643
+ }))
644
+ );
645
+ const messageContent = [
646
+ ...content ? [{ type: "text", text: content }] : [],
647
+ ...imageParts
648
+ ];
649
+ return {
650
+ model,
651
+ mode,
652
+ stream: true,
653
+ messages: [{ role: "user", content: messageContent }]
654
+ };
655
+ };
596
656
  var createDefaultChatTransport = ({
597
657
  apiBaseUrl,
598
658
  authToken,
599
659
  toolExecutionPolicy,
600
660
  streamHeaders,
661
+ resolveModels,
662
+ buildRequestBody,
601
663
  transformStreamPacket,
602
664
  endpoints,
603
665
  axiosInstance
@@ -612,12 +674,13 @@ var createDefaultChatTransport = ({
612
674
  ...streamHeaders
613
675
  };
614
676
  return {
615
- getModels: () => getChatModels(client, resolvedEndpoints.models),
677
+ getModels: () => resolveModels?.() ?? getChatModels(client, resolvedEndpoints.models),
616
678
  startStream: async ({
617
679
  sessionId,
618
680
  model,
619
681
  mode,
620
682
  content,
683
+ attachments,
621
684
  signal,
622
685
  onUpdate,
623
686
  onSessionId,
@@ -628,12 +691,20 @@ var createDefaultChatTransport = ({
628
691
  ...createModeDefaultHeaders(mode),
629
692
  ...resolvedStreamHeaders
630
693
  };
694
+ const requestBody = buildRequestBody ? buildRequestBody({
695
+ sessionId,
696
+ model,
697
+ mode,
698
+ content,
699
+ attachments
700
+ }) : await createDefaultRequestBody({ model, mode, content, attachments });
631
701
  await startChatStream({
632
702
  apiBaseUrl,
633
703
  endpointPath: resolvedEndpoints.completions,
634
704
  sessionId,
635
705
  authToken,
636
706
  requestHeaders,
707
+ requestBody,
637
708
  model,
638
709
  mode,
639
710
  content,
@@ -2339,7 +2410,7 @@ var TextInput = import_styled4.default.input`
2339
2410
  color: rgba(255, 255, 255, 0.34);
2340
2411
  }
2341
2412
  `;
2342
- var InlineOtherInput = (0, import_styled4.default)(import_compass_ui.Input)`
2413
+ var InlineOtherInput = (0, import_styled4.default)(import_compass_ui.InputField)`
2343
2414
  width: 100%;
2344
2415
  margin-top: 0;
2345
2416
 
@@ -4087,7 +4158,8 @@ var useComposerAttachments = () => {
4087
4158
  return [];
4088
4159
  }
4089
4160
  const nextMessageAttachments = currentAttachments.map(({ file: _file, ...attachment }) => ({
4090
- ...attachment
4161
+ ...attachment,
4162
+ file: _file
4091
4163
  }));
4092
4164
  attachmentsRef.current = [];
4093
4165
  setAttachments([]);
@@ -4237,6 +4309,7 @@ var useChatComposer = () => {
4237
4309
  localSessionId,
4238
4310
  sessionId,
4239
4311
  content,
4312
+ attachments: attachments2,
4240
4313
  model,
4241
4314
  mode
4242
4315
  }) => {
@@ -4251,7 +4324,7 @@ var useChatComposer = () => {
4251
4324
  startStreamingMessage(currentSessionId, assistantMessage);
4252
4325
  abortControllerRef.current?.abort();
4253
4326
  abortControllerRef.current = new AbortController();
4254
- lastRequestRef.current = { localSessionId, sessionId, content, model, mode };
4327
+ lastRequestRef.current = { localSessionId, sessionId, content, attachments: attachments2, model, mode };
4255
4328
  let accumulated = "";
4256
4329
  try {
4257
4330
  await transport.startStream({
@@ -4259,6 +4332,7 @@ var useChatComposer = () => {
4259
4332
  model,
4260
4333
  mode,
4261
4334
  content,
4335
+ attachments: attachments2,
4262
4336
  signal: abortControllerRef.current.signal,
4263
4337
  onSessionId: (nextSessionId) => {
4264
4338
  if (!nextSessionId || nextSessionId === currentSessionId)
@@ -4269,6 +4343,7 @@ var useChatComposer = () => {
4269
4343
  localSessionId: nextSessionId,
4270
4344
  sessionId: nextSessionId,
4271
4345
  content,
4346
+ attachments: attachments2,
4272
4347
  model,
4273
4348
  mode
4274
4349
  };
@@ -4324,8 +4399,6 @@ var useChatComposer = () => {
4324
4399
  const send = (0, import_react13.useCallback)(
4325
4400
  async (contentOverride) => {
4326
4401
  const content = (contentOverride ?? value).trim();
4327
- const hasText = Boolean(content);
4328
- const hasAttachments = attachments.length > 0;
4329
4402
  if (!canSendChatMessage({
4330
4403
  value: content,
4331
4404
  attachmentCount: attachments.length,
@@ -4335,7 +4408,7 @@ var useChatComposer = () => {
4335
4408
  })) {
4336
4409
  return;
4337
4410
  }
4338
- if (hasText && !(selectedModel || activeSession?.model || availableModels[0]?.id)) {
4411
+ if (!(selectedModel || activeSession?.model || availableModels[0]?.id)) {
4339
4412
  return;
4340
4413
  }
4341
4414
  const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
@@ -4353,19 +4426,18 @@ var useChatComposer = () => {
4353
4426
  sessionId: localSessionId,
4354
4427
  content,
4355
4428
  attachments: messageAttachments,
4356
- localOnly: hasAttachments,
4429
+ localOnly: false,
4357
4430
  createdAt: nowIso(),
4358
4431
  createMessageId: () => `user-${Date.now()}`
4359
4432
  });
4360
4433
  appendMessage(localSessionId, userMessage);
4361
4434
  setAttachmentNotice(null);
4362
4435
  setValue("");
4363
- if (!hasText)
4364
- return;
4365
4436
  await runStream({
4366
4437
  localSessionId,
4367
4438
  sessionId,
4368
4439
  content,
4440
+ attachments: messageAttachments,
4369
4441
  model: resolvedModel,
4370
4442
  mode: selectedMode
4371
4443
  });
@@ -5324,9 +5396,9 @@ var ComposerExpandButton = import_styled14.default.button`
5324
5396
  var Footer = import_styled14.default.div`
5325
5397
  grid-area: footer;
5326
5398
  display: grid;
5327
- grid-template-columns: minmax(0, 1fr) auto;
5399
+ grid-template-columns: auto minmax(0, 1fr);
5328
5400
  align-items: flex-end;
5329
- gap: 16px;
5401
+ gap: 8px;
5330
5402
  padding: 0 14px 14px;
5331
5403
  `;
5332
5404
  var LeadingActions = import_styled14.default.div`
@@ -5341,6 +5413,9 @@ var TrailingActions = import_styled14.default.div`
5341
5413
  align-items: center;
5342
5414
  flex-wrap: wrap;
5343
5415
  min-width: 0;
5416
+ width: fit-content;
5417
+ max-width: 100%;
5418
+ justify-self: end;
5344
5419
  justify-content: flex-end;
5345
5420
  gap: 8px;
5346
5421
  `;
@@ -5555,6 +5630,7 @@ var Workspace = import_styled17.default.section`
5555
5630
  flex: 1;
5556
5631
  display: flex;
5557
5632
  flex-direction: column;
5633
+ gap: 12px;
5558
5634
  min-height: 0;
5559
5635
  overflow: hidden;
5560
5636
  `;
package/dist/index.mjs CHANGED
@@ -444,6 +444,7 @@ var startChatStream = async ({
444
444
  sessionId,
445
445
  authToken,
446
446
  requestHeaders,
447
+ requestBody,
447
448
  model,
448
449
  mode,
449
450
  content,
@@ -465,12 +466,14 @@ var startChatStream = async ({
465
466
  const response = await fetch(`${apiBaseUrl}${endpointPath}`, {
466
467
  method: "POST",
467
468
  headers,
468
- body: JSON.stringify({
469
- model,
470
- mode,
471
- stream: true,
472
- messages: [{ role: "user", content }]
473
- }),
469
+ body: JSON.stringify(
470
+ requestBody ?? {
471
+ model,
472
+ mode,
473
+ stream: true,
474
+ messages: [{ role: "user", content }]
475
+ }
476
+ ),
474
477
  signal
475
478
  });
476
479
  const contentType = response.headers.get("content-type") ?? "";
@@ -546,11 +549,70 @@ var createModeDefaultHeaders = (mode) => {
546
549
  }
547
550
  return {};
548
551
  };
552
+ var readFileAsDataUrl = (file) => new Promise((resolve, reject) => {
553
+ const reader = new FileReader();
554
+ reader.onload = () => {
555
+ if (typeof reader.result === "string") {
556
+ resolve(reader.result);
557
+ return;
558
+ }
559
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
560
+ };
561
+ reader.onerror = () => {
562
+ reject(new Error(`Failed to read image attachment: ${file.name}`));
563
+ };
564
+ reader.readAsDataURL(file);
565
+ });
566
+ var resolveAttachmentDataUrl = async (attachment) => {
567
+ if (attachment.file) {
568
+ return readFileAsDataUrl(attachment.file);
569
+ }
570
+ if (attachment.previewUrl.startsWith("data:image/")) {
571
+ return attachment.previewUrl;
572
+ }
573
+ throw new Error(`Attachment is missing file data: ${attachment.name}`);
574
+ };
575
+ var createDefaultRequestBody = async ({
576
+ model,
577
+ mode,
578
+ content,
579
+ attachments
580
+ }) => {
581
+ const hasAttachments = Boolean(attachments?.length);
582
+ if (!hasAttachments) {
583
+ return {
584
+ model,
585
+ mode,
586
+ stream: true,
587
+ messages: [{ role: "user", content }]
588
+ };
589
+ }
590
+ const imageParts = await Promise.all(
591
+ (attachments ?? []).map(async (attachment) => ({
592
+ type: "image_url",
593
+ image_url: {
594
+ url: await resolveAttachmentDataUrl(attachment)
595
+ }
596
+ }))
597
+ );
598
+ const messageContent = [
599
+ ...content ? [{ type: "text", text: content }] : [],
600
+ ...imageParts
601
+ ];
602
+ return {
603
+ model,
604
+ mode,
605
+ stream: true,
606
+ messages: [{ role: "user", content: messageContent }]
607
+ };
608
+ };
549
609
  var createDefaultChatTransport = ({
550
610
  apiBaseUrl,
551
611
  authToken,
552
612
  toolExecutionPolicy,
553
613
  streamHeaders,
614
+ resolveModels,
615
+ buildRequestBody,
554
616
  transformStreamPacket,
555
617
  endpoints,
556
618
  axiosInstance
@@ -565,12 +627,13 @@ var createDefaultChatTransport = ({
565
627
  ...streamHeaders
566
628
  };
567
629
  return {
568
- getModels: () => getChatModels(client, resolvedEndpoints.models),
630
+ getModels: () => resolveModels?.() ?? getChatModels(client, resolvedEndpoints.models),
569
631
  startStream: async ({
570
632
  sessionId,
571
633
  model,
572
634
  mode,
573
635
  content,
636
+ attachments,
574
637
  signal,
575
638
  onUpdate,
576
639
  onSessionId,
@@ -581,12 +644,20 @@ var createDefaultChatTransport = ({
581
644
  ...createModeDefaultHeaders(mode),
582
645
  ...resolvedStreamHeaders
583
646
  };
647
+ const requestBody = buildRequestBody ? buildRequestBody({
648
+ sessionId,
649
+ model,
650
+ mode,
651
+ content,
652
+ attachments
653
+ }) : await createDefaultRequestBody({ model, mode, content, attachments });
584
654
  await startChatStream({
585
655
  apiBaseUrl,
586
656
  endpointPath: resolvedEndpoints.completions,
587
657
  sessionId,
588
658
  authToken,
589
659
  requestHeaders,
660
+ requestBody,
590
661
  model,
591
662
  mode,
592
663
  content,
@@ -1495,7 +1566,7 @@ import {
1495
1566
  useState as useState2
1496
1567
  } from "react";
1497
1568
  import styled4 from "@emotion/styled";
1498
- import { Input } from "@xinghunm/compass-ui";
1569
+ import { InputField as Input } from "@xinghunm/compass-ui";
1499
1570
 
1500
1571
  // src/components/chat-thread/components/questionnaire-card-helpers.ts
1501
1572
  var OTHER_OPTION_VALUE = "__other__";
@@ -4044,7 +4115,8 @@ var useComposerAttachments = () => {
4044
4115
  return [];
4045
4116
  }
4046
4117
  const nextMessageAttachments = currentAttachments.map(({ file: _file, ...attachment }) => ({
4047
- ...attachment
4118
+ ...attachment,
4119
+ file: _file
4048
4120
  }));
4049
4121
  attachmentsRef.current = [];
4050
4122
  setAttachments([]);
@@ -4194,6 +4266,7 @@ var useChatComposer = () => {
4194
4266
  localSessionId,
4195
4267
  sessionId,
4196
4268
  content,
4269
+ attachments: attachments2,
4197
4270
  model,
4198
4271
  mode
4199
4272
  }) => {
@@ -4208,7 +4281,7 @@ var useChatComposer = () => {
4208
4281
  startStreamingMessage(currentSessionId, assistantMessage);
4209
4282
  abortControllerRef.current?.abort();
4210
4283
  abortControllerRef.current = new AbortController();
4211
- lastRequestRef.current = { localSessionId, sessionId, content, model, mode };
4284
+ lastRequestRef.current = { localSessionId, sessionId, content, attachments: attachments2, model, mode };
4212
4285
  let accumulated = "";
4213
4286
  try {
4214
4287
  await transport.startStream({
@@ -4216,6 +4289,7 @@ var useChatComposer = () => {
4216
4289
  model,
4217
4290
  mode,
4218
4291
  content,
4292
+ attachments: attachments2,
4219
4293
  signal: abortControllerRef.current.signal,
4220
4294
  onSessionId: (nextSessionId) => {
4221
4295
  if (!nextSessionId || nextSessionId === currentSessionId)
@@ -4226,6 +4300,7 @@ var useChatComposer = () => {
4226
4300
  localSessionId: nextSessionId,
4227
4301
  sessionId: nextSessionId,
4228
4302
  content,
4303
+ attachments: attachments2,
4229
4304
  model,
4230
4305
  mode
4231
4306
  };
@@ -4281,8 +4356,6 @@ var useChatComposer = () => {
4281
4356
  const send = useCallback4(
4282
4357
  async (contentOverride) => {
4283
4358
  const content = (contentOverride ?? value).trim();
4284
- const hasText = Boolean(content);
4285
- const hasAttachments = attachments.length > 0;
4286
4359
  if (!canSendChatMessage({
4287
4360
  value: content,
4288
4361
  attachmentCount: attachments.length,
@@ -4292,7 +4365,7 @@ var useChatComposer = () => {
4292
4365
  })) {
4293
4366
  return;
4294
4367
  }
4295
- if (hasText && !(selectedModel || activeSession?.model || availableModels[0]?.id)) {
4368
+ if (!(selectedModel || activeSession?.model || availableModels[0]?.id)) {
4296
4369
  return;
4297
4370
  }
4298
4371
  const resolvedModel = selectedModel || activeSession?.model || availableModels[0]?.id || "local-image";
@@ -4310,19 +4383,18 @@ var useChatComposer = () => {
4310
4383
  sessionId: localSessionId,
4311
4384
  content,
4312
4385
  attachments: messageAttachments,
4313
- localOnly: hasAttachments,
4386
+ localOnly: false,
4314
4387
  createdAt: nowIso(),
4315
4388
  createMessageId: () => `user-${Date.now()}`
4316
4389
  });
4317
4390
  appendMessage(localSessionId, userMessage);
4318
4391
  setAttachmentNotice(null);
4319
4392
  setValue("");
4320
- if (!hasText)
4321
- return;
4322
4393
  await runStream({
4323
4394
  localSessionId,
4324
4395
  sessionId,
4325
4396
  content,
4397
+ attachments: messageAttachments,
4326
4398
  model: resolvedModel,
4327
4399
  mode: selectedMode
4328
4400
  });
@@ -5281,9 +5353,9 @@ var ComposerExpandButton = styled14.button`
5281
5353
  var Footer = styled14.div`
5282
5354
  grid-area: footer;
5283
5355
  display: grid;
5284
- grid-template-columns: minmax(0, 1fr) auto;
5356
+ grid-template-columns: auto minmax(0, 1fr);
5285
5357
  align-items: flex-end;
5286
- gap: 16px;
5358
+ gap: 8px;
5287
5359
  padding: 0 14px 14px;
5288
5360
  `;
5289
5361
  var LeadingActions = styled14.div`
@@ -5298,6 +5370,9 @@ var TrailingActions = styled14.div`
5298
5370
  align-items: center;
5299
5371
  flex-wrap: wrap;
5300
5372
  min-width: 0;
5373
+ width: fit-content;
5374
+ max-width: 100%;
5375
+ justify-self: end;
5301
5376
  justify-content: flex-end;
5302
5377
  gap: 8px;
5303
5378
  `;
@@ -5512,6 +5587,7 @@ var Workspace = styled17.section`
5512
5587
  flex: 1;
5513
5588
  display: flex;
5514
5589
  flex-direction: column;
5590
+ gap: 12px;
5515
5591
  min-height: 0;
5516
5592
  overflow: hidden;
5517
5593
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xinghunm/ai-chat",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "AI chat React component library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -19,7 +19,7 @@
19
19
  "peerDependencies": {
20
20
  "@emotion/react": ">=11",
21
21
  "@emotion/styled": ">=11",
22
- "@xinghunm/compass-ui": ">=0.9.0",
22
+ "@xinghunm/compass-ui": "0.8.3",
23
23
  "axios": ">=1.0",
24
24
  "react": ">=18",
25
25
  "react-dom": ">=18",