@open-mercato/ui 0.6.1-develop.3227.1.2b7b8ab258 → 0.6.1-develop.3256.1.fe3dec2464
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/.turbo/turbo-build.log +1 -1
- package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
- package/dist/ai/AiAssistantLauncher.js +8 -0
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +226 -58
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiDock.js +3 -2
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/ModelPicker.js +186 -0
- package/dist/ai/ModelPicker.js.map +7 -0
- package/dist/ai/index.js +3 -1
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiChat.js +12 -16
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/operations/LastOperationBanner.js +15 -9
- package/dist/backend/operations/LastOperationBanner.js.map +2 -2
- package/dist/backend/operations/store.js +64 -8
- package/dist/backend/operations/store.js.map +2 -2
- package/dist/backend/utils/bulkDelete.js +46 -0
- package/dist/backend/utils/bulkDelete.js.map +7 -0
- package/package.json +3 -3
- package/src/ai/AiAssistantLauncher.tsx +8 -0
- package/src/ai/AiChat.tsx +229 -11
- package/src/ai/AiDock.tsx +3 -2
- package/src/ai/ModelPicker.tsx +248 -0
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.test.tsx +180 -0
- package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
- package/src/ai/__tests__/AiDock.test.tsx +12 -1
- package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/useAiChat.ts +29 -16
- package/src/backend/operations/LastOperationBanner.tsx +19 -11
- package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +46 -1
- package/src/backend/operations/__tests__/store.test.ts +142 -0
- package/src/backend/operations/store.ts +76 -7
- package/src/backend/utils/__tests__/bulkDelete.test.ts +103 -0
- package/src/backend/utils/bulkDelete.ts +77 -0
package/dist/ai/AiChat.js
CHANGED
|
@@ -26,7 +26,11 @@ import { Button } from "../primitives/button.js";
|
|
|
26
26
|
import { IconButton } from "../primitives/icon-button.js";
|
|
27
27
|
import { Label } from "../primitives/label.js";
|
|
28
28
|
import { Textarea } from "../primitives/textarea.js";
|
|
29
|
+
import { apiCall } from "../backend/utils/apiCall.js";
|
|
29
30
|
import { parseAiContentSegments } from "./AiMessageContent.js";
|
|
31
|
+
import {
|
|
32
|
+
ModelPicker
|
|
33
|
+
} from "./ModelPicker.js";
|
|
30
34
|
import { RecordCard } from "./records/RecordCard.js";
|
|
31
35
|
import {
|
|
32
36
|
defaultAiUiPartRegistry,
|
|
@@ -38,6 +42,73 @@ import {
|
|
|
38
42
|
import { useAiChatUpload } from "./useAiChatUpload.js";
|
|
39
43
|
import { useAiShortcuts } from "./useAiShortcuts.js";
|
|
40
44
|
const PREVIEW_DATA_URL_MAX_BYTES = 2 * 1024 * 1024;
|
|
45
|
+
const COMPACT_FOOTER_MAX_WIDTH = 640;
|
|
46
|
+
const MODEL_PICKER_STORAGE_PREFIX = "om-ai-model-picker:";
|
|
47
|
+
function readModelPickerValue(agentId) {
|
|
48
|
+
if (typeof window === "undefined") return null;
|
|
49
|
+
try {
|
|
50
|
+
const raw = window.localStorage.getItem(`${MODEL_PICKER_STORAGE_PREFIX}${agentId}`);
|
|
51
|
+
if (!raw) return null;
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.modelId === "string") {
|
|
54
|
+
const value = parsed;
|
|
55
|
+
return { providerId: value.providerId, modelId: value.modelId };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function writeModelPickerValue(agentId, value) {
|
|
63
|
+
if (typeof window === "undefined") return;
|
|
64
|
+
try {
|
|
65
|
+
const key = `${MODEL_PICKER_STORAGE_PREFIX}${agentId}`;
|
|
66
|
+
if (value === null) {
|
|
67
|
+
window.localStorage.removeItem(key);
|
|
68
|
+
} else {
|
|
69
|
+
window.localStorage.setItem(key, JSON.stringify({ providerId: value.providerId, modelId: value.modelId }));
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function useAgentModels(agent) {
|
|
75
|
+
const [providers, setProviders] = React.useState([]);
|
|
76
|
+
const [allowRuntimeModelOverride, setAllowRuntimeModelOverride] = React.useState(false);
|
|
77
|
+
const [defaultLabel, setDefaultLabel] = React.useState(null);
|
|
78
|
+
const [loaded, setLoaded] = React.useState(false);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
const modelsUrl = `/api/ai_assistant/ai/agents/${encodeURIComponent(agent)}/models`;
|
|
81
|
+
setLoaded(false);
|
|
82
|
+
setDefaultLabel(null);
|
|
83
|
+
void apiCall(modelsUrl).then((result) => {
|
|
84
|
+
if (!result.ok || !result.result) {
|
|
85
|
+
setLoaded(true);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setAllowRuntimeModelOverride(result.result.allowRuntimeModelOverride);
|
|
89
|
+
setProviders(result.result.providers);
|
|
90
|
+
setDefaultLabel(
|
|
91
|
+
result.result.defaultProviderName && result.result.defaultModelName ? `${result.result.defaultProviderName} / ${result.result.defaultModelName}` : result.result.defaultProviderId && result.result.defaultModelId ? `${result.result.defaultProviderId} / ${result.result.defaultModelId}` : null
|
|
92
|
+
);
|
|
93
|
+
setLoaded(true);
|
|
94
|
+
});
|
|
95
|
+
}, [agent]);
|
|
96
|
+
return { providers, allowRuntimeModelOverride, defaultLabel, loaded };
|
|
97
|
+
}
|
|
98
|
+
function firstAvailableModelPickerValue(providers) {
|
|
99
|
+
for (const provider of providers) {
|
|
100
|
+
const model = provider.models[0];
|
|
101
|
+
if (model) {
|
|
102
|
+
return { providerId: provider.id, modelId: model.id };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function isModelPickerValueAvailable(value, providers) {
|
|
108
|
+
return providers.some(
|
|
109
|
+
(provider) => provider.id === value.providerId && provider.models.some((model) => model.id === value.modelId)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
41
112
|
async function readFileAsDataUrl(file) {
|
|
42
113
|
if (!file.type.startsWith("image/")) return void 0;
|
|
43
114
|
if (file.size > PREVIEW_DATA_URL_MAX_BYTES) return void 0;
|
|
@@ -493,7 +564,8 @@ function AiChat({
|
|
|
493
564
|
suggestions,
|
|
494
565
|
contextItems,
|
|
495
566
|
welcomeTitle,
|
|
496
|
-
welcomeDescription
|
|
567
|
+
welcomeDescription,
|
|
568
|
+
defaultCompactFooter = false
|
|
497
569
|
}) {
|
|
498
570
|
const t = useT();
|
|
499
571
|
const textareaRef = React.useRef(null);
|
|
@@ -503,6 +575,23 @@ function AiChat({
|
|
|
503
575
|
const [pendingFiles, setPendingFiles] = React.useState([]);
|
|
504
576
|
const upload = useAiChatUpload();
|
|
505
577
|
const isUploading = upload.busy;
|
|
578
|
+
const footerRef = React.useRef(null);
|
|
579
|
+
const [isCompactFooter, setIsCompactFooter] = React.useState(defaultCompactFooter);
|
|
580
|
+
React.useEffect(() => {
|
|
581
|
+
const element = footerRef.current;
|
|
582
|
+
if (!element || typeof ResizeObserver === "undefined") return;
|
|
583
|
+
const updateCompactState = (width) => {
|
|
584
|
+
setIsCompactFooter(width < COMPACT_FOOTER_MAX_WIDTH);
|
|
585
|
+
};
|
|
586
|
+
updateCompactState(element.getBoundingClientRect().width);
|
|
587
|
+
const observer = new ResizeObserver((entries) => {
|
|
588
|
+
const entry = entries[0];
|
|
589
|
+
if (!entry) return;
|
|
590
|
+
updateCompactState(entry.contentRect.width);
|
|
591
|
+
});
|
|
592
|
+
observer.observe(element);
|
|
593
|
+
return () => observer.disconnect();
|
|
594
|
+
}, []);
|
|
506
595
|
const uploadedAttachmentIds = React.useMemo(
|
|
507
596
|
() => pendingFiles.map((entry) => entry.attachmentId).filter((id) => typeof id === "string" && id.length > 0),
|
|
508
597
|
[pendingFiles]
|
|
@@ -511,6 +600,60 @@ function AiChat({
|
|
|
511
600
|
() => [...attachmentIds ?? [], ...uploadedAttachmentIds],
|
|
512
601
|
[attachmentIds, uploadedAttachmentIds]
|
|
513
602
|
);
|
|
603
|
+
const {
|
|
604
|
+
providers: modelProviders,
|
|
605
|
+
allowRuntimeModelOverride,
|
|
606
|
+
defaultLabel: modelDefaultLabel,
|
|
607
|
+
loaded: modelProvidersLoaded
|
|
608
|
+
} = useAgentModels(agent);
|
|
609
|
+
const [modelPickerValue, setModelPickerValue] = React.useState(
|
|
610
|
+
() => readModelPickerValue(agent)
|
|
611
|
+
);
|
|
612
|
+
const effectiveModelPickerValue = React.useMemo(() => {
|
|
613
|
+
if (!modelProvidersLoaded || !allowRuntimeModelOverride || modelProviders.length === 0) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
if (modelPickerValue && isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
|
|
617
|
+
return modelPickerValue;
|
|
618
|
+
}
|
|
619
|
+
return firstAvailableModelPickerValue(modelProviders);
|
|
620
|
+
}, [
|
|
621
|
+
allowRuntimeModelOverride,
|
|
622
|
+
modelPickerValue,
|
|
623
|
+
modelProviders,
|
|
624
|
+
modelProvidersLoaded
|
|
625
|
+
]);
|
|
626
|
+
React.useEffect(() => {
|
|
627
|
+
setModelPickerValue(readModelPickerValue(agent));
|
|
628
|
+
}, [agent]);
|
|
629
|
+
React.useEffect(() => {
|
|
630
|
+
if (!modelProvidersLoaded) return;
|
|
631
|
+
if (!allowRuntimeModelOverride || modelProviders.length === 0) {
|
|
632
|
+
if (modelPickerValue !== null) {
|
|
633
|
+
setModelPickerValue(null);
|
|
634
|
+
writeModelPickerValue(agent, null);
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (modelPickerValue && !isModelPickerValueAvailable(modelPickerValue, modelProviders)) {
|
|
639
|
+
const fallback = firstAvailableModelPickerValue(modelProviders);
|
|
640
|
+
setModelPickerValue(fallback);
|
|
641
|
+
writeModelPickerValue(agent, fallback);
|
|
642
|
+
}
|
|
643
|
+
}, [
|
|
644
|
+
agent,
|
|
645
|
+
allowRuntimeModelOverride,
|
|
646
|
+
modelPickerValue,
|
|
647
|
+
modelProviders,
|
|
648
|
+
modelProvidersLoaded
|
|
649
|
+
]);
|
|
650
|
+
const handleModelPickerChange = React.useCallback(
|
|
651
|
+
(value) => {
|
|
652
|
+
setModelPickerValue(value);
|
|
653
|
+
writeModelPickerValue(agent, value);
|
|
654
|
+
},
|
|
655
|
+
[agent]
|
|
656
|
+
);
|
|
514
657
|
const chat = useAiChat({
|
|
515
658
|
agent,
|
|
516
659
|
apiPath,
|
|
@@ -519,7 +662,9 @@ function AiChat({
|
|
|
519
662
|
debug,
|
|
520
663
|
initialMessages,
|
|
521
664
|
onError,
|
|
522
|
-
conversationId
|
|
665
|
+
conversationId,
|
|
666
|
+
providerOverride: effectiveModelPickerValue?.providerId ?? null,
|
|
667
|
+
modelOverride: effectiveModelPickerValue?.modelId ?? null
|
|
523
668
|
});
|
|
524
669
|
const isStreaming = chat.status === "streaming";
|
|
525
670
|
const isSubmitting = chat.status === "submitting";
|
|
@@ -727,15 +872,15 @@ function AiChat({
|
|
|
727
872
|
"section",
|
|
728
873
|
{
|
|
729
874
|
className: cn(
|
|
730
|
-
"flex h-full min-h-[320px] flex-col gap-3 rounded-lg border border-border bg-background p-3",
|
|
875
|
+
"flex h-full min-h-[320px] min-w-0 flex-col gap-3 overflow-hidden rounded-lg border border-border bg-background p-3",
|
|
731
876
|
className
|
|
732
877
|
),
|
|
733
878
|
"aria-label": t("ai_assistant.chat.regionLabel", "AI chat"),
|
|
734
879
|
"data-ai-chat-agent": agent,
|
|
735
880
|
"data-ai-chat-conversation-id": chat.conversationId,
|
|
736
881
|
children: [
|
|
737
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border pb-2", children: [
|
|
738
|
-
/* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center gap-2 text-xs text-muted-foreground", children: contextItems && contextItems.length > 0 ? /* @__PURE__ */ jsx(ContextItemsPill, { items: contextItems }) : /* @__PURE__ */ jsx("span", { className: "font-mono opacity-70", "aria-hidden": true, children: chat.conversationId.slice(0, 8) }) }),
|
|
882
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center justify-between gap-2 border-b border-border pb-2", children: [
|
|
883
|
+
/* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-xs text-muted-foreground", children: contextItems && contextItems.length > 0 ? /* @__PURE__ */ jsx(ContextItemsPill, { items: contextItems }) : /* @__PURE__ */ jsx("span", { className: "font-mono opacity-70", "aria-hidden": true, children: chat.conversationId.slice(0, 8) }) }),
|
|
739
884
|
/* @__PURE__ */ jsx(
|
|
740
885
|
IconButton,
|
|
741
886
|
{
|
|
@@ -758,7 +903,7 @@ function AiChat({
|
|
|
758
903
|
role: "log",
|
|
759
904
|
"aria-live": "polite",
|
|
760
905
|
"aria-label": t("ai_assistant.chat.transcriptLabel", "Chat transcript"),
|
|
761
|
-
className: "flex-1 space-y-2 overflow-y-auto pr-1",
|
|
906
|
+
className: "min-w-0 flex-1 space-y-2 overflow-y-auto overflow-x-hidden pr-1",
|
|
762
907
|
children: [
|
|
763
908
|
chat.messages.length === 0 ? /* @__PURE__ */ jsx(
|
|
764
909
|
WelcomeState,
|
|
@@ -810,7 +955,7 @@ function AiChat({
|
|
|
810
955
|
/* @__PURE__ */ jsxs(
|
|
811
956
|
"form",
|
|
812
957
|
{
|
|
813
|
-
className: "flex flex-col gap-2",
|
|
958
|
+
className: "flex min-w-0 flex-col gap-2",
|
|
814
959
|
onSubmit: (event) => {
|
|
815
960
|
event.preventDefault();
|
|
816
961
|
handleSubmit();
|
|
@@ -862,7 +1007,7 @@ function AiChat({
|
|
|
862
1007
|
onKeyDown: handleKeyDown,
|
|
863
1008
|
rows: 3,
|
|
864
1009
|
"aria-label": t("ai_assistant.chat.composerLabel", "Message composer"),
|
|
865
|
-
className: "resize-none"
|
|
1010
|
+
className: "min-w-0 resize-none"
|
|
866
1011
|
}
|
|
867
1012
|
),
|
|
868
1013
|
/* @__PURE__ */ jsx(
|
|
@@ -877,56 +1022,79 @@ function AiChat({
|
|
|
877
1022
|
"data-ai-chat-file-input": ""
|
|
878
1023
|
}
|
|
879
1024
|
),
|
|
880
|
-
/* @__PURE__ */ jsxs(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1025
|
+
/* @__PURE__ */ jsxs(
|
|
1026
|
+
"div",
|
|
1027
|
+
{
|
|
1028
|
+
ref: footerRef,
|
|
1029
|
+
className: "flex min-w-0 items-center justify-between gap-2",
|
|
1030
|
+
"data-ai-chat-footer": "",
|
|
1031
|
+
"data-ai-chat-footer-compact": isCompactFooter ? "true" : "false",
|
|
1032
|
+
children: [
|
|
1033
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [
|
|
1034
|
+
/* @__PURE__ */ jsx(
|
|
1035
|
+
IconButton,
|
|
1036
|
+
{
|
|
1037
|
+
type: "button",
|
|
1038
|
+
variant: "ghost",
|
|
1039
|
+
size: "sm",
|
|
1040
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1041
|
+
disabled: isBusy || isUploading,
|
|
1042
|
+
"aria-label": t("ai_assistant.chat.attachFile", "Attach file"),
|
|
1043
|
+
children: /* @__PURE__ */ jsx(Paperclip, { className: "size-4", "aria-hidden": true })
|
|
1044
|
+
}
|
|
1045
|
+
),
|
|
1046
|
+
allowRuntimeModelOverride && modelProviders.length > 0 ? /* @__PURE__ */ jsx(
|
|
1047
|
+
ModelPicker,
|
|
1048
|
+
{
|
|
1049
|
+
agentId: agent,
|
|
1050
|
+
value: effectiveModelPickerValue,
|
|
1051
|
+
onChange: handleModelPickerChange,
|
|
1052
|
+
availableProviders: modelProviders,
|
|
1053
|
+
disabled: isBusy,
|
|
1054
|
+
compact: isCompactFooter,
|
|
1055
|
+
defaultLabel: modelDefaultLabel,
|
|
1056
|
+
className: "shrink-0"
|
|
1057
|
+
}
|
|
1058
|
+
) : null,
|
|
1059
|
+
/* @__PURE__ */ jsx("p", { className: cn("text-xs text-muted-foreground", isCompactFooter && "hidden"), children: hasUploadingFiles || isUploading ? t(
|
|
1060
|
+
"ai_assistant.chat.uploadingHint",
|
|
1061
|
+
"Uploading attachments\u2026 Send is disabled until they finish."
|
|
1062
|
+
) : t(
|
|
1063
|
+
"ai_assistant.chat.shortcutHint",
|
|
1064
|
+
"Press Enter to send, Shift+Enter for new line."
|
|
1065
|
+
) })
|
|
1066
|
+
] }),
|
|
1067
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
1068
|
+
isStreaming ? /* @__PURE__ */ jsx(
|
|
1069
|
+
IconButton,
|
|
1070
|
+
{
|
|
1071
|
+
type: "button",
|
|
1072
|
+
variant: "outline",
|
|
1073
|
+
size: "sm",
|
|
1074
|
+
onClick: () => chat.cancel(),
|
|
1075
|
+
"aria-label": t("ai_assistant.chat.cancel", "Cancel streaming response"),
|
|
1076
|
+
children: /* @__PURE__ */ jsx(Square, { className: "size-4", "aria-hidden": true })
|
|
1077
|
+
}
|
|
1078
|
+
) : null,
|
|
1079
|
+
/* @__PURE__ */ jsxs(
|
|
1080
|
+
Button,
|
|
1081
|
+
{
|
|
1082
|
+
type: "submit",
|
|
1083
|
+
size: "sm",
|
|
1084
|
+
disabled: isBusy || isUploading || hasUploadingFiles || input.trim().length === 0,
|
|
1085
|
+
"aria-label": hasUploadingFiles || isUploading ? t("ai_assistant.chat.sendWaitingForUpload", "Waiting for upload to finish\u2026") : t("ai_assistant.chat.send", "Send message"),
|
|
1086
|
+
title: hasUploadingFiles || isUploading ? t("ai_assistant.chat.sendWaitingForUpload", "Waiting for upload to finish\u2026") : void 0,
|
|
1087
|
+
className: cn(isCompactFooter && "w-8 px-0"),
|
|
1088
|
+
children: [
|
|
1089
|
+
/* @__PURE__ */ jsx(Send, { className: "size-4", "aria-hidden": true }),
|
|
1090
|
+
!isCompactFooter ? /* @__PURE__ */ jsx("span", { children: t("ai_assistant.chat.send", "Send message") }) : null
|
|
1091
|
+
]
|
|
1092
|
+
}
|
|
1093
|
+
)
|
|
1094
|
+
] })
|
|
1095
|
+
]
|
|
1096
|
+
}
|
|
1097
|
+
)
|
|
930
1098
|
]
|
|
931
1099
|
}
|
|
932
1100
|
),
|