@agno-hq/chat-react 0.3.0 → 0.3.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.
@@ -1,8 +1,8 @@
1
- import { AgnoClient, activityLabel, isSubRunEvent, applySubRunEvent, isStepEvent, applyStepEvent, isStartedEvent, isToolEvent, toolsFromEvent, mergeTool, isContentEvent, applyContentEvent, isReasoningStepEvent, isReasoningCompletedEvent, isFollowupsCompletedEvent, isPausedEvent, isCompletedEvent, isCancelledEvent, isErrorEvent, sessionRunsToMessages, isToolCompletedEvent } from './chunk-62IEW2HM.js';
1
+ import { AgnoClient, activityLabel, isSubRunEvent, applySubRunEvent, isStepEvent, applyStepEvent, isStartedEvent, isToolEvent, toolsFromEvent, mergeTool, isContentEvent, applyContentEvent, isReasoningStepEvent, isReasoningCompletedEvent, isFollowupsCompletedEvent, isPausedEvent, isCompletedEvent, isCancelledEvent, isErrorEvent, sessionRunsToMessages, isToolCompletedEvent } from './chunk-6V2YIPHF.js';
2
2
  import React7, { createContext, useMemo, useState, useRef, useEffect, useCallback, useContext, useId, isValidElement, cloneElement, useLayoutEffect, useReducer } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import { createPortal } from 'react-dom';
5
- import { Square, ChevronDown as ChevronDown$1, ArrowUp as ArrowUp$1, Paperclip as Paperclip$1, File, FileAudio as FileAudio$1, FileVideo as FileVideo$1, Wrench as Wrench$1, BookOpen as BookOpen$1, Globe as Globe$1, Brain as Brain$1, Box as Box$1, Plus as Plus$1, Check as Check$1, Copy as Copy$1, X, MemoryStick, RefreshCcw, Activity, Trash2, MessageSquare, LoaderCircle } from 'lucide-react';
5
+ import { Square, ChevronDown as ChevronDown$1, ArrowUp as ArrowUp$1, Paperclip as Paperclip$1, File, FileType2, FileAudio as FileAudio$1, FileVideo as FileVideo$1, Wrench as Wrench$1, BookOpen as BookOpen$1, Globe as Globe$1, Link, Brain as Brain$1, Box as Box$1, Plus as Plus$1, Check as Check$1, Copy as Copy$1, X, MemoryStick, RefreshCcw, Activity, Trash2, MessageSquare, LoaderCircle } from 'lucide-react';
6
6
  import { Streamdown } from 'streamdown';
7
7
 
8
8
  var messageCounter = 0;
@@ -577,6 +577,7 @@ function ChatProvider({
577
577
  renderMarkdown,
578
578
  resolveLinkPreview,
579
579
  codeCopy,
580
+ linkComponent,
580
581
  chat: externalChat,
581
582
  as,
582
583
  className,
@@ -587,8 +588,8 @@ function ChatProvider({
587
588
  const internal = useAgnoChat(externalChat ? { entity: null } : options);
588
589
  const chat = externalChat ?? internal;
589
590
  const value = useMemo(
590
- () => ({ chat, classNames: classNames ?? {}, renderMarkdown, resolveLinkPreview, codeCopy }),
591
- [chat, classNames, renderMarkdown, resolveLinkPreview, codeCopy]
591
+ () => ({ chat, classNames: classNames ?? {}, renderMarkdown, resolveLinkPreview, codeCopy, linkComponent }),
592
+ [chat, classNames, renderMarkdown, resolveLinkPreview, codeCopy, linkComponent]
592
593
  );
593
594
  const Wrapper = as === false ? null : as ?? "div";
594
595
  return /* @__PURE__ */ jsx(ChatContext.Provider, { value, children: Wrapper ? /* @__PURE__ */ jsx(
@@ -621,6 +622,9 @@ function useResolvedChat(explicit) {
621
622
  function useCodeCopy() {
622
623
  return useOptionalChatContext()?.codeCopy;
623
624
  }
625
+ function useLinkComponent() {
626
+ return useOptionalChatContext()?.linkComponent;
627
+ }
624
628
  function useResolvedClassNames(explicit) {
625
629
  const ctx = useOptionalChatContext();
626
630
  return useMemo(() => ({ ...ctx?.classNames, ...explicit }), [ctx?.classNames, explicit]);
@@ -633,11 +637,13 @@ var ChevronDown = icon(ChevronDown$1);
633
637
  var ArrowUp = icon(ArrowUp$1);
634
638
  var Paperclip = icon(Paperclip$1);
635
639
  var FileIcon = icon(File);
640
+ var FileType = icon(FileType2);
636
641
  var FileAudio = icon(FileAudio$1);
637
642
  var FileVideo = icon(FileVideo$1);
638
643
  var Wrench = icon(Wrench$1);
639
644
  var BookOpen = icon(BookOpen$1);
640
645
  var Globe = icon(Globe$1);
646
+ var LinkIcon = icon(Link);
641
647
  var Brain = icon(Brain$1);
642
648
  var Box = icon(Box$1);
643
649
  var Plus = icon(Plus$1);
@@ -840,7 +846,16 @@ function ChatLauncher({
840
846
  function markFor(type) {
841
847
  if (type.startsWith("audio/")) return FileAudio;
842
848
  if (type.startsWith("video/")) return FileVideo;
843
- return FileIcon;
849
+ return FileType;
850
+ }
851
+ function formatSize(bytes) {
852
+ return bytes >= 1024 * 1024 ? `${Math.round(bytes / (1024 * 1024) * 10) / 10}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`;
853
+ }
854
+ function kindOf(file) {
855
+ const dot = file.name.lastIndexOf(".");
856
+ if (dot > 0 && dot < file.name.length - 1) return file.name.slice(dot + 1);
857
+ const subtype = file.type.split("/")[1];
858
+ return subtype || "file";
844
859
  }
845
860
  function Item({ file, onRemove }) {
846
861
  const isImage = file.type.startsWith("image/");
@@ -855,15 +870,19 @@ function Item({ file, onRemove }) {
855
870
  };
856
871
  }, [file, isImage]);
857
872
  const Mark = markFor(file.type);
858
- return /* @__PURE__ */ jsxs("div", { className: "agno-file", children: [
859
- /* @__PURE__ */ jsx("button", { type: "button", className: "agno-file__remove", onClick: onRemove, "aria-label": `Remove ${file.name}`, children: /* @__PURE__ */ jsx(Close, { size: 12 }) }),
860
- isImage && url ? /* @__PURE__ */ jsx("img", { className: "agno-file__thumb", src: url, alt: file.name }) : /* @__PURE__ */ jsxs("div", { className: "agno-file__card", children: [
873
+ return /* @__PURE__ */ jsxs("div", { className: cx("agno-file", isImage && url ? "agno-file--image" : "agno-file--card"), children: [
874
+ isImage && url ? /* @__PURE__ */ jsx("img", { className: "agno-file__thumb", src: url, alt: file.name }) : /* @__PURE__ */ jsxs(Fragment, { children: [
861
875
  /* @__PURE__ */ jsx("span", { className: "agno-file__mark", "aria-hidden": true, children: /* @__PURE__ */ jsx(Mark, { size: 16 }) }),
862
876
  /* @__PURE__ */ jsxs("span", { className: "agno-file__meta", children: [
863
877
  /* @__PURE__ */ jsx("span", { className: "agno-file__name", title: file.name, children: file.name }),
864
- /* @__PURE__ */ jsx("span", { className: "agno-file__type", children: file.type || "file" })
878
+ /* @__PURE__ */ jsxs("span", { className: "agno-file__type", children: [
879
+ kindOf(file),
880
+ " \u2022 ",
881
+ formatSize(file.size)
882
+ ] })
865
883
  ] })
866
- ] })
884
+ ] }),
885
+ /* @__PURE__ */ jsx("button", { type: "button", className: "agno-file__remove", onClick: onRemove, "aria-label": `Remove ${file.name}`, children: /* @__PURE__ */ jsx(Close, { size: 12 }) })
867
886
  ] });
868
887
  }
869
888
  function FilePreview({ files, onRemove, onClear, className }) {
@@ -873,20 +892,51 @@ function FilePreview({ files, onRemove, onClear, className }) {
873
892
  /* @__PURE__ */ jsxs("span", { className: "agno-files__count", children: [
874
893
  files.length,
875
894
  " ",
876
- files.length > 1 ? "files" : "file",
877
- " attached"
895
+ files.length > 1 ? "attachments" : "attachment"
878
896
  ] }),
879
897
  onClear && /* @__PURE__ */ jsxs("button", { type: "button", className: "agno-files__clear", onClick: onClear, children: [
880
- /* @__PURE__ */ jsx(Close, { size: 12 }),
881
- "clear ",
882
- files.length > 1 ? "all" : ""
898
+ /* @__PURE__ */ jsx(Close, { size: 14 }),
899
+ "clear all"
883
900
  ] })
884
901
  ] }),
885
- /* @__PURE__ */ jsx("div", { className: "agno-files__row", children: files.map((file, i) => /* @__PURE__ */ jsx(Item, { file, onRemove: () => onRemove(i) }, `${file.name}-${file.lastModified}-${i}`)) })
902
+ /* @__PURE__ */ jsx("div", { className: "agno-files__strip", children: /* @__PURE__ */ jsx("div", { className: "agno-files__row", children: files.map((file, i) => /* @__PURE__ */ jsx(Item, { file, onRemove: () => onRemove(i) }, `${file.name}-${file.lastModified}-${i}`)) }) })
886
903
  ] });
887
904
  }
888
- var MIN_HEIGHT = 31;
889
- var MAX_HEIGHT = 213;
905
+ var BOUNDS = {
906
+ dock: { min: 31, max: 213 },
907
+ compact: { min: 24, max: 120 }
908
+ };
909
+ function fileAccepted(file, accept) {
910
+ if (!accept) return true;
911
+ const name = file.name.toLowerCase();
912
+ const type = file.type.toLowerCase();
913
+ return accept.split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean).some((rule) => {
914
+ if (rule.startsWith(".")) return name.endsWith(rule);
915
+ if (rule.endsWith("/*")) return type.startsWith(rule.slice(0, -1));
916
+ return type === rule;
917
+ });
918
+ }
919
+ function admitFiles(current, incoming, limits) {
920
+ const { accept, maxFiles, maxFileSize, maxTotalSize } = limits;
921
+ const files = [...current];
922
+ let total = current.reduce((sum, f) => sum + f.size, 0);
923
+ let error = null;
924
+ const refuse = (reason) => {
925
+ error ?? (error = reason);
926
+ };
927
+ for (const file of incoming) {
928
+ if (!fileAccepted(file, accept)) refuse("Cannot add this file type");
929
+ else if (maxFileSize != null && file.size > maxFileSize) refuse(`Cannot add files over ${formatSize(maxFileSize)}`);
930
+ else if (maxFiles != null && files.length >= maxFiles) refuse(`Cannot add more than ${maxFiles} attachments`);
931
+ else if (maxTotalSize != null && total + file.size > maxTotalSize) refuse("Cannot add more files");
932
+ else {
933
+ files.push(file);
934
+ total += file.size;
935
+ }
936
+ }
937
+ return { files, error };
938
+ }
939
+ var hasFiles = (e) => Array.from(e.dataTransfer.types).includes("Files");
890
940
  function ChatInput({
891
941
  onSend,
892
942
  onStop,
@@ -894,6 +944,19 @@ function ChatInput({
894
944
  busy,
895
945
  placeholder,
896
946
  allowFiles = false,
947
+ compact = false,
948
+ accept,
949
+ maxFiles,
950
+ maxFileSize,
951
+ maxTotalSize,
952
+ value: controlled,
953
+ onValueChange,
954
+ textareaRef: externalRef,
955
+ textareaProps,
956
+ renderTextarea,
957
+ above,
958
+ below,
959
+ dropLabel = "Drop files to attach",
897
960
  leading,
898
961
  trailing,
899
962
  className,
@@ -901,109 +964,213 @@ function ChatInput({
901
964
  }) {
902
965
  const ctx = useOptionalChatContext();
903
966
  const cn = useResolvedClassNames(classNames);
904
- const [value, setValue] = useState("");
905
- const [files, setFiles] = useState([]);
967
+ const [internal, setInternal] = useState("");
968
+ const value = controlled ?? internal;
969
+ const setValue = (next) => {
970
+ if (controlled === void 0) setInternal(next);
971
+ onValueChange?.(next);
972
+ };
973
+ const [files, setFilesState] = useState([]);
974
+ const filesRef = useRef([]);
975
+ const setFiles = (next) => {
976
+ filesRef.current = next;
977
+ setFilesState(next);
978
+ };
979
+ const [fileError, setFileError] = useState(null);
980
+ const [dragging, setDragging] = useState(false);
981
+ const dragDepth = useRef(0);
906
982
  const fileRef = useRef(null);
907
983
  const textareaRef = useRef(null);
984
+ const setTextareaRef = useCallback(
985
+ (el2) => {
986
+ textareaRef.current = el2;
987
+ if (typeof externalRef === "function") externalRef(el2);
988
+ else if (externalRef) externalRef.current = el2;
989
+ },
990
+ [externalRef]
991
+ );
908
992
  const adjustHeight = useCallback(() => {
909
993
  const el2 = textareaRef.current;
910
- if (!el2) return;
911
- el2.style.height = `${MIN_HEIGHT}px`;
912
- el2.style.height = `${Math.min(Math.max(el2.scrollHeight, MIN_HEIGHT), MAX_HEIGHT)}px`;
913
- }, []);
994
+ if (!el2 || renderTextarea) return;
995
+ const { min, max } = compact ? BOUNDS.compact : BOUNDS.dock;
996
+ el2.style.height = `${min}px`;
997
+ el2.style.height = `${Math.min(Math.max(el2.scrollHeight, min), max)}px`;
998
+ }, [renderTextarea, compact]);
914
999
  useEffect(adjustHeight, [value, adjustHeight]);
915
1000
  const send = onSend ?? ((message, attached) => void ctx?.chat.sendMessage(message, attached ? { files: attached } : void 0));
916
1001
  const stop = onStop ?? (ctx ? () => void ctx.chat.cancel() : void 0);
917
1002
  const isBusy = busy ?? ctx?.chat.isStreaming ?? false;
918
1003
  const isDisabled = disabled ?? ctx?.chat.isPaused ?? false;
919
- const submit = () => {
1004
+ const limits = { accept, maxFiles, maxFileSize, maxTotalSize };
1005
+ const addFiles = (incoming) => {
1006
+ if (!incoming.length) return;
1007
+ const next = admitFiles(filesRef.current, incoming, limits);
1008
+ setFiles(next.files);
1009
+ setFileError(next.error);
1010
+ if (fileRef.current) fileRef.current.value = "";
1011
+ };
1012
+ const submit = async () => {
920
1013
  const text = value.trim();
921
1014
  if (!text && files.length === 0) return;
922
- send(text, files.length ? files : void 0);
1015
+ const sent = files.length ? files : void 0;
1016
+ const result = await send(text, sent);
1017
+ if (result === false) return;
923
1018
  setValue("");
924
1019
  setFiles([]);
1020
+ setFileError(null);
925
1021
  };
926
1022
  const canSend = !isDisabled && !isBusy && (Boolean(value.trim()) || files.length > 0);
927
- return /* @__PURE__ */ jsx("div", { className: cx("agno-input", cn.input, className), children: /* @__PURE__ */ jsxs("div", { className: "agno-input__dock", children: [
1023
+ const onKeyDown = (e) => {
1024
+ textareaProps?.onKeyDown?.(e);
1025
+ if (e.defaultPrevented) return;
1026
+ if (e.key === "Enter" && !e.nativeEvent.isComposing && !e.shiftKey) {
1027
+ e.preventDefault();
1028
+ if (canSend) void submit();
1029
+ }
1030
+ };
1031
+ const onDragEnter = (e) => {
1032
+ if (!allowFiles || !hasFiles(e)) return;
1033
+ e.preventDefault();
1034
+ dragDepth.current += 1;
1035
+ setDragging(true);
1036
+ };
1037
+ const onDragOver = (e) => {
1038
+ if (!allowFiles || !hasFiles(e)) return;
1039
+ e.preventDefault();
1040
+ e.dataTransfer.dropEffect = "copy";
1041
+ };
1042
+ const onDragLeave = (e) => {
1043
+ if (!allowFiles || dragDepth.current === 0) return;
1044
+ e.preventDefault();
1045
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
1046
+ if (dragDepth.current === 0) setDragging(false);
1047
+ };
1048
+ const onDrop = (e) => {
1049
+ if (!allowFiles || !hasFiles(e)) return;
1050
+ e.preventDefault();
1051
+ dragDepth.current = 0;
1052
+ setDragging(false);
1053
+ addFiles(Array.from(e.dataTransfer.files));
1054
+ textareaRef.current?.focus();
1055
+ };
1056
+ const fieldProps = {
1057
+ rows: 1,
1058
+ ...textareaProps,
1059
+ ref: setTextareaRef,
1060
+ className: cx("agno-input__textarea", cn.textarea, textareaProps?.className),
1061
+ value,
1062
+ placeholder: placeholder ?? (isBusy ? "Running..." : "Ask anything..."),
1063
+ disabled: isDisabled,
1064
+ onChange: (e) => {
1065
+ setValue(e.target.value);
1066
+ textareaProps?.onChange?.(e);
1067
+ },
1068
+ onKeyDown
1069
+ };
1070
+ const iconSize = compact ? 14 : 16;
1071
+ const field = /* @__PURE__ */ jsx("div", { className: "agno-input__field", children: renderTextarea ? renderTextarea(fieldProps) : /* @__PURE__ */ jsx("textarea", { ...fieldProps }) });
1072
+ const attach = allowFiles && /* @__PURE__ */ jsxs(Fragment, { children: [
928
1073
  /* @__PURE__ */ jsx(
929
- FilePreview,
1074
+ "button",
930
1075
  {
931
- files,
932
- onRemove: (i) => setFiles((prev) => prev.filter((_, j) => j !== i)),
933
- onClear: () => setFiles([]),
934
- className: cn.files
1076
+ type: "button",
1077
+ className: cx("agno-btn", compact ? "agno-btn--ghost" : "agno-btn--outline", "agno-btn--icon", cn.attachButton),
1078
+ onClick: () => fileRef.current?.click(),
1079
+ disabled: isDisabled,
1080
+ "aria-label": files.length ? `Attach files; ${files.length} attached` : "Attach files",
1081
+ title: files.length ? files.map((f) => f.name).join(", ") : void 0,
1082
+ children: /* @__PURE__ */ jsx(Paperclip, { size: iconSize })
935
1083
  }
936
1084
  ),
937
- /* @__PURE__ */ jsx("div", { className: "agno-input__field", children: /* @__PURE__ */ jsx(
938
- "textarea",
1085
+ /* @__PURE__ */ jsx(
1086
+ "input",
939
1087
  {
940
- ref: textareaRef,
941
- className: cx("agno-input__textarea", cn.textarea),
942
- value,
943
- placeholder: placeholder ?? (isBusy ? "Running..." : "Ask anything..."),
944
- disabled: isDisabled,
945
- rows: 1,
946
- onChange: (e) => setValue(e.target.value),
947
- onKeyDown: (e) => {
948
- if (e.key === "Enter" && !e.nativeEvent.isComposing && !e.shiftKey) {
949
- e.preventDefault();
950
- if (canSend) submit();
951
- }
952
- }
1088
+ ref: fileRef,
1089
+ type: "file",
1090
+ multiple: true,
1091
+ hidden: true,
1092
+ accept,
1093
+ onChange: (e) => addFiles(Array.from(e.target.files ?? []))
953
1094
  }
954
- ) }),
955
- /* @__PURE__ */ jsxs("div", { className: cx("agno-input__row", cn.inputRow), children: [
956
- /* @__PURE__ */ jsxs("div", { className: "agno-input__actions", children: [
957
- leading,
958
- allowFiles && /* @__PURE__ */ jsxs(Fragment, { children: [
959
- /* @__PURE__ */ jsx(
960
- "button",
961
- {
962
- type: "button",
963
- className: cx("agno-btn", "agno-btn--outline", "agno-btn--icon", cn.attachButton),
964
- onClick: () => fileRef.current?.click(),
965
- disabled: isDisabled,
966
- "aria-label": "Attach files",
967
- children: /* @__PURE__ */ jsx(Paperclip, { size: 16 })
968
- }
969
- ),
970
- /* @__PURE__ */ jsx(
971
- "input",
972
- {
973
- ref: fileRef,
974
- type: "file",
975
- multiple: true,
976
- hidden: true,
977
- onChange: (e) => setFiles((prev) => [...prev, ...Array.from(e.target.files ?? [])])
978
- }
979
- )
980
- ] })
981
- ] }),
982
- /* @__PURE__ */ jsxs("div", { className: "agno-input__actions", children: [
983
- trailing,
984
- isBusy && stop ? /* @__PURE__ */ jsx(
985
- "button",
986
- {
987
- type: "button",
988
- className: cx("agno-btn", "agno-btn--destructive", "agno-btn--icon", cn.stopButton),
989
- onClick: stop,
990
- "aria-label": "Stop",
991
- children: /* @__PURE__ */ jsx(Stop, { size: 16 })
992
- }
993
- ) : /* @__PURE__ */ jsx(
994
- "button",
1095
+ )
1096
+ ] });
1097
+ const primary = isBusy && stop ? /* @__PURE__ */ jsx(
1098
+ "button",
1099
+ {
1100
+ type: "button",
1101
+ className: cx("agno-btn", "agno-btn--destructive", "agno-btn--icon", cn.stopButton),
1102
+ onClick: stop,
1103
+ "aria-label": "Stop",
1104
+ children: /* @__PURE__ */ jsx(Stop, { size: iconSize })
1105
+ }
1106
+ ) : /* @__PURE__ */ jsx(
1107
+ "button",
1108
+ {
1109
+ type: "button",
1110
+ className: cx("agno-btn", "agno-btn--icon", cn.sendButton),
1111
+ onClick: () => void submit(),
1112
+ disabled: !canSend,
1113
+ "aria-label": "Send",
1114
+ children: /* @__PURE__ */ jsx(ArrowUp, { size: iconSize })
1115
+ }
1116
+ );
1117
+ return /* @__PURE__ */ jsx(
1118
+ "div",
1119
+ {
1120
+ className: cx("agno-input", compact && "agno-input--compact", cn.input, className),
1121
+ "data-dragging": dragging ? "true" : void 0,
1122
+ onDragEnter,
1123
+ onDragOver,
1124
+ onDragLeave,
1125
+ onDrop,
1126
+ children: /* @__PURE__ */ jsxs("div", { className: "agno-input__dock", children: [
1127
+ dragging && /* @__PURE__ */ jsxs("div", { className: cx("agno-input__drop", cn.dropOverlay), "aria-hidden": true, children: [
1128
+ /* @__PURE__ */ jsx(Paperclip, { size: 16 }),
1129
+ /* @__PURE__ */ jsx("span", { children: dropLabel })
1130
+ ] }),
1131
+ above,
1132
+ /* @__PURE__ */ jsx(
1133
+ FilePreview,
995
1134
  {
996
- type: "button",
997
- className: cx("agno-btn", "agno-btn--icon", cn.sendButton),
998
- onClick: submit,
999
- disabled: !canSend,
1000
- "aria-label": "Send",
1001
- children: /* @__PURE__ */ jsx(ArrowUp, { size: 16 })
1135
+ files,
1136
+ onRemove: (i) => {
1137
+ setFiles(filesRef.current.filter((_, j) => j !== i));
1138
+ setFileError(null);
1139
+ },
1140
+ onClear: () => {
1141
+ setFiles([]);
1142
+ setFileError(null);
1143
+ },
1144
+ className: cn.files
1002
1145
  }
1003
- )
1146
+ ),
1147
+ fileError && /* @__PURE__ */ jsx("p", { className: cx("agno-input__error", cn.fileError), role: "alert", children: fileError }),
1148
+ compact ? (
1149
+ // One line: the field, then every control beside it.
1150
+ /* @__PURE__ */ jsxs("div", { className: cx("agno-input__row", cn.inputRow), children: [
1151
+ leading,
1152
+ field,
1153
+ /* @__PURE__ */ jsxs("div", { className: "agno-input__actions", children: [
1154
+ attach,
1155
+ trailing,
1156
+ primary
1157
+ ] })
1158
+ ] })
1159
+ ) : field,
1160
+ below,
1161
+ !compact && /* @__PURE__ */ jsxs("div", { className: cx("agno-input__row", cn.inputRow), children: [
1162
+ /* @__PURE__ */ jsxs("div", { className: "agno-input__actions", children: [
1163
+ leading,
1164
+ attach
1165
+ ] }),
1166
+ /* @__PURE__ */ jsxs("div", { className: "agno-input__actions", children: [
1167
+ trailing,
1168
+ primary
1169
+ ] })
1170
+ ] })
1004
1171
  ] })
1005
- ] })
1006
- ] }) });
1172
+ }
1173
+ );
1007
1174
  }
1008
1175
  function normalizeFollowups(items) {
1009
1176
  if (!items?.length) return [];
@@ -1526,20 +1693,15 @@ var PREVIEW_WIDTH = 320;
1526
1693
  var GAP = 8;
1527
1694
  var OPEN_DELAY = 140;
1528
1695
  var CLOSE_DELAY = 120;
1529
- function usePlacement(anchor, open) {
1696
+ function usePlacement(anchor, open, { width = PREVIEW_WIDTH, align = "center" } = {}) {
1530
1697
  const [placement, setPlacement] = useState(null);
1531
1698
  useIsomorphicLayoutEffect(() => {
1532
1699
  if (!open || !anchor) return;
1533
1700
  const place = () => {
1534
1701
  const rect = anchor.getBoundingClientRect();
1535
1702
  const side = rect.top > 220 ? "top" : "bottom";
1536
- const left = Math.max(
1537
- GAP,
1538
- Math.min(
1539
- rect.left + rect.width / 2 - PREVIEW_WIDTH / 2,
1540
- window.innerWidth - PREVIEW_WIDTH - GAP
1541
- )
1542
- );
1703
+ const wanted = align === "center" ? rect.left + rect.width / 2 - width / 2 : rect.left;
1704
+ const left = Math.max(GAP, Math.min(wanted, window.innerWidth - width - GAP));
1543
1705
  setPlacement({
1544
1706
  top: side === "top" ? rect.top - GAP : rect.bottom + GAP,
1545
1707
  left,
@@ -1553,7 +1715,7 @@ function usePlacement(anchor, open) {
1553
1715
  window.removeEventListener("scroll", place, true);
1554
1716
  window.removeEventListener("resize", place);
1555
1717
  };
1556
- }, [anchor, open]);
1718
+ }, [anchor, open, width, align]);
1557
1719
  return placement;
1558
1720
  }
1559
1721
  function HoverPreview({
@@ -1655,16 +1817,20 @@ function labelOf(children) {
1655
1817
  }
1656
1818
  function CitationMarker({
1657
1819
  source,
1658
- className
1820
+ className,
1821
+ Link
1659
1822
  }) {
1660
- return /* @__PURE__ */ jsx(HoverPreview, { source, className: "agno-cite__wrap", children: /* @__PURE__ */ jsx(
1823
+ const props = {
1824
+ className: cx("agno-cite", className),
1825
+ href: source.url ?? `#${source.anchorId}`,
1826
+ "aria-label": `Source ${source.index}: ${source.title}`
1827
+ };
1828
+ return /* @__PURE__ */ jsx(HoverPreview, { source, className: "agno-cite__wrap", children: Link && source.url ? /* @__PURE__ */ jsx(Link, { ...props, children: source.index }) : /* @__PURE__ */ jsx(
1661
1829
  "a",
1662
1830
  {
1663
- className: cx("agno-cite", className),
1664
- href: source.url ?? `#${source.anchorId}`,
1831
+ ...props,
1665
1832
  target: source.url ? "_blank" : void 0,
1666
1833
  rel: source.url ? "noreferrer noopener" : void 0,
1667
- "aria-label": `Source ${source.index}: ${source.title}`,
1668
1834
  children: source.index
1669
1835
  }
1670
1836
  ) });
@@ -1763,8 +1929,8 @@ function elementsFor(ctx) {
1763
1929
  ...rest
1764
1930
  }) {
1765
1931
  const cited = citationForLink(labelOf(children), href, ctx);
1766
- if (cited) return /* @__PURE__ */ jsx(CitationMarker, { source: cited, className: ctx.markerClass });
1767
- const link = /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noreferrer noopener", ...rest, children });
1932
+ if (cited) return /* @__PURE__ */ jsx(CitationMarker, { source: cited, className: ctx.markerClass, Link: ctx.Link });
1933
+ const link = ctx.Link && href ? /* @__PURE__ */ jsx(ctx.Link, { href, ...rest, children }) : /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noreferrer noopener", ...rest, children });
1768
1934
  const source = sourceForUrl(href, ctx.sources);
1769
1935
  if (!href || !/^https?:\/\//i.test(href) || !source && !ctx.canResolve) return link;
1770
1936
  return /* @__PURE__ */ jsx(
@@ -1830,23 +1996,27 @@ function Markdown({
1830
1996
  sources,
1831
1997
  streaming,
1832
1998
  codeCopy,
1999
+ linkComponent,
1833
2000
  options
1834
2001
  }) {
1835
2002
  const fromMessage = useSources();
1836
2003
  const cn = useResolvedClassNames();
1837
2004
  const canResolve = Boolean(useLinkPreviewResolver());
1838
2005
  const fromProvider = useCodeCopy();
2006
+ const providerLink = useLinkComponent();
1839
2007
  const cited = sources ?? fromMessage;
1840
2008
  const copy = codeCopy ?? fromProvider ?? true;
2009
+ const Link = linkComponent ?? providerLink;
1841
2010
  const components = useMemo(
1842
2011
  () => elementsFor({
1843
2012
  sources: cited,
1844
2013
  canResolve,
1845
2014
  markerClass: cn.citationMarker,
1846
2015
  copyClass: cn.copyButton,
1847
- codeCopy: copy
2016
+ codeCopy: copy,
2017
+ Link
1848
2018
  }),
1849
- [cited, canResolve, cn.citationMarker, cn.copyButton, copy]
2019
+ [cited, canResolve, cn.citationMarker, cn.copyButton, copy, Link]
1850
2020
  );
1851
2021
  const text = useMemo(() => linkCitations(content, cited), [content, cited]);
1852
2022
  return /* @__PURE__ */ jsx(
@@ -2257,12 +2427,25 @@ function SourceCard({
2257
2427
  className
2258
2428
  }) {
2259
2429
  const cn = useResolvedClassNames();
2430
+ const Link = useLinkComponent();
2260
2431
  const { preview } = useLinkPreview(source.url, false);
2261
2432
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2262
- /* @__PURE__ */ jsx(Favicon, { url: preview?.favicon ?? source.url, kind: source.kind }),
2263
- /* @__PURE__ */ jsx("span", { className: "agno-source__title", children: preview?.title ?? source.title })
2433
+ /* @__PURE__ */ jsx(
2434
+ Favicon,
2435
+ {
2436
+ className: "agno-source__icon",
2437
+ url: preview?.favicon ?? source.url,
2438
+ kind: source.kind,
2439
+ size: 24
2440
+ }
2441
+ ),
2442
+ /* @__PURE__ */ jsx("span", { className: "agno-source__title", children: preview?.title ?? source.title }),
2443
+ source.url && /* @__PURE__ */ jsx(LinkIcon, { className: "agno-source__link" })
2264
2444
  ] });
2265
2445
  const classes = cx("agno-source", `agno-source--${source.kind}`, cn.sourceCard, className);
2446
+ if (source.url && Link) {
2447
+ return /* @__PURE__ */ jsx(Link, { id: source.anchorId, className: classes, href: source.url, title: source.url, children: body });
2448
+ }
2266
2449
  return source.url ? /* @__PURE__ */ jsx(
2267
2450
  "a",
2268
2451
  {
@@ -2276,33 +2459,118 @@ function SourceCard({
2276
2459
  }
2277
2460
  ) : /* @__PURE__ */ jsx("div", { id: source.anchorId, className: classes, children: body });
2278
2461
  }
2462
+ var LIST_WIDTH = 240;
2463
+ function SourcesOverflow({ sources }) {
2464
+ const [open, setOpen] = useState(false);
2465
+ const [anchor, setAnchor] = useState(null);
2466
+ const timer = useRef(null);
2467
+ const id = useId();
2468
+ const placement = usePlacement(anchor, open, { width: LIST_WIDTH, align: "start" });
2469
+ const schedule = useCallback((next) => {
2470
+ if (timer.current) clearTimeout(timer.current);
2471
+ timer.current = setTimeout(() => setOpen(next), next ? OPEN_DELAY : CLOSE_DELAY);
2472
+ }, []);
2473
+ useEffect(
2474
+ () => () => {
2475
+ if (timer.current) clearTimeout(timer.current);
2476
+ },
2477
+ []
2478
+ );
2479
+ useEffect(() => {
2480
+ if (!open) return;
2481
+ const onKey = (e) => {
2482
+ if (e.key === "Escape") setOpen(false);
2483
+ };
2484
+ document.addEventListener("keydown", onKey);
2485
+ return () => document.removeEventListener("keydown", onKey);
2486
+ }, [open]);
2487
+ const [mounted, setMounted] = useState(false);
2488
+ useEffect(() => setMounted(true), []);
2489
+ const light = anchor?.closest(".agno-light") ? "agno-light" : void 0;
2490
+ return /* @__PURE__ */ jsxs(
2491
+ "span",
2492
+ {
2493
+ className: "agno-sources__overflow",
2494
+ ref: setAnchor,
2495
+ onMouseEnter: () => schedule(true),
2496
+ onMouseLeave: () => schedule(false),
2497
+ children: [
2498
+ /* @__PURE__ */ jsxs(
2499
+ "button",
2500
+ {
2501
+ type: "button",
2502
+ className: "agno-sources__more",
2503
+ "aria-label": `${sources.length} more sources`,
2504
+ "aria-expanded": open,
2505
+ "aria-controls": open ? id : void 0,
2506
+ onClick: () => setOpen((v) => !v),
2507
+ onFocus: () => setOpen(true),
2508
+ onBlur: () => schedule(false),
2509
+ children: [
2510
+ /* @__PURE__ */ jsx("span", { className: "agno-sources__stack", "aria-hidden": true, children: sources.slice(0, 3).map((source) => /* @__PURE__ */ jsx(
2511
+ Favicon,
2512
+ {
2513
+ className: "agno-sources__stack-icon",
2514
+ url: source.url,
2515
+ kind: source.kind,
2516
+ size: 24
2517
+ },
2518
+ source.anchorId
2519
+ )) }),
2520
+ /* @__PURE__ */ jsxs("span", { className: "agno-sources__more-label", children: [
2521
+ "+",
2522
+ sources.length,
2523
+ " more"
2524
+ ] })
2525
+ ]
2526
+ }
2527
+ ),
2528
+ mounted && open && placement && createPortal(
2529
+ /* @__PURE__ */ jsx(
2530
+ "div",
2531
+ {
2532
+ id,
2533
+ role: "group",
2534
+ "aria-label": "More sources",
2535
+ className: cx("agno-sources__list", `agno-sources__list--${placement.side}`, light),
2536
+ style: { top: placement.top, left: placement.left, width: LIST_WIDTH },
2537
+ onMouseEnter: () => schedule(true),
2538
+ onMouseLeave: () => schedule(false),
2539
+ onFocus: () => schedule(true),
2540
+ onBlur: () => schedule(false),
2541
+ children: sources.map((source) => /* @__PURE__ */ jsx(SourceCard, { source, className: "agno-source--row" }, source.anchorId))
2542
+ }
2543
+ ),
2544
+ document.body
2545
+ )
2546
+ ]
2547
+ }
2548
+ );
2549
+ }
2279
2550
  function Citations({
2280
2551
  references,
2281
2552
  citations,
2282
2553
  sources: given,
2283
- title = "Sources",
2284
- max = 4,
2554
+ title = "Sources used",
2555
+ max = 3,
2285
2556
  className,
2286
2557
  classNames
2287
2558
  }) {
2288
2559
  const cn = useResolvedClassNames(classNames);
2289
2560
  const uid = useId();
2290
- const [expanded, setExpanded] = useState(false);
2291
2561
  const sources = useMemo(
2292
2562
  () => given ?? collectSources(references, citations, uid),
2293
2563
  [given, references, citations, uid]
2294
2564
  );
2295
2565
  if (sources.length === 0) return null;
2296
- const limit = max > 0 && !expanded ? max : sources.length;
2566
+ const limit = max > 0 ? max : sources.length;
2297
2567
  const visible = sources.slice(0, limit);
2298
- const hidden = sources.length - visible.length;
2568
+ const hidden = sources.slice(limit);
2299
2569
  return /* @__PURE__ */ jsxs("section", { className: cx("agno-sources", cn.citations, className), "aria-label": title, children: [
2300
2570
  /* @__PURE__ */ jsx("div", { className: "agno-sources__head", children: title }),
2301
- /* @__PURE__ */ jsx("div", { className: "agno-sources__grid", children: visible.map((source) => /* @__PURE__ */ jsx(SourceCard, { source }, source.anchorId)) }),
2302
- hidden > 0 && /* @__PURE__ */ jsxs("button", { type: "button", className: "agno-sources__more", onClick: () => setExpanded(true), children: [
2303
- "Show ",
2304
- hidden,
2305
- " more"
2571
+ /* @__PURE__ */ jsxs("div", { className: "agno-sources__grid", children: [
2572
+ visible.map((source) => /* @__PURE__ */ jsx(SourceCard, { source }, source.anchorId)),
2573
+ hidden.length > 0 && /* @__PURE__ */ jsx(SourcesOverflow, { sources: hidden })
2306
2574
  ] })
2307
2575
  ] });
2308
2576
  }
@@ -2518,6 +2786,7 @@ function ChatWindow({
2518
2786
  userInitials,
2519
2787
  emptyState,
2520
2788
  allowFiles,
2789
+ compactInput,
2521
2790
  disabled,
2522
2791
  renderMarkdown,
2523
2792
  showErrors,
@@ -2526,6 +2795,7 @@ function ChatWindow({
2526
2795
  hideFollowups,
2527
2796
  onFollowup,
2528
2797
  followupsTitle,
2798
+ renderMessage,
2529
2799
  header,
2530
2800
  composer,
2531
2801
  className,
@@ -2552,6 +2822,7 @@ function ChatWindow({
2552
2822
  renderMarkdown: markdown,
2553
2823
  classNames: cn,
2554
2824
  emptyState,
2825
+ renderMessage,
2555
2826
  footer: pausedMessage ? /* @__PURE__ */ jsx(
2556
2827
  HumanInput,
2557
2828
  {
@@ -2594,6 +2865,7 @@ function ChatWindow({
2594
2865
  disabled: disabled || chat.isPaused,
2595
2866
  placeholder,
2596
2867
  allowFiles,
2868
+ compact: compactInput,
2597
2869
  classNames: cn
2598
2870
  }
2599
2871
  )
@@ -2982,6 +3254,7 @@ function AgnoChat(props) {
2982
3254
  chat,
2983
3255
  placeholder: props.placeholder,
2984
3256
  allowFiles: props.allowFiles ?? true,
3257
+ compactInput: props.compactInput,
2985
3258
  disabled: !entity,
2986
3259
  renderMarkdown: props.renderMarkdown,
2987
3260
  showErrors: props.showErrors,
@@ -2999,6 +3272,7 @@ function AgnoChat(props) {
2999
3272
  disabled: !entity || chat.isPaused,
3000
3273
  placeholder: props.placeholder,
3001
3274
  allowFiles: props.allowFiles ?? true,
3275
+ compact: props.compactInput,
3002
3276
  classNames: cn,
3003
3277
  trailing: showPicker ? /* @__PURE__ */ jsx(
3004
3278
  EntitySelector,
@@ -3027,6 +3301,7 @@ function AgnoChat(props) {
3027
3301
  renderMarkdown: props.renderMarkdown,
3028
3302
  resolveLinkPreview: props.resolveLinkPreview,
3029
3303
  codeCopy: props.codeCopy,
3304
+ linkComponent: props.linkComponent,
3030
3305
  as: false,
3031
3306
  children: root
3032
3307
  }
@@ -3221,6 +3496,6 @@ function EventLog({
3221
3496
  ] });
3222
3497
  }
3223
3498
 
3224
- export { AgnoChat, AgnoMark, ArrowUp, BehindTheScenes, BookOpen, Box, Brain, ChatBubble, ChatInput, ChatLauncher, ChatProvider, ChatWindow, Check, ChevronDown, Citations, Close, Copy, EntityBadge, EntitySelector, EventLog, Favicon, FileAudio, FileIcon, FilePreview, FileVideo, Followups, Globe, GridLoader, HoverPreview, HumanInput, LinkPreviewCard, Markdown, MemberResponses, Memory, Message, MessageList, Multimedia, Paperclip, Plus, Pulse, QuickPrompts, Reasoning, Rerun, SessionList, SourceCard, SourcesProvider, Spinner, StatusIndicator, Stop, TeamGlyph, ToolCalls, Trash, WorkflowSteps, Wrench, behindTheScenesItems, behindTheScenesLabel, collectSources, cx, displayUrl, faviconUrl, getProviderIcon, hasBehindTheScenes, linkCitations, normalizeFollowups, quickPromptLabel, quickPromptText, sourceHost, sourcesFromMarkdown, useAgnoChat, useChatContext, useLinkPreview, useOptionalChatContext, useResolvedChat, useResolvedClassNames, useSources, withoutSourcesLine, writeClipboard };
3225
- //# sourceMappingURL=chunk-MUOMZG4V.js.map
3226
- //# sourceMappingURL=chunk-MUOMZG4V.js.map
3499
+ export { AgnoChat, AgnoMark, ArrowUp, BehindTheScenes, BookOpen, Box, Brain, ChatBubble, ChatInput, ChatLauncher, ChatProvider, ChatWindow, Check, ChevronDown, Citations, Close, Copy, EntityBadge, EntitySelector, EventLog, Favicon, FileAudio, FileIcon, FilePreview, FileType, FileVideo, Followups, Globe, GridLoader, HoverPreview, HumanInput, LinkIcon, LinkPreviewCard, Markdown, MemberResponses, Memory, Message, MessageList, Multimedia, Paperclip, Plus, Pulse, QuickPrompts, Reasoning, Rerun, SessionList, SourceCard, SourcesProvider, Spinner, StatusIndicator, Stop, TeamGlyph, ToolCalls, Trash, WorkflowSteps, Wrench, admitFiles, behindTheScenesItems, behindTheScenesLabel, collectSources, cx, displayUrl, faviconUrl, fileAccepted, getProviderIcon, hasBehindTheScenes, linkCitations, normalizeFollowups, quickPromptLabel, quickPromptText, sourceHost, sourcesFromMarkdown, useAgnoChat, useChatContext, useLinkComponent, useLinkPreview, useOptionalChatContext, useResolvedChat, useResolvedClassNames, useSources, withoutSourcesLine, writeClipboard };
3500
+ //# sourceMappingURL=chunk-BZC2674Z.js.map
3501
+ //# sourceMappingURL=chunk-BZC2674Z.js.map