@proveanything/smartlinks-utils-ui 0.1.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { cn } from './chunk-L7FQ52F5.js';
2
2
  import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
3
  import * as SL from '@proveanything/smartlinks';
4
- import { Filter, Search, LayoutGrid, List, X, Loader2, AlertCircle, ImageOff, Clipboard, Pencil, Check, Upload, Link, Trash2, FileIcon, Image, Film, Music, FileText } from 'lucide-react';
4
+ import { Filter, Search, LayoutGrid, List, X, Loader2, AlertCircle, ImageOff, Clipboard, Pencil, Check, Upload, Link, MicOff, Mic, ChevronDown, ChevronRight, Sparkles, Image, Trash2, FileIcon, Film, Music, FileText } from 'lucide-react';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/components/AssetPicker/types.ts
@@ -98,6 +98,29 @@ function useAssets({ scope, accept, pageSize }) {
98
98
  }
99
99
  }
100
100
  }, [upload]);
101
+ const uploadFromRemoteUrl = useCallback(async (url, opts) => {
102
+ setUploading(true);
103
+ setUploadProgress(0);
104
+ try {
105
+ const result = await SL.asset.uploadFromUrl({
106
+ url,
107
+ scope,
108
+ metadata: { name: opts?.name, ...opts?.metadata || {} }
109
+ });
110
+ if (mountedRef.current) {
111
+ setAssets((prev) => [result, ...prev]);
112
+ }
113
+ return result;
114
+ } catch (err) {
115
+ if (mountedRef.current) setError(err?.message || "Remote URL ingest failed");
116
+ return null;
117
+ } finally {
118
+ if (mountedRef.current) {
119
+ setUploading(false);
120
+ setUploadProgress(0);
121
+ }
122
+ }
123
+ }, [scope]);
101
124
  const remove = useCallback(async (assetId) => {
102
125
  try {
103
126
  await SL.asset.remove({ assetId, scope });
@@ -117,6 +140,7 @@ function useAssets({ scope, accept, pageSize }) {
117
140
  refresh: fetchAssets,
118
141
  upload,
119
142
  uploadFromUrl,
143
+ uploadFromRemoteUrl,
120
144
  remove,
121
145
  uploading,
122
146
  uploadProgress
@@ -187,6 +211,7 @@ var AssetGridItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
187
211
  allowDelete && onDelete && /* @__PURE__ */ jsx(
188
212
  "button",
189
213
  {
214
+ type: "button",
190
215
  className: "absolute top-2 left-2 w-6 h-6 rounded-full bg-destructive/80 hover:bg-destructive text-destructive-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity",
191
216
  onClick: (e) => {
192
217
  e.stopPropagation();
@@ -237,6 +262,7 @@ var AssetListItem = ({ asset: asset2, selected, onToggle, onDoubleClick, onDelet
237
262
  allowDelete && onDelete && /* @__PURE__ */ jsx(
238
263
  "button",
239
264
  {
265
+ type: "button",
240
266
  className: "w-6 h-6 rounded-full bg-destructive/80 hover:bg-destructive text-destructive-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0",
241
267
  onClick: (e) => {
242
268
  e.stopPropagation();
@@ -411,6 +437,7 @@ var UploadZone = ({
411
437
  ) : /* @__PURE__ */ jsxs(
412
438
  "button",
413
439
  {
440
+ type: "button",
414
441
  onClick: () => setEditingName(true),
415
442
  className: "flex items-center gap-1 mx-auto px-2 py-1 text-sm text-muted-foreground hover:text-foreground rounded hover:bg-accent transition-colors",
416
443
  title: "Rename",
@@ -430,6 +457,7 @@ var UploadZone = ({
430
457
  /* @__PURE__ */ jsxs(
431
458
  "button",
432
459
  {
460
+ type: "button",
433
461
  onClick: handleCancelPaste,
434
462
  className: "px-3 py-1.5 text-xs font-medium rounded-md border border-border text-muted-foreground hover:bg-accent transition-colors flex items-center gap-1",
435
463
  children: [
@@ -441,6 +469,7 @@ var UploadZone = ({
441
469
  /* @__PURE__ */ jsxs(
442
470
  "button",
443
471
  {
472
+ type: "button",
444
473
  onClick: handleConfirmPaste,
445
474
  className: "px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors flex items-center gap-1",
446
475
  children: [
@@ -525,8 +554,9 @@ var UrlImport = ({
525
554
  }) => {
526
555
  const [url, setUrl] = useState("");
527
556
  const [error, setError] = useState("");
528
- const handleSubmit = async (e) => {
529
- e.preventDefault();
557
+ const handleSubmit = useCallback(async (e) => {
558
+ e?.preventDefault();
559
+ e?.stopPropagation();
530
560
  if (!url.trim()) return;
531
561
  try {
532
562
  new URL(url.trim());
@@ -537,8 +567,8 @@ var UrlImport = ({
537
567
  setError("");
538
568
  const result = await onImport(url.trim());
539
569
  if (result) setUrl("");
540
- };
541
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: cn("flex gap-2", className), children: [
570
+ }, [url, onImport]);
571
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex gap-2", className), children: [
542
572
  /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
543
573
  /* @__PURE__ */ jsx(Link, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }),
544
574
  /* @__PURE__ */ jsx(
@@ -550,6 +580,13 @@ var UrlImport = ({
550
580
  setUrl(e.target.value);
551
581
  setError("");
552
582
  },
583
+ onKeyDown: (e) => {
584
+ if (e.key === "Enter") {
585
+ e.preventDefault();
586
+ e.stopPropagation();
587
+ handleSubmit();
588
+ }
589
+ },
553
590
  placeholder: "https://example.com/image.png",
554
591
  disabled: importing,
555
592
  className: cn(
@@ -564,7 +601,8 @@ var UrlImport = ({
564
601
  /* @__PURE__ */ jsxs(
565
602
  "button",
566
603
  {
567
- type: "submit",
604
+ type: "button",
605
+ onClick: handleSubmit,
568
606
  disabled: !url.trim() || importing,
569
607
  className: cn(
570
608
  "px-3 py-2 text-sm font-medium rounded-md transition-colors",
@@ -581,6 +619,566 @@ var UrlImport = ({
581
619
  error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive mt-1 absolute", children: error })
582
620
  ] });
583
621
  };
622
+ var ASPECT_PRESETS = [
623
+ { key: "square", label: "Square", hint: "1:1", size: "1024x1024" },
624
+ { key: "landscape", label: "Landscape", hint: "7:4", size: "1792x1024" },
625
+ { key: "portrait", label: "Portrait", hint: "4:7", size: "1024x1792" },
626
+ { key: "wide", label: "Wide", hint: "16:9", size: "1536x864" },
627
+ { key: "tall", label: "Tall", hint: "9:16", size: "864x1536" }
628
+ ];
629
+ var QUALITY_OPTIONS = [
630
+ { value: "standard", label: "Standard" },
631
+ { value: "hd", label: "HD" }
632
+ ];
633
+ var STYLE_OPTIONS = [
634
+ { value: "vivid", label: "Vivid" },
635
+ { value: "natural", label: "Natural" }
636
+ ];
637
+ function extractImageUrl(response) {
638
+ if (!response) return null;
639
+ if (typeof response === "string") return response;
640
+ if (response.url) return response.url;
641
+ if (response.imageUrl) return response.imageUrl;
642
+ if (response.image_url) return response.image_url;
643
+ if (Array.isArray(response.data) && response.data[0]?.url) return response.data[0].url;
644
+ if (Array.isArray(response.images) && response.images[0]?.url) return response.images[0].url;
645
+ if (response.b64_json) return `data:image/png;base64,${response.b64_json}`;
646
+ if (Array.isArray(response.data) && response.data[0]?.b64_json) {
647
+ return `data:image/png;base64,${response.data[0].b64_json}`;
648
+ }
649
+ return null;
650
+ }
651
+ var AIImageGenerate = ({
652
+ collectionId,
653
+ onSave,
654
+ saving,
655
+ className
656
+ }) => {
657
+ const [prompt, setPrompt] = useState("");
658
+ const [aspect, setAspect] = useState("square");
659
+ const [quality, setQuality] = useState("standard");
660
+ const [style, setStyle] = useState("vivid");
661
+ const [advancedOpen, setAdvancedOpen] = useState(false);
662
+ const [provider, setProvider] = useState("");
663
+ const [model, setModel] = useState("");
664
+ const [generating, setGenerating] = useState(false);
665
+ const [error, setError] = useState(null);
666
+ const [previewUrl, setPreviewUrl] = useState(null);
667
+ const [saved, setSaved] = useState(false);
668
+ const SpeechRecognitionCtor = typeof window !== "undefined" ? window.SpeechRecognition || window.webkitSpeechRecognition : null;
669
+ const voiceSupported = !!SpeechRecognitionCtor;
670
+ const recognitionRef = useRef(null);
671
+ const baseTextRef = useRef("");
672
+ const [listening, setListening] = useState(false);
673
+ const [voiceError, setVoiceError] = useState(null);
674
+ useEffect(() => {
675
+ return () => {
676
+ try {
677
+ recognitionRef.current?.stop();
678
+ } catch {
679
+ }
680
+ recognitionRef.current = null;
681
+ };
682
+ }, []);
683
+ const startListening = useCallback(() => {
684
+ if (!voiceSupported) return;
685
+ setVoiceError(null);
686
+ try {
687
+ const rec = new SpeechRecognitionCtor();
688
+ rec.continuous = true;
689
+ rec.interimResults = true;
690
+ rec.lang = typeof navigator !== "undefined" && navigator.language || "en-US";
691
+ baseTextRef.current = prompt ? prompt.replace(/\s+$/, "") + " " : "";
692
+ rec.onresult = (event) => {
693
+ let finalText = "";
694
+ let interimText = "";
695
+ for (let i = event.resultIndex; i < event.results.length; i++) {
696
+ const r = event.results[i];
697
+ if (r.isFinal) finalText += r[0].transcript;
698
+ else interimText += r[0].transcript;
699
+ }
700
+ if (finalText) {
701
+ baseTextRef.current = (baseTextRef.current + finalText).replace(/\s+$/, "") + " ";
702
+ setPrompt(baseTextRef.current);
703
+ } else {
704
+ setPrompt(baseTextRef.current + interimText);
705
+ }
706
+ };
707
+ rec.onerror = (e) => {
708
+ const code = e?.error || "unknown";
709
+ if (code === "not-allowed" || code === "service-not-allowed") {
710
+ setVoiceError("Microphone permission denied.");
711
+ } else if (code === "no-speech") {
712
+ setVoiceError("No speech detected.");
713
+ } else if (code !== "aborted") {
714
+ setVoiceError(`Voice input error: ${code}`);
715
+ }
716
+ };
717
+ rec.onend = () => {
718
+ setListening(false);
719
+ };
720
+ recognitionRef.current = rec;
721
+ rec.start();
722
+ setListening(true);
723
+ } catch (err) {
724
+ setVoiceError(err?.message || "Could not start voice input");
725
+ setListening(false);
726
+ }
727
+ }, [voiceSupported, prompt, SpeechRecognitionCtor]);
728
+ const stopListening = useCallback(() => {
729
+ try {
730
+ recognitionRef.current?.stop();
731
+ } catch {
732
+ }
733
+ setListening(false);
734
+ }, []);
735
+ const toggleListening = useCallback(() => {
736
+ if (listening) stopListening();
737
+ else startListening();
738
+ }, [listening, startListening, stopListening]);
739
+ const handleGenerate = useCallback(async () => {
740
+ if (!prompt.trim() || !collectionId) return;
741
+ setGenerating(true);
742
+ setError(null);
743
+ setPreviewUrl(null);
744
+ setSaved(false);
745
+ try {
746
+ const sizeValue = ASPECT_PRESETS.find((a) => a.key === aspect)?.size || "1024x1024";
747
+ const params = {
748
+ prompt: prompt.trim(),
749
+ size: sizeValue,
750
+ quality,
751
+ style
752
+ };
753
+ if (provider.trim()) params.provider = provider.trim();
754
+ if (model.trim()) params.model = model.trim();
755
+ const res = await SL.ai.generateImage(collectionId, params);
756
+ const url = extractImageUrl(res);
757
+ if (!url) {
758
+ setError("AI did not return an image URL.");
759
+ } else {
760
+ setPreviewUrl(url);
761
+ }
762
+ } catch (err) {
763
+ setError(err?.message || "Image generation failed");
764
+ } finally {
765
+ setGenerating(false);
766
+ }
767
+ }, [prompt, aspect, quality, style, provider, model, collectionId]);
768
+ const handleSave = useCallback(async () => {
769
+ if (!previewUrl) return;
770
+ const trimmed = prompt.trim().slice(0, 60).replace(/\s+/g, "-") || "ai-image";
771
+ const result = await onSave(previewUrl, `ai-${trimmed}.png`);
772
+ if (result) {
773
+ setSaved(true);
774
+ setTimeout(() => {
775
+ setPreviewUrl(null);
776
+ setSaved(false);
777
+ }, 1500);
778
+ }
779
+ }, [previewUrl, prompt, onSave]);
780
+ if (!collectionId) {
781
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-muted text-muted-foreground text-sm", children: [
782
+ /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
783
+ "AI image generation requires a collection scope."
784
+ ] });
785
+ }
786
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", className), children: [
787
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
788
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
789
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: "Prompt" }),
790
+ voiceSupported && /* @__PURE__ */ jsxs(
791
+ "button",
792
+ {
793
+ type: "button",
794
+ onClick: toggleListening,
795
+ disabled: generating,
796
+ title: listening ? "Stop dictation" : "Dictate prompt",
797
+ className: cn(
798
+ "flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-md border transition-colors",
799
+ listening ? "border-destructive text-destructive bg-destructive/10 animate-pulse" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted"
800
+ ),
801
+ children: [
802
+ listening ? /* @__PURE__ */ jsx(MicOff, { className: "w-3 h-3" }) : /* @__PURE__ */ jsx(Mic, { className: "w-3 h-3" }),
803
+ listening ? "Listening\u2026" : "Voice"
804
+ ]
805
+ }
806
+ )
807
+ ] }),
808
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
809
+ /* @__PURE__ */ jsx(
810
+ "textarea",
811
+ {
812
+ value: prompt,
813
+ onChange: (e) => setPrompt(e.target.value),
814
+ onKeyDown: (e) => {
815
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
816
+ e.preventDefault();
817
+ handleGenerate();
818
+ }
819
+ },
820
+ placeholder: "A serene mountain lake at golden hour, photorealistic, 35mm film\u2026",
821
+ rows: 3,
822
+ disabled: generating,
823
+ className: "w-full px-3 py-2 text-sm rounded-md border border-border bg-transparent placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring resize-none"
824
+ }
825
+ ),
826
+ listening && /* @__PURE__ */ jsxs("span", { className: "absolute top-2 right-2 flex items-center gap-1 text-[10px] text-destructive", children: [
827
+ /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-destructive animate-pulse" }),
828
+ "REC"
829
+ ] })
830
+ ] }),
831
+ /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground", children: [
832
+ "Tip: \u2318/Ctrl + Enter to generate",
833
+ voiceSupported ? " \xB7 click Voice to dictate" : ""
834
+ ] }),
835
+ voiceError && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-destructive", children: voiceError })
836
+ ] }),
837
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
838
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: "Aspect ratio" }),
839
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: ASPECT_PRESETS.map((a) => {
840
+ const active = aspect === a.key;
841
+ return /* @__PURE__ */ jsxs(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ onClick: () => setAspect(a.key),
846
+ disabled: generating,
847
+ className: cn(
848
+ "px-2.5 py-1 text-xs rounded-md border transition-colors flex items-center gap-1",
849
+ active ? "border-primary bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:text-foreground hover:bg-muted"
850
+ ),
851
+ children: [
852
+ /* @__PURE__ */ jsx("span", { children: a.label }),
853
+ /* @__PURE__ */ jsx("span", { className: cn("text-[10px]", active ? "opacity-80" : "opacity-60"), children: a.hint })
854
+ ]
855
+ },
856
+ a.key
857
+ );
858
+ }) })
859
+ ] }),
860
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
861
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
862
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: "Quality" }),
863
+ /* @__PURE__ */ jsx(
864
+ "select",
865
+ {
866
+ value: quality,
867
+ onChange: (e) => setQuality(e.target.value),
868
+ disabled: generating,
869
+ className: "text-sm py-2 px-2 rounded-md border border-border bg-transparent text-foreground focus:outline-none focus:ring-1 focus:ring-ring cursor-pointer",
870
+ children: QUALITY_OPTIONS.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
871
+ }
872
+ )
873
+ ] }),
874
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
875
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: "Style" }),
876
+ /* @__PURE__ */ jsx(
877
+ "select",
878
+ {
879
+ value: style,
880
+ onChange: (e) => setStyle(e.target.value),
881
+ disabled: generating,
882
+ className: "text-sm py-2 px-2 rounded-md border border-border bg-transparent text-foreground focus:outline-none focus:ring-1 focus:ring-ring cursor-pointer",
883
+ children: STYLE_OPTIONS.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
884
+ }
885
+ )
886
+ ] })
887
+ ] }),
888
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
889
+ /* @__PURE__ */ jsxs(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ onClick: () => setAdvancedOpen((v) => !v),
894
+ className: "self-start flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors",
895
+ children: [
896
+ advancedOpen ? /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3" }),
897
+ "Advanced"
898
+ ]
899
+ }
900
+ ),
901
+ advancedOpen && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2 p-2 rounded-md border border-border bg-muted/20", children: [
902
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
903
+ /* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-foreground", children: "Provider" }),
904
+ /* @__PURE__ */ jsx(
905
+ "input",
906
+ {
907
+ type: "text",
908
+ value: provider,
909
+ onChange: (e) => setProvider(e.target.value),
910
+ placeholder: "e.g. openai, google, stability",
911
+ disabled: generating,
912
+ className: "text-xs py-1.5 px-2 rounded-md border border-border bg-transparent text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
913
+ }
914
+ )
915
+ ] }),
916
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
917
+ /* @__PURE__ */ jsx("label", { className: "text-[11px] font-medium text-foreground", children: "Model" }),
918
+ /* @__PURE__ */ jsx(
919
+ "input",
920
+ {
921
+ type: "text",
922
+ value: model,
923
+ onChange: (e) => setModel(e.target.value),
924
+ placeholder: "e.g. gpt-image-1, imagen-3",
925
+ disabled: generating,
926
+ className: "text-xs py-1.5 px-2 rounded-md border border-border bg-transparent text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
927
+ }
928
+ )
929
+ ] }),
930
+ /* @__PURE__ */ jsx("p", { className: "col-span-2 text-[10px] text-muted-foreground", children: "Leave blank to use the platform default. Quality and Style options may be ignored by some providers." })
931
+ ] })
932
+ ] }),
933
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(
934
+ "button",
935
+ {
936
+ type: "button",
937
+ onClick: handleGenerate,
938
+ disabled: !prompt.trim() || generating,
939
+ className: cn(
940
+ "px-4 py-2 text-sm font-medium rounded-md transition-colors",
941
+ "bg-primary text-primary-foreground hover:bg-primary/90",
942
+ "disabled:opacity-50 disabled:cursor-not-allowed",
943
+ "flex items-center gap-1.5"
944
+ ),
945
+ children: [
946
+ generating ? /* @__PURE__ */ jsx(Loader2, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Sparkles, { className: "w-3.5 h-3.5" }),
947
+ generating ? "Generating\u2026" : "Generate"
948
+ ]
949
+ }
950
+ ) }),
951
+ error && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-2.5 rounded-md bg-destructive/10 text-destructive text-xs", children: [
952
+ /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
953
+ error
954
+ ] }),
955
+ previewUrl && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 border border-border rounded-md p-3 bg-muted/30", children: [
956
+ /* @__PURE__ */ jsx("div", { className: "relative rounded overflow-hidden bg-background flex items-center justify-center", style: { minHeight: "12rem" }, children: /* @__PURE__ */ jsx(
957
+ "img",
958
+ {
959
+ src: previewUrl,
960
+ alt: prompt,
961
+ className: "max-w-full max-h-80 object-contain"
962
+ }
963
+ ) }),
964
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
965
+ /* @__PURE__ */ jsx(
966
+ "button",
967
+ {
968
+ type: "button",
969
+ onClick: () => {
970
+ setPreviewUrl(null);
971
+ setSaved(false);
972
+ },
973
+ disabled: saving,
974
+ className: "px-3 py-1.5 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors",
975
+ children: "Discard"
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsxs(
979
+ "button",
980
+ {
981
+ type: "button",
982
+ onClick: handleSave,
983
+ disabled: saving || saved,
984
+ className: cn(
985
+ "px-3 py-1.5 text-xs font-medium rounded-md transition-colors flex items-center gap-1.5",
986
+ saved ? "bg-primary text-primary-foreground" : "bg-primary text-primary-foreground hover:bg-primary/90",
987
+ "disabled:opacity-70 disabled:cursor-not-allowed"
988
+ ),
989
+ children: [
990
+ saving ? /* @__PURE__ */ jsx(Loader2, { className: "w-3 h-3 animate-spin" }) : saved ? /* @__PURE__ */ jsx(Check, { className: "w-3 h-3" }) : null,
991
+ saved ? "Saved" : saving ? "Saving\u2026" : "Save to library"
992
+ ]
993
+ }
994
+ )
995
+ ] })
996
+ ] })
997
+ ] });
998
+ };
999
+ var ORIENTATIONS = [
1000
+ { value: "", label: "Any shape" },
1001
+ { value: "landscape", label: "Landscape" },
1002
+ { value: "portrait", label: "Portrait" },
1003
+ { value: "squarish", label: "Square" }
1004
+ ];
1005
+ function getThumb(p) {
1006
+ return p.thumbUrl || p.thumbnailUrl || p.thumbnail || p.previewUrl || p.urls?.small || p.urls?.thumb || p.src?.medium || p.src?.small || p.url;
1007
+ }
1008
+ function getFullUrl(p) {
1009
+ return p.urls?.full || p.urls?.regular || p.src?.original || p.src?.large || p.url;
1010
+ }
1011
+ var StockPhotoSearch = ({
1012
+ collectionId,
1013
+ onSave,
1014
+ saving,
1015
+ className
1016
+ }) => {
1017
+ const [query, setQuery] = useState("");
1018
+ const [orientation, setOrientation] = useState("");
1019
+ const [searching, setSearching] = useState(false);
1020
+ const [error, setError] = useState(null);
1021
+ const [results, setResults] = useState([]);
1022
+ const [savedUrl, setSavedUrl] = useState(null);
1023
+ const [pendingUrl, setPendingUrl] = useState(null);
1024
+ const handleSearch = useCallback(async () => {
1025
+ if (!query.trim() || !collectionId) return;
1026
+ setSearching(true);
1027
+ setError(null);
1028
+ setResults([]);
1029
+ setSavedUrl(null);
1030
+ try {
1031
+ const params = {
1032
+ query: query.trim(),
1033
+ per_page: 24
1034
+ };
1035
+ if (orientation) params.orientation = orientation;
1036
+ const res = await SL.ai.searchPhotos(collectionId, params);
1037
+ const list = Array.isArray(res) ? res : res?.photos || res?.results || [];
1038
+ setResults(list);
1039
+ if (list.length === 0) {
1040
+ setError("No photos matched your search.");
1041
+ }
1042
+ } catch (err) {
1043
+ setError(err?.message || "Stock photo search failed");
1044
+ } finally {
1045
+ setSearching(false);
1046
+ }
1047
+ }, [query, orientation, collectionId]);
1048
+ const handleSave = useCallback(async (photo) => {
1049
+ const fullUrl = getFullUrl(photo);
1050
+ setPendingUrl(fullUrl);
1051
+ const namePart = (photo.alt || query.trim() || "stock-photo").slice(0, 60).replace(/\s+/g, "-");
1052
+ const result = await onSave(fullUrl, `stock-${namePart}.jpg`);
1053
+ setPendingUrl(null);
1054
+ if (result) {
1055
+ setSavedUrl(fullUrl);
1056
+ setTimeout(() => setSavedUrl(null), 1500);
1057
+ }
1058
+ }, [onSave, query]);
1059
+ if (!collectionId) {
1060
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-muted text-muted-foreground text-sm", children: [
1061
+ /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
1062
+ "Stock photo search requires a collection scope."
1063
+ ] });
1064
+ }
1065
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", className), children: [
1066
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 flex-wrap", children: [
1067
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-[12rem]", children: [
1068
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }),
1069
+ /* @__PURE__ */ jsx(
1070
+ "input",
1071
+ {
1072
+ type: "text",
1073
+ value: query,
1074
+ onChange: (e) => setQuery(e.target.value),
1075
+ onKeyDown: (e) => {
1076
+ if (e.key === "Enter") {
1077
+ e.preventDefault();
1078
+ e.stopPropagation();
1079
+ handleSearch();
1080
+ }
1081
+ },
1082
+ placeholder: "Search free stock photos\u2026",
1083
+ disabled: searching,
1084
+ className: "w-full pl-8 pr-3 py-2 text-sm rounded-md border border-border bg-transparent placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
1085
+ }
1086
+ )
1087
+ ] }),
1088
+ /* @__PURE__ */ jsx(
1089
+ "select",
1090
+ {
1091
+ value: orientation,
1092
+ onChange: (e) => setOrientation(e.target.value),
1093
+ disabled: searching,
1094
+ className: "text-sm py-2 px-2 rounded-md border border-border bg-transparent text-foreground focus:outline-none focus:ring-1 focus:ring-ring cursor-pointer",
1095
+ children: ORIENTATIONS.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
1096
+ }
1097
+ ),
1098
+ /* @__PURE__ */ jsxs(
1099
+ "button",
1100
+ {
1101
+ type: "button",
1102
+ onClick: handleSearch,
1103
+ disabled: !query.trim() || searching,
1104
+ className: cn(
1105
+ "px-4 py-2 text-sm font-medium rounded-md transition-colors",
1106
+ "bg-primary text-primary-foreground hover:bg-primary/90",
1107
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1108
+ "flex items-center gap-1.5"
1109
+ ),
1110
+ children: [
1111
+ searching ? /* @__PURE__ */ jsx(Loader2, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Search, { className: "w-3.5 h-3.5" }),
1112
+ searching ? "Searching\u2026" : "Search"
1113
+ ]
1114
+ }
1115
+ )
1116
+ ] }),
1117
+ error && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-2.5 rounded-md bg-destructive/10 text-destructive text-xs", children: [
1118
+ /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
1119
+ error
1120
+ ] }),
1121
+ results.length > 0 && /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2", children: results.map((photo, i) => {
1122
+ const thumb = getThumb(photo);
1123
+ const full = getFullUrl(photo);
1124
+ const isPending = pendingUrl === full;
1125
+ const isSaved = savedUrl === full;
1126
+ return /* @__PURE__ */ jsxs(
1127
+ "div",
1128
+ {
1129
+ className: "group relative aspect-square rounded-md overflow-hidden border border-border bg-muted/30",
1130
+ children: [
1131
+ /* @__PURE__ */ jsx(
1132
+ "img",
1133
+ {
1134
+ src: thumb,
1135
+ alt: photo.alt || "",
1136
+ loading: "lazy",
1137
+ className: "w-full h-full object-cover"
1138
+ }
1139
+ ),
1140
+ /* @__PURE__ */ jsx(
1141
+ "button",
1142
+ {
1143
+ type: "button",
1144
+ onClick: () => handleSave(photo),
1145
+ disabled: !!pendingUrl || saving,
1146
+ className: cn(
1147
+ "absolute inset-0 flex items-center justify-center text-xs font-medium",
1148
+ "bg-background/80 opacity-0 group-hover:opacity-100 transition-opacity",
1149
+ "text-foreground gap-1.5",
1150
+ (isPending || isSaved) && "opacity-100"
1151
+ ),
1152
+ children: isPending ? /* @__PURE__ */ jsxs(Fragment, { children: [
1153
+ /* @__PURE__ */ jsx(Loader2, { className: "w-3.5 h-3.5 animate-spin" }),
1154
+ " Saving\u2026"
1155
+ ] }) : isSaved ? /* @__PURE__ */ jsxs(Fragment, { children: [
1156
+ /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5" }),
1157
+ " Saved"
1158
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1159
+ /* @__PURE__ */ jsx(Image, { className: "w-3.5 h-3.5" }),
1160
+ " Save"
1161
+ ] })
1162
+ }
1163
+ ),
1164
+ photo.photographer && /* @__PURE__ */ jsx("div", { className: "absolute bottom-0 inset-x-0 px-1.5 py-0.5 text-[10px] text-background bg-foreground/60 truncate", children: photo.photographerUrl ? /* @__PURE__ */ jsx(
1165
+ "a",
1166
+ {
1167
+ href: photo.photographerUrl,
1168
+ target: "_blank",
1169
+ rel: "noreferrer noopener",
1170
+ onClick: (e) => e.stopPropagation(),
1171
+ className: "hover:underline",
1172
+ children: photo.photographer
1173
+ }
1174
+ ) : photo.photographer })
1175
+ ]
1176
+ },
1177
+ `${full}-${i}`
1178
+ );
1179
+ }) })
1180
+ ] });
1181
+ };
584
1182
  var ScopedAssetBrowser = ({ scope, accept, pageSize, viewMode, search, selectedIds, onToggleSelect, onDoubleClickSelect, onDelete, allowDelete, emptyText }) => {
585
1183
  const { assets, loading, error, refresh } = useAssets({ scope, accept, pageSize });
586
1184
  const filteredAssets = useMemo(() => {
@@ -597,7 +1195,7 @@ var ScopedAssetBrowser = ({ scope, accept, pageSize, viewMode, search, selectedI
597
1195
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 rounded-md bg-destructive/10 text-destructive text-sm", children: [
598
1196
  /* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
599
1197
  error,
600
- /* @__PURE__ */ jsx("button", { onClick: refresh, className: "ml-auto underline text-xs", children: "Retry" })
1198
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: refresh, className: "ml-auto underline text-xs", children: "Retry" })
601
1199
  ] });
602
1200
  }
603
1201
  if (filteredAssets.length === 0) {
@@ -625,6 +1223,8 @@ var AssetPickerContent = ({
625
1223
  productScope,
626
1224
  allowUpload = true,
627
1225
  allowUrlImport = true,
1226
+ allowAIGenerate,
1227
+ allowStockPhotos,
628
1228
  multiple = false,
629
1229
  accept: acceptProp,
630
1230
  showTypeFilter,
@@ -636,7 +1236,7 @@ var AssetPickerContent = ({
636
1236
  pageSize = 50,
637
1237
  onConfirm
638
1238
  }) => {
639
- const { assets, upload, uploadFromUrl, uploading, uploadProgress } = useAssets({
1239
+ const { assets, upload, uploadFromUrl, uploadFromRemoteUrl, uploading, uploadProgress } = useAssets({
640
1240
  scope,
641
1241
  accept: acceptProp,
642
1242
  pageSize
@@ -657,6 +1257,10 @@ var AssetPickerContent = ({
657
1257
  return entry?.prefix;
658
1258
  }, [acceptProp, mimeFilter]);
659
1259
  const shouldShowFilter = showTypeFilter ?? !acceptProp;
1260
+ const collectionId = scope.collectionId;
1261
+ const acceptsImages = !acceptProp || acceptProp.startsWith("image");
1262
+ const aiEnabled = (allowAIGenerate ?? acceptsImages) && !!collectionId;
1263
+ const stockEnabled = (allowStockPhotos ?? acceptsImages) && !!collectionId;
660
1264
  const activeScope = useMemo(() => {
661
1265
  if (hasProductScope && scopeTab === "product") {
662
1266
  return { type: "product", collectionId: productScope.collectionId, productId: productScope.productId };
@@ -715,6 +1319,14 @@ var AssetPickerContent = ({
715
1319
  }
716
1320
  return result;
717
1321
  }, [uploadFromUrl, multiple, onSelect, toSelection]);
1322
+ const handleRemoteIngest = useCallback(async (url, name) => {
1323
+ const result = await uploadFromRemoteUrl(url, { name });
1324
+ if (result && !multiple) {
1325
+ setSelectedIds(/* @__PURE__ */ new Set([result.id]));
1326
+ onSelect?.(toSelection(result));
1327
+ }
1328
+ return result;
1329
+ }, [uploadFromRemoteUrl, multiple, onSelect, toSelection]);
718
1330
  const handleDelete = useCallback(async (assetId) => {
719
1331
  setSelectedIds((prev) => {
720
1332
  const next = new Set(prev);
@@ -738,13 +1350,16 @@ var AssetPickerContent = ({
738
1350
  const tabs = [
739
1351
  { key: "browse", label: "Browse", show: true },
740
1352
  { key: "upload", label: "Upload", show: allowUpload },
741
- { key: "url", label: "URL", show: allowUrlImport }
1353
+ { key: "url", label: "URL", show: allowUrlImport },
1354
+ { key: "ai", label: "AI Generate", show: aiEnabled },
1355
+ { key: "stock", label: "Stock", show: stockEnabled }
742
1356
  ];
743
1357
  return /* @__PURE__ */ jsxs("div", { className: "smartlinks-ui-asset-picker-content flex flex-col gap-3", children: [
744
1358
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
745
1359
  /* @__PURE__ */ jsx("div", { className: "flex gap-1 bg-muted rounded-md p-0.5", children: tabs.filter((t) => t.show).map((t) => /* @__PURE__ */ jsx(
746
1360
  "button",
747
1361
  {
1362
+ type: "button",
748
1363
  onClick: () => setTab(t.key),
749
1364
  className: cn(
750
1365
  "px-2.5 py-1 text-xs font-medium rounded transition-colors flex items-center gap-1",
@@ -784,6 +1399,7 @@ var AssetPickerContent = ({
784
1399
  /* @__PURE__ */ jsx(
785
1400
  "button",
786
1401
  {
1402
+ type: "button",
787
1403
  onClick: () => setViewMode("grid"),
788
1404
  className: cn("p-1 rounded", viewMode === "grid" ? "bg-background shadow-sm" : ""),
789
1405
  title: "Grid view",
@@ -793,6 +1409,7 @@ var AssetPickerContent = ({
793
1409
  /* @__PURE__ */ jsx(
794
1410
  "button",
795
1411
  {
1412
+ type: "button",
796
1413
  onClick: () => setViewMode("list"),
797
1414
  className: cn("p-1 rounded", viewMode === "list" ? "bg-background shadow-sm" : ""),
798
1415
  title: "List view",
@@ -805,6 +1422,7 @@ var AssetPickerContent = ({
805
1422
  /* @__PURE__ */ jsx(
806
1423
  "button",
807
1424
  {
1425
+ type: "button",
808
1426
  onClick: () => setScopeTab("collection"),
809
1427
  className: cn(
810
1428
  "px-3 py-1.5 text-xs font-medium border-b-2 transition-colors -mb-px",
@@ -816,6 +1434,7 @@ var AssetPickerContent = ({
816
1434
  /* @__PURE__ */ jsx(
817
1435
  "button",
818
1436
  {
1437
+ type: "button",
819
1438
  onClick: () => setScopeTab("product"),
820
1439
  className: cn(
821
1440
  "px-3 py-1.5 text-xs font-medium border-b-2 transition-colors -mb-px",
@@ -859,6 +1478,22 @@ var AssetPickerContent = ({
859
1478
  importing: uploading
860
1479
  }
861
1480
  ),
1481
+ tab === "ai" && aiEnabled && /* @__PURE__ */ jsx(
1482
+ AIImageGenerate,
1483
+ {
1484
+ collectionId,
1485
+ onSave: handleRemoteIngest,
1486
+ saving: uploading
1487
+ }
1488
+ ),
1489
+ tab === "stock" && stockEnabled && /* @__PURE__ */ jsx(
1490
+ StockPhotoSearch,
1491
+ {
1492
+ collectionId,
1493
+ onSave: handleRemoteIngest,
1494
+ saving: uploading
1495
+ }
1496
+ ),
862
1497
  onConfirm && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-border", children: [
863
1498
  /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
864
1499
  selectedIds.size,
@@ -868,6 +1503,7 @@ var AssetPickerContent = ({
868
1503
  /* @__PURE__ */ jsx(
869
1504
  "button",
870
1505
  {
1506
+ type: "button",
871
1507
  onClick: handleConfirm,
872
1508
  disabled: selectedIds.size === 0,
873
1509
  className: cn(
@@ -891,20 +1527,31 @@ var PickerDialog = ({ open, onClose, maxWidth, children }) => {
891
1527
  onClick: onClose
892
1528
  }
893
1529
  ),
894
- /* @__PURE__ */ jsxs("div", { className: "relative z-10 w-full max-h-[85vh] bg-background rounded-xl shadow-2xl border border-border flex flex-col overflow-hidden mx-4", style: { maxWidth: maxWidth || "56rem" }, children: [
895
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border", children: [
896
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Select Asset" }),
897
- /* @__PURE__ */ jsx(
898
- "button",
899
- {
900
- onClick: onClose,
901
- className: "p-1 rounded hover:bg-accent transition-colors",
902
- children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-muted-foreground" })
903
- }
904
- )
905
- ] }),
906
- /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-4", children })
907
- ] })
1530
+ /* @__PURE__ */ jsxs(
1531
+ "div",
1532
+ {
1533
+ className: "relative z-10 w-full max-h-[85vh] bg-background rounded-xl shadow-2xl border border-border flex flex-col overflow-hidden mx-4",
1534
+ style: { maxWidth: maxWidth || "56rem" },
1535
+ onClick: (e) => e.stopPropagation(),
1536
+ onMouseDown: (e) => e.stopPropagation(),
1537
+ onTouchStart: (e) => e.stopPropagation(),
1538
+ children: [
1539
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border", children: [
1540
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Select Asset" }),
1541
+ /* @__PURE__ */ jsx(
1542
+ "button",
1543
+ {
1544
+ type: "button",
1545
+ onClick: onClose,
1546
+ className: "p-1 rounded hover:bg-accent transition-colors",
1547
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-muted-foreground" })
1548
+ }
1549
+ )
1550
+ ] }),
1551
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-4", children })
1552
+ ]
1553
+ }
1554
+ )
908
1555
  ] });
909
1556
  };
910
1557
  var AssetPicker = (props) => {
@@ -936,5 +1583,5 @@ var AssetPicker = (props) => {
936
1583
  };
937
1584
 
938
1585
  export { ASSET_MIME_FILTERS, AssetPicker, useAssets };
939
- //# sourceMappingURL=chunk-ECAGO3HU.js.map
940
- //# sourceMappingURL=chunk-ECAGO3HU.js.map
1586
+ //# sourceMappingURL=chunk-XA5J6CZL.js.map
1587
+ //# sourceMappingURL=chunk-XA5J6CZL.js.map