lemma-sdk 0.2.31 → 0.2.33
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 +138 -52
- package/dist/browser/lemma-client.js +23 -8
- package/dist/namespaces/desks.d.ts +5 -2
- package/dist/namespaces/desks.js +5 -2
- package/dist/namespaces/files.d.ts +11 -0
- package/dist/namespaces/files.js +12 -0
- package/dist/openapi_client/models/DeskBundleUploadRequest.d.ts +1 -1
- package/dist/openapi_client/services/DesksService.d.ts +4 -4
- package/dist/openapi_client/services/DesksService.js +6 -6
- package/dist/openapi_client/services/PublicDesksService.d.ts +2 -2
- package/dist/openapi_client/services/PublicDesksService.js +3 -3
- package/dist/react/index.d.ts +13 -1
- package/dist/react/index.js +6 -0
- package/dist/react/useAssistantController.js +82 -37
- package/dist/react/useAssistantRuntime.js +8 -4
- package/dist/react/useAssistantSession.js +44 -2
- package/dist/react/useConversationMessages.js +19 -2
- package/dist/react/useCreateRecord.d.ts +33 -3
- package/dist/react/useCreateRecord.js +12 -2
- package/dist/react/useFile.d.ts +18 -0
- package/dist/react/useFile.js +58 -0
- package/dist/react/useFilePreview.d.ts +23 -0
- package/dist/react/useFilePreview.js +76 -0
- package/dist/react/useFileSearch.d.ts +26 -0
- package/dist/react/useFileSearch.js +64 -0
- package/dist/react/useFileTree.d.ts +21 -0
- package/dist/react/useFileTree.js +59 -0
- package/dist/react/useFiles.d.ts +29 -0
- package/dist/react/useFiles.js +90 -0
- package/dist/react/useForeignKeyOptions.d.ts +18 -0
- package/dist/react/useFunctionRun.d.ts +17 -0
- package/dist/react/useJoinedRecords.d.ts +57 -2
- package/dist/react/useJoinedRecords.js +54 -5
- package/dist/react/useRecord.d.ts +16 -0
- package/dist/react/useRecordForm.d.ts +42 -3
- package/dist/react/useRecordForm.js +43 -6
- package/dist/react/useRecords.js +8 -5
- package/dist/react/useReferencingRecords.d.ts +66 -0
- package/dist/react/useReferencingRecords.js +159 -0
- package/dist/react/useRelatedRecords.d.ts +17 -0
- package/dist/react/useReverseRelatedRecords.d.ts +21 -0
- package/dist/react/useUpdateRecord.d.ts +34 -3
- package/dist/react/useUpdateRecord.js +13 -2
- package/dist/types.d.ts +6 -1
- package/package.json +2 -1
|
@@ -541,7 +541,6 @@ function isConversationRunning(status) {
|
|
|
541
541
|
}
|
|
542
542
|
export function useAssistantController({ client, podId, assistantName, assistantId, organizationId, enabled = true, }) {
|
|
543
543
|
const [localError, setLocalError] = useState(null);
|
|
544
|
-
const [messages, setMessages] = useState([]);
|
|
545
544
|
const [conversations, setConversations] = useState([]);
|
|
546
545
|
const [activeConversationId, setActiveConversationId] = useState(null);
|
|
547
546
|
const [availableModels, setAvailableModels] = useState([]);
|
|
@@ -555,10 +554,14 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
555
554
|
const [olderMessagesCursor, setOlderMessagesCursor] = useState(null);
|
|
556
555
|
const activeConversationIdRef = useRef(null);
|
|
557
556
|
const conversationsRef = useRef([]);
|
|
557
|
+
const isStreamingRef = useRef(false);
|
|
558
|
+
const sessionIsStreamingRef = useRef(false);
|
|
558
559
|
const suppressAutoSelectRef = useRef(false);
|
|
559
560
|
const lastAutoLoadedConversationIdRef = useRef(null);
|
|
560
561
|
const loadingConversationIdRef = useRef(null);
|
|
561
562
|
const skipInitialLoadConversationIdsRef = useRef(new Set());
|
|
563
|
+
const loadConversationMessagesRef = useRef(null);
|
|
564
|
+
const resumeIfRunningRef = useRef(null);
|
|
562
565
|
const scope = useMemo(() => ({
|
|
563
566
|
podId: podId ?? null,
|
|
564
567
|
assistantName: assistantName ?? assistantId ?? null,
|
|
@@ -715,12 +718,24 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
715
718
|
setIsLoadingOlderMessages(false);
|
|
716
719
|
}
|
|
717
720
|
}, [isLoadingMessages, isLoadingOlderMessages, mergeMessages, olderMessagesCursor, sessionLoadMessages]);
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
loadConversationMessagesRef.current = loadConversationMessages;
|
|
723
|
+
}, [loadConversationMessages]);
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
resumeIfRunningRef.current = sessionResumeIfRunning;
|
|
726
|
+
}, [sessionResumeIfRunning]);
|
|
718
727
|
useEffect(() => {
|
|
719
728
|
activeConversationIdRef.current = activeConversationId;
|
|
720
729
|
}, [activeConversationId]);
|
|
721
730
|
useEffect(() => {
|
|
722
731
|
conversationsRef.current = conversations;
|
|
723
732
|
}, [conversations]);
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
isStreamingRef.current = isStreaming;
|
|
735
|
+
}, [isStreaming]);
|
|
736
|
+
useEffect(() => {
|
|
737
|
+
sessionIsStreamingRef.current = sessionIsStreaming;
|
|
738
|
+
}, [sessionIsStreaming]);
|
|
724
739
|
useEffect(() => {
|
|
725
740
|
if (!enabled) {
|
|
726
741
|
setAvailableModels([]);
|
|
@@ -738,22 +753,17 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
738
753
|
cancelled = true;
|
|
739
754
|
};
|
|
740
755
|
}, [enabled, loadAvailableModels]);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
setMessages([]);
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
756
|
+
const messages = useMemo(() => {
|
|
757
|
+
if (!activeConversationId)
|
|
758
|
+
return [];
|
|
747
759
|
const normalized = sortMessagesByCreatedAt(runtimeMessages)
|
|
748
|
-
.filter((message) => message.conversation_id ===
|
|
749
|
-
if (normalized.length === 0)
|
|
750
|
-
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
760
|
+
.filter((message) => message.conversation_id === activeConversationId);
|
|
761
|
+
if (normalized.length === 0)
|
|
762
|
+
return [];
|
|
753
763
|
const nextMessages = mapConversationMessages(normalized);
|
|
754
764
|
const pendingText = sessionStreamingText.trim();
|
|
755
765
|
if (pendingText.length > 0) {
|
|
756
|
-
const streamingId = `streaming-${
|
|
766
|
+
const streamingId = `streaming-${activeConversationId}`;
|
|
757
767
|
nextMessages.push({
|
|
758
768
|
id: streamingId,
|
|
759
769
|
role: "assistant",
|
|
@@ -762,8 +772,8 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
762
772
|
parts: [{ id: `${streamingId}-text`, type: "text", text: pendingText }],
|
|
763
773
|
});
|
|
764
774
|
}
|
|
765
|
-
|
|
766
|
-
}, [runtimeMessages, sessionStreamingText]);
|
|
775
|
+
return nextMessages;
|
|
776
|
+
}, [activeConversationId, runtimeMessages, sessionStreamingText]);
|
|
767
777
|
useEffect(() => {
|
|
768
778
|
if (!activeConversationId)
|
|
769
779
|
return;
|
|
@@ -794,7 +804,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
794
804
|
setAvailableModels([]);
|
|
795
805
|
setConversationModelState(null);
|
|
796
806
|
setConversations([]);
|
|
797
|
-
setMessages([]);
|
|
798
807
|
setLocalError(null);
|
|
799
808
|
setOlderMessagesCursor(null);
|
|
800
809
|
setIsLoadingConversations(false);
|
|
@@ -810,7 +819,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
810
819
|
setActiveConversationId(null);
|
|
811
820
|
setConversationModelState(null);
|
|
812
821
|
setConversations([]);
|
|
813
|
-
setMessages([]);
|
|
814
822
|
setLocalError(null);
|
|
815
823
|
clearRuntimeMessages();
|
|
816
824
|
setOlderMessagesCursor(null);
|
|
@@ -823,7 +831,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
823
831
|
clearRuntimeMessages();
|
|
824
832
|
lastAutoLoadedConversationIdRef.current = null;
|
|
825
833
|
loadingConversationIdRef.current = null;
|
|
826
|
-
setMessages([]);
|
|
827
834
|
setOlderMessagesCursor(null);
|
|
828
835
|
return;
|
|
829
836
|
}
|
|
@@ -842,12 +849,12 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
842
849
|
loadingConversationIdRef.current = activeConversationId;
|
|
843
850
|
const loadConversation = async () => {
|
|
844
851
|
setOlderMessagesCursor(null);
|
|
845
|
-
await
|
|
852
|
+
await loadConversationMessagesRef.current?.(activeConversationId);
|
|
846
853
|
if (cancelled)
|
|
847
854
|
return;
|
|
848
855
|
lastAutoLoadedConversationIdRef.current = activeConversationId;
|
|
849
856
|
try {
|
|
850
|
-
await
|
|
857
|
+
await resumeIfRunningRef.current?.(activeConversationId);
|
|
851
858
|
}
|
|
852
859
|
catch (error) {
|
|
853
860
|
if (cancelled)
|
|
@@ -863,9 +870,9 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
863
870
|
return () => {
|
|
864
871
|
cancelled = true;
|
|
865
872
|
};
|
|
866
|
-
}, [activeConversationId, clearRuntimeMessages, enabled
|
|
873
|
+
}, [activeConversationId, clearRuntimeMessages, enabled]);
|
|
867
874
|
const stop = useCallback(() => {
|
|
868
|
-
const hadActiveStream =
|
|
875
|
+
const hadActiveStream = sessionIsStreamingRef.current || isStreamingRef.current;
|
|
869
876
|
sessionCancel();
|
|
870
877
|
setIsStreaming(false);
|
|
871
878
|
const conversationId = activeConversationIdRef.current;
|
|
@@ -881,13 +888,34 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
881
888
|
touchConversation(conversationId, { status: previousStatus });
|
|
882
889
|
setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to stop conversation"));
|
|
883
890
|
});
|
|
884
|
-
}, [
|
|
891
|
+
}, [sessionCancel, sessionStop, touchConversation]);
|
|
885
892
|
const selectConversation = useCallback((conversationId) => {
|
|
886
|
-
if (
|
|
893
|
+
if (sessionIsStreamingRef.current || isStreamingRef.current) {
|
|
887
894
|
sessionCancel();
|
|
888
895
|
setIsStreaming(false);
|
|
889
896
|
}
|
|
890
|
-
const
|
|
897
|
+
const currentConversationId = activeConversationIdRef.current;
|
|
898
|
+
if (conversationId && conversationId === currentConversationId) {
|
|
899
|
+
if (loadingConversationIdRef.current === conversationId
|
|
900
|
+
|| lastAutoLoadedConversationIdRef.current === conversationId) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
loadingConversationIdRef.current = conversationId;
|
|
904
|
+
setLocalError(null);
|
|
905
|
+
setOlderMessagesCursor(null);
|
|
906
|
+
void loadConversationMessagesRef.current?.(conversationId)
|
|
907
|
+
.then(() => resumeIfRunningRef.current?.(conversationId))
|
|
908
|
+
.catch((error) => {
|
|
909
|
+
setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to resume conversation"));
|
|
910
|
+
})
|
|
911
|
+
.finally(() => {
|
|
912
|
+
if (loadingConversationIdRef.current === conversationId) {
|
|
913
|
+
loadingConversationIdRef.current = null;
|
|
914
|
+
}
|
|
915
|
+
lastAutoLoadedConversationIdRef.current = conversationId;
|
|
916
|
+
});
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
891
919
|
suppressAutoSelectRef.current = conversationId === null;
|
|
892
920
|
setLocalError(null);
|
|
893
921
|
activeConversationIdRef.current = conversationId;
|
|
@@ -895,15 +923,8 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
895
923
|
loadingConversationIdRef.current = null;
|
|
896
924
|
setOlderMessagesCursor(null);
|
|
897
925
|
clearRuntimeMessages();
|
|
898
|
-
setMessages([]);
|
|
899
|
-
if (wasSameConversation) {
|
|
900
|
-
void loadConversationMessages(conversationId);
|
|
901
|
-
void sessionResumeIfRunning(conversationId).catch((error) => {
|
|
902
|
-
setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to resume conversation"));
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
926
|
setActiveConversationId(conversationId);
|
|
906
|
-
}, [clearRuntimeMessages,
|
|
927
|
+
}, [clearRuntimeMessages, sessionCancel]);
|
|
907
928
|
const resetConversationState = useCallback((keepPendingFiles = false) => {
|
|
908
929
|
stop();
|
|
909
930
|
clearRuntimeMessages();
|
|
@@ -913,7 +934,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
913
934
|
loadingConversationIdRef.current = null;
|
|
914
935
|
skipInitialLoadConversationIdsRef.current.clear();
|
|
915
936
|
setActiveConversationId(null);
|
|
916
|
-
setMessages([]);
|
|
917
937
|
setLocalError(null);
|
|
918
938
|
setOlderMessagesCursor(null);
|
|
919
939
|
if (!keepPendingFiles) {
|
|
@@ -945,7 +965,6 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
945
965
|
setActiveConversationId(createdConversation.id);
|
|
946
966
|
setConversationModelState((createdConversation.model ?? conversationModel ?? null));
|
|
947
967
|
clearRuntimeMessages();
|
|
948
|
-
setMessages([]);
|
|
949
968
|
setOlderMessagesCursor(null);
|
|
950
969
|
return createdConversation.id;
|
|
951
970
|
}, [clearRuntimeMessages, conversationModel, scope, sessionCreateConversation]);
|
|
@@ -1099,7 +1118,7 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
1099
1118
|
const activeConversation = conversations.find((conversation) => conversation.id === activeConversationId);
|
|
1100
1119
|
return isConversationRunning(activeConversation?.status);
|
|
1101
1120
|
}, [activeConversationId, conversations]);
|
|
1102
|
-
return {
|
|
1121
|
+
return useMemo(() => ({
|
|
1103
1122
|
messages,
|
|
1104
1123
|
conversations,
|
|
1105
1124
|
activeConversationId,
|
|
@@ -1125,5 +1144,31 @@ export function useAssistantController({ client, podId, assistantName, assistant
|
|
|
1125
1144
|
loadOlderMessages,
|
|
1126
1145
|
clearMessages,
|
|
1127
1146
|
stop,
|
|
1128
|
-
}
|
|
1147
|
+
}), [
|
|
1148
|
+
activeConversationId,
|
|
1149
|
+
availableModels,
|
|
1150
|
+
clearMessages,
|
|
1151
|
+
clearPendingFiles,
|
|
1152
|
+
completedActions,
|
|
1153
|
+
conversationModel,
|
|
1154
|
+
conversations,
|
|
1155
|
+
error,
|
|
1156
|
+
isActiveConversationRunning,
|
|
1157
|
+
isLoading,
|
|
1158
|
+
isLoadingConversations,
|
|
1159
|
+
isLoadingMessages,
|
|
1160
|
+
isLoadingOlderMessages,
|
|
1161
|
+
isUploadingFiles,
|
|
1162
|
+
loadOlderMessages,
|
|
1163
|
+
messages,
|
|
1164
|
+
olderMessagesCursor,
|
|
1165
|
+
pendingActions,
|
|
1166
|
+
pendingFiles,
|
|
1167
|
+
removePendingFile,
|
|
1168
|
+
selectConversation,
|
|
1169
|
+
sendMessage,
|
|
1170
|
+
setConversationModel,
|
|
1171
|
+
stop,
|
|
1172
|
+
uploadFiles,
|
|
1173
|
+
]);
|
|
1129
1174
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
function isRecord(value) {
|
|
3
3
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4
4
|
}
|
|
@@ -115,6 +115,7 @@ export function useAssistantRuntime({ conversationId = null, sessionConversation
|
|
|
115
115
|
setRuntimeMessages([]);
|
|
116
116
|
}, []);
|
|
117
117
|
useEffect(() => {
|
|
118
|
+
lastSessionMessageIdRef.current = null;
|
|
118
119
|
setRuntimeMessages((previous) => {
|
|
119
120
|
if (!conversationId) {
|
|
120
121
|
return [];
|
|
@@ -122,12 +123,15 @@ export function useAssistantRuntime({ conversationId = null, sessionConversation
|
|
|
122
123
|
return previous.filter((message) => message.conversation_id === conversationId);
|
|
123
124
|
});
|
|
124
125
|
}, [conversationId]);
|
|
126
|
+
const lastSessionMessageIdRef = useRef(null);
|
|
125
127
|
useEffect(() => {
|
|
126
128
|
if (sessionMessages.length === 0)
|
|
127
129
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
const lastSessionMessage = sessionMessages[sessionMessages.length - 1];
|
|
131
|
+
const lastSessionId = lastSessionMessage?.id ?? null;
|
|
132
|
+
if (lastSessionId && lastSessionId === lastSessionMessageIdRef.current)
|
|
133
|
+
return;
|
|
134
|
+
lastSessionMessageIdRef.current = lastSessionId;
|
|
131
135
|
const fallbackConversationId = sessionConversationId ?? conversationId;
|
|
132
136
|
const normalized = sessionMessages
|
|
133
137
|
.map((message) => toRuntimeMessage(message, fallbackConversationId))
|
|
@@ -61,6 +61,8 @@ export function useAssistantSession(options) {
|
|
|
61
61
|
const statusRef = useRef(undefined);
|
|
62
62
|
const streamingTextRef = useRef("");
|
|
63
63
|
const autoResumedKeyRef = useRef(null);
|
|
64
|
+
const autoLoadInFlightKeyRef = useRef(null);
|
|
65
|
+
const lastAutoLoadedKeyRef = useRef(null);
|
|
64
66
|
const onEventRef = useRef(onEvent);
|
|
65
67
|
const onStatusRef = useRef(onStatus);
|
|
66
68
|
const onMessageRef = useRef(onMessage);
|
|
@@ -73,6 +75,8 @@ export function useAssistantSession(options) {
|
|
|
73
75
|
return currentConversationId;
|
|
74
76
|
}
|
|
75
77
|
autoResumedKeyRef.current = null;
|
|
78
|
+
autoLoadInFlightKeyRef.current = null;
|
|
79
|
+
lastAutoLoadedKeyRef.current = null;
|
|
76
80
|
streamingTextRef.current = "";
|
|
77
81
|
setStreamingText("");
|
|
78
82
|
setConversation(null);
|
|
@@ -113,7 +117,12 @@ export function useAssistantSession(options) {
|
|
|
113
117
|
onStatusRef.current?.(normalized);
|
|
114
118
|
}
|
|
115
119
|
}, []);
|
|
120
|
+
const pendingStreamingFlushRef = useRef(null);
|
|
116
121
|
const clearStreamingText = useCallback(() => {
|
|
122
|
+
if (pendingStreamingFlushRef.current) {
|
|
123
|
+
clearTimeout(pendingStreamingFlushRef.current);
|
|
124
|
+
pendingStreamingFlushRef.current = null;
|
|
125
|
+
}
|
|
117
126
|
streamingTextRef.current = "";
|
|
118
127
|
setStreamingText("");
|
|
119
128
|
}, []);
|
|
@@ -121,7 +130,19 @@ export function useAssistantSession(options) {
|
|
|
121
130
|
if (!token)
|
|
122
131
|
return;
|
|
123
132
|
streamingTextRef.current += token;
|
|
124
|
-
|
|
133
|
+
if (!pendingStreamingFlushRef.current) {
|
|
134
|
+
pendingStreamingFlushRef.current = setTimeout(() => {
|
|
135
|
+
pendingStreamingFlushRef.current = null;
|
|
136
|
+
setStreamingText(streamingTextRef.current);
|
|
137
|
+
}, 0);
|
|
138
|
+
}
|
|
139
|
+
}, []);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
return () => {
|
|
142
|
+
if (pendingStreamingFlushRef.current) {
|
|
143
|
+
clearTimeout(pendingStreamingFlushRef.current);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
125
146
|
}, []);
|
|
126
147
|
const cancel = useCallback(() => {
|
|
127
148
|
abortRef.current?.abort();
|
|
@@ -485,10 +506,18 @@ export function useAssistantSession(options) {
|
|
|
485
506
|
}, [status]);
|
|
486
507
|
useEffect(() => {
|
|
487
508
|
if (!autoLoad || !conversationId) {
|
|
509
|
+
autoLoadInFlightKeyRef.current = null;
|
|
510
|
+
lastAutoLoadedKeyRef.current = null;
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const bootstrapKey = `${conversationId}:${autoResume ? "resume" : "load"}`;
|
|
514
|
+
if (autoLoadInFlightKeyRef.current === bootstrapKey
|
|
515
|
+
|| lastAutoLoadedKeyRef.current === bootstrapKey) {
|
|
488
516
|
return;
|
|
489
517
|
}
|
|
490
518
|
const controller = new AbortController();
|
|
491
519
|
let cancelled = false;
|
|
520
|
+
autoLoadInFlightKeyRef.current = bootstrapKey;
|
|
492
521
|
const bootstrapConversation = async () => {
|
|
493
522
|
const latestConversation = await refreshConversation(conversationId);
|
|
494
523
|
if (cancelled)
|
|
@@ -503,7 +532,20 @@ export function useAssistantSession(options) {
|
|
|
503
532
|
return;
|
|
504
533
|
await resumeIfRunning(conversationId);
|
|
505
534
|
};
|
|
506
|
-
void bootstrapConversation()
|
|
535
|
+
void bootstrapConversation()
|
|
536
|
+
.catch((bootstrapError) => {
|
|
537
|
+
if (cancelled)
|
|
538
|
+
return;
|
|
539
|
+
const normalized = normalizeError(bootstrapError, "Failed to load assistant conversation.");
|
|
540
|
+
setError(normalized);
|
|
541
|
+
onErrorRef.current?.(bootstrapError);
|
|
542
|
+
})
|
|
543
|
+
.finally(() => {
|
|
544
|
+
if (autoLoadInFlightKeyRef.current === bootstrapKey) {
|
|
545
|
+
autoLoadInFlightKeyRef.current = null;
|
|
546
|
+
}
|
|
547
|
+
lastAutoLoadedKeyRef.current = bootstrapKey;
|
|
548
|
+
});
|
|
507
549
|
return () => {
|
|
508
550
|
cancelled = true;
|
|
509
551
|
controller.abort();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { extractConversationMessageText, getLatestAssistantMessage, isConversationRunningStatus, normalizeConversationStatus, sortConversationMessagesByCreatedAt, } from "./assistant-output.js";
|
|
3
3
|
import { useAssistantSession, } from "./useAssistantSession.js";
|
|
4
4
|
function resolveConversationId(preferred, fallback) {
|
|
@@ -16,6 +16,8 @@ export function useConversationMessages({ client, podId, assistantName, assistan
|
|
|
16
16
|
const [nextPageToken, setNextPageToken] = useState(null);
|
|
17
17
|
const [isLoading, setIsLoading] = useState(false);
|
|
18
18
|
const [isLoadingOlder, setIsLoadingOlder] = useState(false);
|
|
19
|
+
const autoLoadInFlightKeyRef = useRef(null);
|
|
20
|
+
const lastAutoLoadedKeyRef = useRef(null);
|
|
19
21
|
const { conversation: sessionConversation, conversationId: sessionConversationId, messages: sessionMessages, status, streamingText, isStreaming, error, refreshConversation, loadMessages, sendMessage, resume, resumeIfRunning, stop, cancel, clearMessages: clearSessionMessages, createConversation, } = useAssistantSession({
|
|
20
22
|
client,
|
|
21
23
|
podId,
|
|
@@ -74,6 +76,8 @@ export function useConversationMessages({ client, podId, assistantName, assistan
|
|
|
74
76
|
}, [enabled, isLoading, isLoadingOlder, limit, loadMessages, nextPageToken, sessionConversationId]);
|
|
75
77
|
useEffect(() => {
|
|
76
78
|
if (!enabled || !conversationId) {
|
|
79
|
+
autoLoadInFlightKeyRef.current = null;
|
|
80
|
+
lastAutoLoadedKeyRef.current = null;
|
|
77
81
|
setNextPageToken(null);
|
|
78
82
|
setIsLoading(false);
|
|
79
83
|
setIsLoadingOlder(false);
|
|
@@ -84,6 +88,12 @@ export function useConversationMessages({ client, podId, assistantName, assistan
|
|
|
84
88
|
setNextPageToken(null);
|
|
85
89
|
if (!autoLoad)
|
|
86
90
|
return;
|
|
91
|
+
const bootstrapKey = `${conversationId}:${limit}:${autoResume ? "resume" : "load"}`;
|
|
92
|
+
if (autoLoadInFlightKeyRef.current === bootstrapKey
|
|
93
|
+
|| lastAutoLoadedKeyRef.current === bootstrapKey) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
autoLoadInFlightKeyRef.current = bootstrapKey;
|
|
87
97
|
let cancelled = false;
|
|
88
98
|
const bootstrap = async () => {
|
|
89
99
|
await refresh({ conversationId, limit });
|
|
@@ -91,7 +101,14 @@ export function useConversationMessages({ client, podId, assistantName, assistan
|
|
|
91
101
|
return;
|
|
92
102
|
await resumeIfRunning(conversationId);
|
|
93
103
|
};
|
|
94
|
-
void bootstrap()
|
|
104
|
+
void bootstrap()
|
|
105
|
+
.catch(() => undefined)
|
|
106
|
+
.finally(() => {
|
|
107
|
+
if (autoLoadInFlightKeyRef.current === bootstrapKey) {
|
|
108
|
+
autoLoadInFlightKeyRef.current = null;
|
|
109
|
+
}
|
|
110
|
+
lastAutoLoadedKeyRef.current = bootstrapKey;
|
|
111
|
+
});
|
|
95
112
|
return () => {
|
|
96
113
|
cancelled = true;
|
|
97
114
|
};
|
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
-
import type { RecordResponse } from "../types.js";
|
|
2
|
+
import type { FunctionRun, RecordResponse } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* React hook for creating a single record. Manages loading/error state and
|
|
5
|
+
* exposes a `create` function you can call from event handlers.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - `"direct"` (default): calls `records.create` directly.
|
|
9
|
+
* - `"function"`: calls `functions.runs.create`, routing the create through
|
|
10
|
+
* a pod function that may enforce business logic.
|
|
11
|
+
*
|
|
12
|
+
* @example Direct create
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const { create, isSubmitting } = useCreateRecord({ client, tableName: "comments" });
|
|
15
|
+
* await create({ body: "Hello", issue_id: "123" });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example Function-backed create
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const { create, isSubmitting } = useCreateRecord({
|
|
21
|
+
* client,
|
|
22
|
+
* tableName: "issues",
|
|
23
|
+
* createVia: "function",
|
|
24
|
+
* createFunctionName: "create-issue",
|
|
25
|
+
* });
|
|
26
|
+
* await create({ title: "Bug", team_id: "team_1" });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
3
29
|
export interface UseCreateRecordOptions {
|
|
4
30
|
client: LemmaClient;
|
|
5
31
|
podId?: string;
|
|
6
32
|
tableName: string;
|
|
7
33
|
enabled?: boolean;
|
|
8
|
-
|
|
34
|
+
/** How the record is created. `"direct"` calls `records.create`. `"function"` calls `functions.runs.create`. */
|
|
35
|
+
createVia?: "direct" | "function";
|
|
36
|
+
/** Function name to run when `createVia` is `"function"`. Falls back to `tableName` if omitted. */
|
|
37
|
+
createFunctionName?: string;
|
|
38
|
+
onSuccess?: (record: Record<string, unknown>, response: RecordResponse | FunctionRun) => void;
|
|
9
39
|
onError?: (error: unknown) => void;
|
|
10
40
|
}
|
|
11
41
|
export interface UseCreateRecordResult<TRecord extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -15,4 +45,4 @@ export interface UseCreateRecordResult<TRecord extends Record<string, unknown> =
|
|
|
15
45
|
create: (data: Record<string, unknown>) => Promise<TRecord | null>;
|
|
16
46
|
reset: () => void;
|
|
17
47
|
}
|
|
18
|
-
export declare function useCreateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, enabled, onSuccess, onError, }: UseCreateRecordOptions): UseCreateRecordResult<TRecord>;
|
|
48
|
+
export declare function useCreateRecord<TRecord extends Record<string, unknown> = Record<string, unknown>>({ client, podId, tableName, enabled, createVia, createFunctionName, onSuccess, onError, }: UseCreateRecordOptions): UseCreateRecordResult<TRecord>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { normalizeError, resolvePodClient } from "./utils.js";
|
|
3
|
-
export function useCreateRecord({ client, podId, tableName, enabled = true, onSuccess, onError, }) {
|
|
3
|
+
export function useCreateRecord({ client, podId, tableName, enabled = true, createVia = "direct", createFunctionName, onSuccess, onError, }) {
|
|
4
4
|
const [createdRecord, setCreatedRecord] = useState(null);
|
|
5
5
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
6
6
|
const [error, setError] = useState(null);
|
|
@@ -18,6 +18,16 @@ export function useCreateRecord({ client, podId, tableName, enabled = true, onSu
|
|
|
18
18
|
setError(null);
|
|
19
19
|
try {
|
|
20
20
|
const scopedClient = resolvePodClient(client, podId);
|
|
21
|
+
if (createVia === "function") {
|
|
22
|
+
const functionName = createFunctionName ?? trimmedTableName;
|
|
23
|
+
const run = await scopedClient.functions.runs.create(functionName, { input: data });
|
|
24
|
+
const nextRecord = (run.output_data ?? { id: run.id, ...data });
|
|
25
|
+
setCreatedRecord(nextRecord);
|
|
26
|
+
if (nextRecord) {
|
|
27
|
+
onSuccessRef.current?.(nextRecord, run);
|
|
28
|
+
}
|
|
29
|
+
return nextRecord;
|
|
30
|
+
}
|
|
21
31
|
const response = await scopedClient.records.create(trimmedTableName, data);
|
|
22
32
|
const nextRecord = (response.data ?? null);
|
|
23
33
|
setCreatedRecord(nextRecord);
|
|
@@ -35,7 +45,7 @@ export function useCreateRecord({ client, podId, tableName, enabled = true, onSu
|
|
|
35
45
|
finally {
|
|
36
46
|
setIsSubmitting(false);
|
|
37
47
|
}
|
|
38
|
-
}, [client, isEnabled, podId, trimmedTableName]);
|
|
48
|
+
}, [client, createFunctionName, createVia, isEnabled, podId, trimmedTableName]);
|
|
39
49
|
const reset = useCallback(() => {
|
|
40
50
|
setCreatedRecord(null);
|
|
41
51
|
setError(null);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
import type { FileResponse } from "../types.js";
|
|
3
|
+
export interface UseFileOptions {
|
|
4
|
+
client: LemmaClient;
|
|
5
|
+
podId?: string;
|
|
6
|
+
path?: string | null;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
autoLoad?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface UseFileResult {
|
|
11
|
+
file: FileResponse | null;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
refresh: (overrides?: {
|
|
15
|
+
path?: string | null;
|
|
16
|
+
}) => Promise<FileResponse | null>;
|
|
17
|
+
}
|
|
18
|
+
export declare function useFile({ client, podId, path, enabled, autoLoad, }: UseFileOptions): UseFileResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { normalizeError, resolvePodClient } from "./utils.js";
|
|
3
|
+
export function useFile({ client, podId, path = null, enabled = true, autoLoad = true, }) {
|
|
4
|
+
const [file, setFile] = useState(null);
|
|
5
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const trimmedPath = typeof path === "string" ? path.trim() : "";
|
|
8
|
+
const isEnabled = enabled && trimmedPath.length > 0;
|
|
9
|
+
const refresh = useCallback(async (overrides = {}, signal) => {
|
|
10
|
+
const nextPath = typeof overrides.path === "string" ? overrides.path.trim() : trimmedPath;
|
|
11
|
+
if (!enabled || nextPath.length === 0) {
|
|
12
|
+
setFile(null);
|
|
13
|
+
setError(null);
|
|
14
|
+
setIsLoading(false);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
setIsLoading(true);
|
|
18
|
+
setError(null);
|
|
19
|
+
try {
|
|
20
|
+
const scopedClient = resolvePodClient(client, podId);
|
|
21
|
+
const nextFile = await scopedClient.files.get(nextPath);
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
return null;
|
|
24
|
+
setFile(nextFile);
|
|
25
|
+
return nextFile;
|
|
26
|
+
}
|
|
27
|
+
catch (refreshError) {
|
|
28
|
+
if (signal?.aborted)
|
|
29
|
+
return null;
|
|
30
|
+
setError(normalizeError(refreshError, "Failed to load file."));
|
|
31
|
+
setFile(null);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (!signal?.aborted)
|
|
36
|
+
setIsLoading(false);
|
|
37
|
+
}
|
|
38
|
+
}, [client, enabled, podId, trimmedPath]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isEnabled) {
|
|
41
|
+
setFile(null);
|
|
42
|
+
setError(null);
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!autoLoad)
|
|
47
|
+
return;
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
void refresh({}, controller.signal);
|
|
50
|
+
return () => controller.abort();
|
|
51
|
+
}, [autoLoad, isEnabled, refresh]);
|
|
52
|
+
return useMemo(() => ({
|
|
53
|
+
file,
|
|
54
|
+
isLoading,
|
|
55
|
+
error,
|
|
56
|
+
refresh,
|
|
57
|
+
}), [error, file, isLoading, refresh]);
|
|
58
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LemmaClient } from "../client.js";
|
|
2
|
+
export type FilePreviewMode = "rendered" | "artifact";
|
|
3
|
+
export interface UseFilePreviewOptions {
|
|
4
|
+
client: LemmaClient;
|
|
5
|
+
podId?: string;
|
|
6
|
+
path?: string | null;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
autoLoad?: boolean;
|
|
9
|
+
mode?: FilePreviewMode;
|
|
10
|
+
artifact?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface UseFilePreviewResult {
|
|
13
|
+
content: string | null;
|
|
14
|
+
blob: Blob | null;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
refresh: (overrides?: {
|
|
18
|
+
path?: string | null;
|
|
19
|
+
mode?: FilePreviewMode;
|
|
20
|
+
artifact?: string;
|
|
21
|
+
}) => Promise<string | null>;
|
|
22
|
+
}
|
|
23
|
+
export declare function useFilePreview({ client, podId, path, enabled, autoLoad, mode, artifact, }: UseFilePreviewOptions): UseFilePreviewResult;
|