@agno-hq/chat-react 0.3.0 → 0.3.2

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