@ewjdev/anyclick-react 3.0.0 → 4.0.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.
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  useEffect as useEffect4,
7
7
  useId,
8
8
  useLayoutEffect,
9
- useMemo as useMemo3,
9
+ useMemo as useMemo4,
10
10
  useRef as useRef4,
11
11
  useState as useState5
12
12
  } from "react";
@@ -34,10 +34,11 @@ import {
34
34
  import React, {
35
35
  useCallback as useCallback2,
36
36
  useEffect as useEffect2,
37
- useMemo,
37
+ useMemo as useMemo2,
38
38
  useRef as useRef2,
39
39
  useState as useState2
40
40
  } from "react";
41
+ import { buildAnyclickPayload } from "@ewjdev/anyclick-core";
41
42
  import {
42
43
  AlertCircle,
43
44
  ChevronDown,
@@ -231,6 +232,7 @@ var quickChatStyles = {
231
232
  // Suggestions - compact horizontal scroll
232
233
  suggestionsContainer: {
233
234
  display: "flex",
235
+ flexDirection: "column",
234
236
  gap: "6px",
235
237
  padding: "6px 8px",
236
238
  overflowX: "auto",
@@ -493,8 +495,131 @@ var quickChatKeyframes = `
493
495
  `;
494
496
 
495
497
  // src/QuickChat/useQuickChat.ts
496
- import { useCallback, useEffect, useRef, useState } from "react";
497
- import { getElementInspectInfo } from "@ewjdev/anyclick-core";
498
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
499
+ import { useChat } from "@ai-sdk/react";
500
+ import { DefaultChatTransport } from "ai";
501
+
502
+ // src/QuickChat/store.ts
503
+ import { create } from "zustand";
504
+ import { createJSONStorage, persist } from "zustand/middleware";
505
+ var STORE_NAME = "anyclick-quick-chat-store";
506
+ var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
507
+ function generateMessageId() {
508
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
509
+ }
510
+ var storage = createJSONStorage(() => localStorage);
511
+ function filterExpiredMessages(messages) {
512
+ const now = Date.now();
513
+ return messages.filter((msg) => msg.expiresAt > now);
514
+ }
515
+ var useQuickChatStore = create()(
516
+ persist(
517
+ (set, get) => ({
518
+ messages: [],
519
+ isPinned: false,
520
+ input: "",
521
+ contextChunks: [],
522
+ suggestedPrompts: [],
523
+ isLoadingSuggestions: false,
524
+ isSending: false,
525
+ isStreaming: false,
526
+ error: null,
527
+ lastSyncedAt: null,
528
+ setInput: (input) => set({ input }),
529
+ addMessage: (message) => {
530
+ const id = generateMessageId();
531
+ const now = Date.now();
532
+ const storedMessage = {
533
+ ...message,
534
+ id,
535
+ timestamp: now,
536
+ expiresAt: now + TWENTY_FOUR_HOURS_MS
537
+ };
538
+ set((state) => {
539
+ const newMessages = [...state.messages, storedMessage];
540
+ return {
541
+ messages: newMessages
542
+ };
543
+ });
544
+ return id;
545
+ },
546
+ updateMessage: (id, updates) => {
547
+ set((state) => ({
548
+ messages: state.messages.map(
549
+ (msg) => msg.id === id ? { ...msg, ...updates } : msg
550
+ )
551
+ }));
552
+ },
553
+ clearMessages: () => set({ messages: [], error: null }),
554
+ setIsPinned: (pinned) => set({ isPinned: pinned }),
555
+ setContextChunks: (chunks) => set({ contextChunks: chunks }),
556
+ toggleChunk: (chunkId) => {
557
+ set((state) => ({
558
+ contextChunks: state.contextChunks.map(
559
+ (chunk) => chunk.id === chunkId ? { ...chunk, included: !chunk.included } : chunk
560
+ )
561
+ }));
562
+ },
563
+ toggleAllChunks: (included) => {
564
+ set((state) => ({
565
+ contextChunks: state.contextChunks.map((chunk) => ({
566
+ ...chunk,
567
+ included
568
+ }))
569
+ }));
570
+ },
571
+ setSuggestedPrompts: (prompts) => set({ suggestedPrompts: prompts }),
572
+ setIsLoadingSuggestions: (loading) => set({ isLoadingSuggestions: loading }),
573
+ setIsSending: (sending) => set({ isSending: sending }),
574
+ setIsStreaming: (streaming) => set({ isStreaming: streaming }),
575
+ setError: (error) => set({ error }),
576
+ setLastSyncedAt: (timestamp) => set({ lastSyncedAt: timestamp }),
577
+ pruneExpiredMessages: () => {
578
+ set((state) => ({
579
+ messages: filterExpiredMessages(state.messages)
580
+ }));
581
+ },
582
+ getActiveMessages: () => {
583
+ const state = get();
584
+ const now = Date.now();
585
+ const active = state.messages.filter((msg) => msg.expiresAt > now).map(({ expiresAt, ...msg }) => msg);
586
+ console.log("[store] getActiveMessages", {
587
+ totalMessages: state.messages.length,
588
+ activeMessages: active.length,
589
+ activeIds: active.map((m) => m.id)
590
+ });
591
+ return active;
592
+ },
593
+ hydrate: (messages) => {
594
+ const now = Date.now();
595
+ const storedMessages = messages.map((msg) => ({
596
+ ...msg,
597
+ expiresAt: now + TWENTY_FOUR_HOURS_MS
598
+ }));
599
+ set({ messages: storedMessages, lastSyncedAt: now });
600
+ }
601
+ }),
602
+ {
603
+ name: STORE_NAME,
604
+ storage,
605
+ partialize: (state) => ({
606
+ messages: state.messages,
607
+ isPinned: state.isPinned,
608
+ lastSyncedAt: state.lastSyncedAt
609
+ }),
610
+ onRehydrateStorage: () => (state) => {
611
+ if (state) {
612
+ state.pruneExpiredMessages();
613
+ }
614
+ }
615
+ }
616
+ )
617
+ );
618
+ function useActiveMessages() {
619
+ const messages = useQuickChatStore((state) => state.messages);
620
+ const now = Date.now();
621
+ return messages.filter((msg) => msg.expiresAt > now).map(({ expiresAt, ...msg }) => msg);
622
+ }
498
623
 
499
624
  // src/QuickChat/types.ts
500
625
  var DEFAULT_T3CHAT_CONFIG = {
@@ -504,9 +629,9 @@ var DEFAULT_T3CHAT_CONFIG = {
504
629
  };
505
630
  var DEFAULT_QUICK_CHAT_CONFIG = {
506
631
  endpoint: "/api/anyclick/chat",
507
- model: "gpt-5-mini",
632
+ model: "gpt-5-nano",
508
633
  prePassModel: "gpt-5-nano",
509
- maxResponseLength: 500,
634
+ maxResponseLength: 1e4,
510
635
  showRedactionUI: true,
511
636
  showSuggestions: true,
512
637
  systemPrompt: "You are a helpful assistant that provides quick, concise answers about web elements and UI. Keep responses brief and actionable.",
@@ -515,57 +640,9 @@ var DEFAULT_QUICK_CHAT_CONFIG = {
515
640
  t3chat: DEFAULT_T3CHAT_CONFIG
516
641
  };
517
642
 
518
- // src/QuickChat/useQuickChat.ts
519
- var PINNED_STATE_KEY = "anyclick-quick-chat-pinned";
520
- var CHAT_HISTORY_KEY = "anyclick-quick-chat-history";
521
- function generateId() {
522
- return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
523
- }
524
- function getPinnedState() {
525
- if (typeof window === "undefined") return false;
526
- try {
527
- return sessionStorage.getItem(PINNED_STATE_KEY) === "true";
528
- } catch {
529
- return false;
530
- }
531
- }
532
- function getChatHistory() {
533
- if (typeof window === "undefined") return [];
534
- try {
535
- const stored = sessionStorage.getItem(CHAT_HISTORY_KEY);
536
- if (stored) {
537
- const parsed = JSON.parse(stored);
538
- return parsed.map((msg) => ({
539
- ...msg,
540
- isStreaming: false,
541
- actions: void 0
542
- }));
543
- }
544
- } catch {
545
- }
546
- return [];
547
- }
548
- function saveChatHistory(messages) {
549
- if (typeof window === "undefined") return;
550
- try {
551
- const toStore = messages.slice(-10).map((msg) => ({
552
- id: msg.id,
553
- role: msg.role,
554
- content: msg.content,
555
- timestamp: msg.timestamp
556
- }));
557
- sessionStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(toStore));
558
- } catch {
559
- }
560
- }
561
- function clearChatHistory() {
562
- if (typeof window === "undefined") return;
563
- try {
564
- sessionStorage.removeItem(CHAT_HISTORY_KEY);
565
- } catch {
566
- }
567
- }
568
- function extractContextChunks(targetElement, containerElement) {
643
+ // src/QuickChat/useQuickChat.context.ts
644
+ import { getElementInspectInfo } from "@ewjdev/anyclick-core";
645
+ function extractContextChunks(targetElement, _containerElement) {
569
646
  const chunks = [];
570
647
  if (!targetElement) return chunks;
571
648
  try {
@@ -592,7 +669,7 @@ function extractContextChunks(targetElement, containerElement) {
592
669
  }
593
670
  if (info.computedStyles) {
594
671
  const styleEntries = [];
595
- for (const [category, styles] of Object.entries(info.computedStyles)) {
672
+ for (const [, styles] of Object.entries(info.computedStyles)) {
596
673
  if (styles && typeof styles === "object") {
597
674
  const entries = Object.entries(styles).slice(0, 2);
598
675
  for (const [k, v] of entries) {
@@ -630,7 +707,9 @@ function extractContextChunks(targetElement, containerElement) {
630
707
  }
631
708
  }
632
709
  if (info.boxModel) {
633
- const boxContent = `${Math.round(info.boxModel.content.width)}x${Math.round(info.boxModel.content.height)}px`;
710
+ const boxContent = `${Math.round(info.boxModel.content.width)}x${Math.round(
711
+ info.boxModel.content.height
712
+ )}px`;
634
713
  chunks.push({
635
714
  id: "element-dimensions",
636
715
  label: "Dimensions",
@@ -641,54 +720,358 @@ function extractContextChunks(targetElement, containerElement) {
641
720
  });
642
721
  }
643
722
  } catch (error) {
644
- console.error("Failed to extract context:", error);
723
+ console.error("[useQuickChat] Failed to extract context:", error);
645
724
  }
646
725
  return chunks;
647
726
  }
648
727
  function buildContextString(chunks) {
649
- const includedChunks = chunks.filter((c) => c.included);
650
- if (includedChunks.length === 0) return "";
651
- return includedChunks.map((c) => `[${c.label}]: ${c.content}`).join("\n");
728
+ const included = chunks.filter((c) => c.included);
729
+ if (included.length === 0) return "";
730
+ return included.map((c) => `[${c.label}]: ${c.content}`).join("\n");
731
+ }
732
+
733
+ // src/QuickChat/useQuickChat.debug.ts
734
+ function createDebugInfo(args) {
735
+ return {
736
+ status: args.status,
737
+ ok: args.ok,
738
+ contentType: args.contentType ?? null,
739
+ rawTextPreview: args.rawText ?? "",
740
+ timestamp: Date.now(),
741
+ error: args.error
742
+ };
743
+ }
744
+
745
+ // src/QuickChat/useQuickChat.messages.ts
746
+ function getUIMessageText(msg) {
747
+ const partsText = msg.parts?.map((p) => {
748
+ const part = p;
749
+ if (typeof part.text === "string") return part.text;
750
+ if (typeof part.content === "string") return part.content;
751
+ return "";
752
+ }).join("") ?? "";
753
+ if (partsText) return partsText;
754
+ const maybeContent = msg.content;
755
+ return typeof maybeContent === "string" ? maybeContent : "";
756
+ }
757
+ function chatMessagesToUiMessages(messages) {
758
+ return messages.map((msg) => ({
759
+ id: msg.id,
760
+ role: msg.role,
761
+ parts: [{ type: "text", text: msg.content }]
762
+ }));
763
+ }
764
+ function safeCopyToClipboard(text) {
765
+ try {
766
+ if (typeof navigator === "undefined") return;
767
+ void navigator.clipboard.writeText(text);
768
+ } catch {
769
+ }
770
+ }
771
+ function buildAssistantActions(messageText, setInput) {
772
+ return [
773
+ {
774
+ id: "copy",
775
+ label: "Copy",
776
+ onClick: () => safeCopyToClipboard(messageText)
777
+ },
778
+ {
779
+ id: "research",
780
+ label: "Research more",
781
+ onClick: () => setInput(`Tell me more about: ${messageText.slice(0, 50)}`)
782
+ }
783
+ ];
784
+ }
785
+ function uiMessagesToChatMessages(args) {
786
+ const { uiMessages, status, setInput } = args;
787
+ const last = uiMessages[uiMessages.length - 1];
788
+ return uiMessages.map((msg) => {
789
+ const text = getUIMessageText(msg);
790
+ const isStreaming = status === "streaming" && msg.role === "assistant" && msg === last;
791
+ const actions = msg.role === "assistant" && status === "ready" ? buildAssistantActions(text, setInput) : void 0;
792
+ return {
793
+ id: msg.id,
794
+ role: msg.role,
795
+ content: text,
796
+ timestamp: Date.now(),
797
+ isStreaming,
798
+ actions
799
+ };
800
+ });
801
+ }
802
+
803
+ // src/QuickChat/useQuickChat.rateLimit.ts
804
+ function safeJsonParse(text) {
805
+ try {
806
+ return JSON.parse(text);
807
+ } catch {
808
+ return null;
809
+ }
810
+ }
811
+ function formatRetryAt(retryAtMs) {
812
+ try {
813
+ const d = new Date(retryAtMs);
814
+ return new Intl.DateTimeFormat(void 0, {
815
+ hour: "numeric",
816
+ minute: "2-digit"
817
+ }).format(d);
818
+ } catch {
819
+ return new Date(retryAtMs).toLocaleTimeString();
820
+ }
821
+ }
822
+ function getRequestIdFromHeaders(res) {
823
+ return res?.headers?.get?.("X-Anyclick-Request-Id") ?? res?.headers?.get?.("x-anyclick-request-id") ?? void 0;
824
+ }
825
+ function parseRetryAfterSeconds(args) {
826
+ const { payload, res } = args;
827
+ if (typeof payload?.retryAfterSeconds === "number")
828
+ return payload.retryAfterSeconds;
829
+ const headerRetryAfter = res?.headers?.get?.("Retry-After");
830
+ if (!headerRetryAfter) return void 0;
831
+ const n = Number(headerRetryAfter);
832
+ return Number.isFinite(n) ? n : void 0;
652
833
  }
834
+ function parseRetryAt(args) {
835
+ const { payload, retryAfterSeconds, nowMs } = args;
836
+ if (typeof payload?.retryAt === "number" && Number.isFinite(payload.retryAt)) {
837
+ return payload.retryAt;
838
+ }
839
+ if (typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds)) {
840
+ return nowMs + Math.max(0, retryAfterSeconds) * 1e3;
841
+ }
842
+ return void 0;
843
+ }
844
+ function buildNotice(args) {
845
+ const { rawText, endpoint, res, nowMs } = args;
846
+ const parsed = safeJsonParse(rawText);
847
+ const payload = parsed && typeof parsed === "object" ? parsed : null;
848
+ const retryAfterSeconds = parseRetryAfterSeconds({ payload, res });
849
+ const retryAt = parseRetryAt({ payload, retryAfterSeconds, nowMs });
850
+ const timePart = retryAt ? `Try again at ${formatRetryAt(retryAt)}.` : "";
851
+ const message = timePart ? `Rate limited. ${timePart}` : "Rate limited.";
852
+ const requestId = payload?.requestId ?? getRequestIdFromHeaders(res);
853
+ return {
854
+ status: 429,
855
+ message,
856
+ retryAt,
857
+ retryAfterSeconds,
858
+ requestId,
859
+ endpoint,
860
+ raw: rawText
861
+ };
862
+ }
863
+ async function rateLimitNoticeFromResponse(res, endpoint) {
864
+ if (res.status !== 429) return null;
865
+ const raw = await res.text().catch(() => "");
866
+ return buildNotice({ rawText: raw, endpoint, res, nowMs: Date.now() });
867
+ }
868
+ function rateLimitNoticeFromError(args) {
869
+ const { statusCode, response, responseText, endpoint } = args;
870
+ if (statusCode !== 429) return null;
871
+ const raw = responseText ?? "";
872
+ return buildNotice({
873
+ rawText: raw,
874
+ endpoint,
875
+ res: response,
876
+ nowMs: Date.now()
877
+ });
878
+ }
879
+
880
+ // src/QuickChat/useQuickChat.ts
653
881
  function useQuickChat(targetElement, containerElement, config = {}) {
882
+ console.count("useQuickChat");
654
883
  const mergedConfig = { ...DEFAULT_QUICK_CHAT_CONFIG, ...config };
655
- const abortControllerRef = useRef(null);
656
884
  const initializedRef = useRef(false);
657
- const [state, setState] = useState({
658
- input: "",
659
- messages: [],
660
- isLoadingSuggestions: false,
661
- isSending: false,
662
- isStreaming: false,
663
- suggestedPrompts: [],
664
- contextChunks: [],
665
- error: null
885
+ const syncTimeoutRef = useRef(null);
886
+ const [manualSending, setManualSending] = useState(false);
887
+ const [debugInfo, setDebugInfo] = useState(null);
888
+ const [rateLimitNotice, setRateLimitNotice] = useState(null);
889
+ const {
890
+ input,
891
+ setInput,
892
+ isPinned,
893
+ setIsPinned,
894
+ contextChunks,
895
+ setContextChunks,
896
+ toggleChunk,
897
+ toggleAllChunks,
898
+ suggestedPrompts,
899
+ setSuggestedPrompts,
900
+ isLoadingSuggestions,
901
+ setIsLoadingSuggestions,
902
+ error,
903
+ setError,
904
+ addMessage,
905
+ clearMessages: storeClearMessages,
906
+ setLastSyncedAt,
907
+ lastSyncedAt,
908
+ setIsSending: setStoreIsSending,
909
+ setIsStreaming: setStoreIsStreaming
910
+ } = useQuickChatStore();
911
+ const storedMessages = useActiveMessages();
912
+ const contextString = useMemo(
913
+ () => buildContextString(contextChunks),
914
+ [contextChunks]
915
+ );
916
+ const chatBody = useMemo(
917
+ () => ({
918
+ action: "chat",
919
+ context: contextString,
920
+ model: mergedConfig.model,
921
+ systemPrompt: mergedConfig.systemPrompt,
922
+ maxLength: mergedConfig.maxResponseLength
923
+ }),
924
+ [
925
+ contextString,
926
+ mergedConfig.model,
927
+ mergedConfig.systemPrompt,
928
+ mergedConfig.maxResponseLength
929
+ ]
930
+ );
931
+ const transport = useMemo(
932
+ () => new DefaultChatTransport({
933
+ api: mergedConfig.endpoint,
934
+ body: chatBody
935
+ }),
936
+ [mergedConfig.endpoint, chatBody]
937
+ );
938
+ const handleRateLimitResponse = useCallback(
939
+ async (res, endpoint) => {
940
+ const notice = await rateLimitNoticeFromResponse(res, endpoint);
941
+ if (!notice) return false;
942
+ setRateLimitNotice(notice);
943
+ setError(null);
944
+ return true;
945
+ },
946
+ [setError]
947
+ );
948
+ const scheduleBackendSync = useCallback(() => {
949
+ if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
950
+ syncTimeoutRef.current = setTimeout(async () => {
951
+ try {
952
+ const currentMessages = useQuickChatStore.getState().getActiveMessages();
953
+ if (currentMessages.length === 0) return;
954
+ const endpoint = `${mergedConfig.endpoint}/history`;
955
+ const res = await fetch(endpoint, {
956
+ method: "POST",
957
+ headers: { "Content-Type": "application/json" },
958
+ body: JSON.stringify({
959
+ action: "save",
960
+ messages: currentMessages
961
+ })
962
+ });
963
+ if (await handleRateLimitResponse(res, endpoint)) return;
964
+ if (res.ok) setLastSyncedAt(Date.now());
965
+ } catch (err) {
966
+ console.error("[useQuickChat] Failed to sync chat history:", err);
967
+ }
968
+ }, 1e3);
969
+ }, [handleRateLimitResponse, mergedConfig.endpoint, setLastSyncedAt]);
970
+ const {
971
+ messages: aiMessages,
972
+ sendMessage: aiSendMessage,
973
+ status,
974
+ stop,
975
+ setMessages: setAiMessages
976
+ } = useChat({
977
+ transport,
978
+ onError: (err) => {
979
+ const response = err.response ?? void 0;
980
+ const statusCode = err.status ?? (response ? response.status : void 0) ?? -1;
981
+ const responseText = err.responseText ?? err.message ?? "";
982
+ setDebugInfo(
983
+ createDebugInfo({
984
+ status: statusCode,
985
+ ok: false,
986
+ contentType: response?.headers?.get?.("content-type") ?? null,
987
+ rawText: responseText,
988
+ error: err.message
989
+ })
990
+ );
991
+ setStoreIsStreaming(false);
992
+ setStoreIsSending(false);
993
+ const notice = rateLimitNoticeFromError({
994
+ statusCode,
995
+ response,
996
+ responseText,
997
+ endpoint: mergedConfig.endpoint
998
+ });
999
+ if (notice) {
1000
+ setRateLimitNotice(notice);
1001
+ setError(null);
1002
+ return;
1003
+ }
1004
+ setRateLimitNotice(null);
1005
+ setError(err.message);
1006
+ },
1007
+ onFinish: ({ message }) => {
1008
+ const messageText = getUIMessageText(message);
1009
+ const current = useQuickChatStore.getState().getActiveMessages();
1010
+ const last = current[current.length - 1];
1011
+ const alreadyHaveSameTail = last?.role === "assistant" && last.content === messageText;
1012
+ if (!alreadyHaveSameTail && messageText) {
1013
+ addMessage({
1014
+ role: message.role,
1015
+ content: messageText,
1016
+ isStreaming: false
1017
+ });
1018
+ }
1019
+ scheduleBackendSync();
1020
+ setStoreIsStreaming(false);
1021
+ setStoreIsSending(false);
1022
+ }
666
1023
  });
1024
+ const loadFromBackend = useCallback(async () => {
1025
+ try {
1026
+ const endpoint = `${mergedConfig.endpoint}/history`;
1027
+ const response = await fetch(endpoint, {
1028
+ method: "POST",
1029
+ headers: { "Content-Type": "application/json" },
1030
+ body: JSON.stringify({ action: "load" })
1031
+ });
1032
+ if (await handleRateLimitResponse(response, endpoint)) return;
1033
+ if (!response.ok) return;
1034
+ const data = await response.json().catch(() => null);
1035
+ if (!data?.messages || !Array.isArray(data.messages)) return;
1036
+ if (data.messages.length === 0) return;
1037
+ useQuickChatStore.getState().hydrate(data.messages);
1038
+ setAiMessages(chatMessagesToUiMessages(data.messages));
1039
+ } catch (err) {
1040
+ console.error("[useQuickChat] Failed to load chat history:", err);
1041
+ }
1042
+ }, [handleRateLimitResponse, mergedConfig.endpoint, setAiMessages]);
1043
+ const messages = useMemo(
1044
+ () => uiMessagesToChatMessages({
1045
+ uiMessages: aiMessages,
1046
+ status,
1047
+ setInput
1048
+ }),
1049
+ [aiMessages, setInput, status]
1050
+ );
1051
+ const isStreaming = status === "streaming";
1052
+ const isSending = status === "submitted" || status === "streaming" || manualSending;
667
1053
  useEffect(() => {
668
1054
  if (initializedRef.current) return;
669
1055
  initializedRef.current = true;
670
- const isPinned = getPinnedState();
671
- if (isPinned) {
672
- const history = getChatHistory();
673
- if (history.length > 0) {
674
- setState((prev) => ({ ...prev, messages: history }));
675
- }
1056
+ if (storedMessages.length > 0) {
1057
+ setAiMessages(chatMessagesToUiMessages(storedMessages));
1058
+ } else {
1059
+ void loadFromBackend();
676
1060
  }
677
- }, []);
1061
+ }, [loadFromBackend, setAiMessages, storedMessages]);
678
1062
  useEffect(() => {
679
- if (targetElement) {
680
- const chunks = extractContextChunks(targetElement, containerElement);
681
- setState((prev) => ({ ...prev, contextChunks: chunks }));
682
- }
683
- }, [targetElement, containerElement]);
1063
+ if (!targetElement) return;
1064
+ const chunks = extractContextChunks(targetElement, containerElement);
1065
+ setContextChunks(chunks);
1066
+ }, [targetElement, containerElement, setContextChunks]);
684
1067
  useEffect(() => {
685
- if (!mergedConfig.showSuggestions || state.contextChunks.length === 0 || state.suggestedPrompts.length > 0) {
686
- return;
687
- }
1068
+ if (!mergedConfig.showSuggestions) return;
1069
+ if (!contextString) return;
1070
+ if (suggestedPrompts.length > 0) return;
1071
+ let cancelled = false;
688
1072
  const fetchSuggestions = async () => {
689
- setState((prev) => ({ ...prev, isLoadingSuggestions: true }));
1073
+ setIsLoadingSuggestions(true);
690
1074
  try {
691
- const contextString = buildContextString(state.contextChunks);
692
1075
  const response = await fetch(mergedConfig.endpoint, {
693
1076
  method: "POST",
694
1077
  headers: { "Content-Type": "application/json" },
@@ -698,250 +1081,142 @@ function useQuickChat(targetElement, containerElement, config = {}) {
698
1081
  model: mergedConfig.prePassModel
699
1082
  })
700
1083
  });
1084
+ if (await handleRateLimitResponse(response, mergedConfig.endpoint)) {
1085
+ return;
1086
+ }
701
1087
  if (response.ok) {
702
- const data = await response.json();
703
- if (data.suggestions && Array.isArray(data.suggestions)) {
704
- setState((prev) => ({
705
- ...prev,
706
- suggestedPrompts: data.suggestions.map(
707
- (text, i) => ({
1088
+ const data = await response.json().catch(() => null);
1089
+ if (data?.suggestions && Array.isArray(data.suggestions)) {
1090
+ if (!cancelled) {
1091
+ setSuggestedPrompts(
1092
+ data.suggestions.map((text, i) => ({
708
1093
  id: `suggestion-${i}`,
709
1094
  text
710
- })
711
- ),
712
- isLoadingSuggestions: false
713
- }));
1095
+ }))
1096
+ );
1097
+ setIsLoadingSuggestions(false);
1098
+ }
714
1099
  return;
715
1100
  }
716
1101
  }
717
- } catch (error) {
718
- console.error("Failed to fetch suggestions:", error);
1102
+ } catch (err) {
1103
+ console.error("[useQuickChat] Failed to fetch suggestions:", err);
719
1104
  }
720
- setState((prev) => ({
721
- ...prev,
722
- suggestedPrompts: [
1105
+ if (!cancelled) {
1106
+ setSuggestedPrompts([
723
1107
  { id: "s1", text: "What is this element?" },
724
1108
  { id: "s2", text: "How can I style this?" },
725
1109
  { id: "s3", text: "Is this accessible?" }
726
- ],
727
- isLoadingSuggestions: false
728
- }));
1110
+ ]);
1111
+ setIsLoadingSuggestions(false);
1112
+ }
1113
+ };
1114
+ void fetchSuggestions();
1115
+ return () => {
1116
+ cancelled = true;
729
1117
  };
730
- fetchSuggestions();
731
1118
  }, [
732
- state.contextChunks,
733
- state.suggestedPrompts.length,
734
- mergedConfig.showSuggestions,
1119
+ contextString,
1120
+ handleRateLimitResponse,
735
1121
  mergedConfig.endpoint,
736
- mergedConfig.prePassModel
1122
+ mergedConfig.prePassModel,
1123
+ mergedConfig.showSuggestions,
1124
+ setIsLoadingSuggestions,
1125
+ setSuggestedPrompts,
1126
+ suggestedPrompts.length
737
1127
  ]);
738
- const setInput = useCallback((value) => {
739
- setState((prev) => ({ ...prev, input: value }));
740
- }, []);
741
- const toggleChunk = useCallback((chunkId) => {
742
- setState((prev) => ({
743
- ...prev,
744
- contextChunks: prev.contextChunks.map(
745
- (chunk) => chunk.id === chunkId ? { ...chunk, included: !chunk.included } : chunk
746
- )
747
- }));
748
- }, []);
749
- const toggleAllChunks = useCallback((included) => {
750
- setState((prev) => ({
751
- ...prev,
752
- contextChunks: prev.contextChunks.map((chunk) => ({
753
- ...chunk,
754
- included
755
- }))
756
- }));
757
- }, []);
758
- const selectSuggestion = useCallback((prompt) => {
759
- setState((prev) => ({ ...prev, input: prompt.text }));
760
- }, []);
1128
+ const selectSuggestion = useCallback(
1129
+ (prompt) => {
1130
+ setInput(prompt.text);
1131
+ },
1132
+ [setInput]
1133
+ );
761
1134
  const sendMessage = useCallback(
762
1135
  async (messageText) => {
763
- const text = (messageText || state.input).trim();
1136
+ const text = (messageText || input).trim();
764
1137
  if (!text) return;
765
- if (abortControllerRef.current) {
766
- abortControllerRef.current.abort();
767
- }
768
- abortControllerRef.current = new AbortController();
769
- const userMessage = {
770
- id: generateId(),
771
- role: "user",
772
- content: text,
773
- timestamp: Date.now()
774
- };
775
- const assistantMessageId = generateId();
776
- const assistantMessage = {
777
- id: assistantMessageId,
778
- role: "assistant",
779
- content: "",
780
- timestamp: Date.now(),
781
- isStreaming: true
782
- };
783
- setState((prev) => ({
784
- ...prev,
785
- input: "",
786
- messages: [...prev.messages, userMessage, assistantMessage],
787
- isSending: true,
788
- isStreaming: true,
789
- error: null
790
- }));
1138
+ setInput("");
1139
+ setError(null);
1140
+ setManualSending(true);
1141
+ setStoreIsSending(true);
1142
+ setStoreIsStreaming(true);
1143
+ setDebugInfo(null);
791
1144
  try {
792
- const contextString = buildContextString(state.contextChunks);
793
- const response = await fetch(mergedConfig.endpoint, {
794
- method: "POST",
795
- headers: { "Content-Type": "application/json" },
796
- body: JSON.stringify({
797
- action: "chat",
798
- message: text,
799
- context: contextString,
800
- model: mergedConfig.model,
801
- systemPrompt: mergedConfig.systemPrompt,
802
- maxLength: mergedConfig.maxResponseLength
803
- }),
804
- signal: abortControllerRef.current.signal
1145
+ addMessage({
1146
+ role: "user",
1147
+ content: text
805
1148
  });
806
- if (!response.ok) {
807
- throw new Error(`Request failed: ${response.status}`);
808
- }
809
- const reader = response.body?.getReader();
810
- if (!reader) {
811
- throw new Error("No response body");
812
- }
813
- const decoder = new TextDecoder();
814
- let fullContent = "";
815
- while (true) {
816
- const { done, value } = await reader.read();
817
- if (done) break;
818
- const chunk = decoder.decode(value, { stream: true });
819
- fullContent += chunk;
820
- if (fullContent.length > mergedConfig.maxResponseLength) {
821
- fullContent = fullContent.slice(0, mergedConfig.maxResponseLength) + "...";
822
- }
823
- setState((prev) => ({
824
- ...prev,
825
- messages: prev.messages.map(
826
- (msg) => msg.id === assistantMessageId ? { ...msg, content: fullContent } : msg
827
- )
828
- }));
829
- }
830
- setState((prev) => ({
831
- ...prev,
832
- messages: prev.messages.map(
833
- (msg) => msg.id === assistantMessageId ? {
834
- ...msg,
835
- content: fullContent,
836
- isStreaming: false,
837
- actions: [
838
- {
839
- id: "copy",
840
- label: "Copy",
841
- onClick: () => {
842
- navigator.clipboard.writeText(fullContent);
843
- }
844
- },
845
- {
846
- id: "research",
847
- label: "Research more",
848
- onClick: () => {
849
- setState((p) => ({
850
- ...p,
851
- input: `Tell me more about: ${text}`
852
- }));
853
- }
854
- }
855
- ]
856
- } : msg
857
- ),
858
- isSending: false,
859
- isStreaming: false
860
- }));
861
- } catch (error) {
862
- if (error.name === "AbortError") {
863
- return;
864
- }
865
- console.error("Chat error:", error);
866
- setState((prev) => ({
867
- ...prev,
868
- messages: prev.messages.map(
869
- (msg) => msg.id === assistantMessageId ? {
870
- ...msg,
871
- content: "Sorry, I couldn't process your request. Please try again.",
872
- isStreaming: false
873
- } : msg
874
- ),
875
- isSending: false,
876
- isStreaming: false,
877
- error: error.message
878
- }));
1149
+ await aiSendMessage({ text });
1150
+ } catch (err) {
1151
+ const msg = err instanceof Error ? err.message : String(err);
1152
+ setError(msg);
1153
+ } finally {
1154
+ setManualSending(false);
1155
+ setStoreIsSending(false);
1156
+ setStoreIsStreaming(false);
879
1157
  }
880
1158
  },
881
- [state.input, state.contextChunks, mergedConfig]
1159
+ [
1160
+ addMessage,
1161
+ aiSendMessage,
1162
+ input,
1163
+ setError,
1164
+ setInput,
1165
+ setStoreIsSending,
1166
+ setStoreIsStreaming
1167
+ ]
882
1168
  );
883
1169
  const clearMessages = useCallback(() => {
884
- if (abortControllerRef.current) {
885
- abortControllerRef.current.abort();
886
- }
887
- clearChatHistory();
888
- setState((prev) => ({
889
- ...prev,
890
- messages: [],
891
- error: null
892
- }));
893
- }, []);
894
- useEffect(() => {
895
- if (state.messages.length > 0 && getPinnedState()) {
896
- const completedMessages = state.messages.filter(
897
- (msg) => !msg.isStreaming
898
- );
899
- if (completedMessages.length > 0) {
900
- saveChatHistory(completedMessages);
901
- }
902
- }
903
- }, [state.messages]);
1170
+ stop();
1171
+ storeClearMessages();
1172
+ setAiMessages([]);
1173
+ const endpoint = `${mergedConfig.endpoint}/history`;
1174
+ fetch(endpoint, {
1175
+ method: "POST",
1176
+ headers: { "Content-Type": "application/json" },
1177
+ body: JSON.stringify({ action: "clear" })
1178
+ }).then((res) => handleRateLimitResponse(res, endpoint)).catch(
1179
+ (err) => console.error("[useQuickChat] Failed to clear backend history:", err)
1180
+ );
1181
+ }, [
1182
+ handleRateLimitResponse,
1183
+ mergedConfig.endpoint,
1184
+ setAiMessages,
1185
+ stop,
1186
+ storeClearMessages
1187
+ ]);
904
1188
  useEffect(() => {
905
1189
  return () => {
906
- if (abortControllerRef.current) {
907
- abortControllerRef.current.abort();
908
- }
1190
+ if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
909
1191
  };
910
1192
  }, []);
911
1193
  return {
912
- ...state,
1194
+ input,
1195
+ messages,
1196
+ isLoadingSuggestions,
1197
+ isSending,
1198
+ isStreaming,
1199
+ suggestedPrompts,
1200
+ contextChunks,
1201
+ error,
1202
+ debugInfo,
1203
+ rateLimitNotice,
1204
+ isPinned,
1205
+ lastSyncedAt,
913
1206
  config: mergedConfig,
914
1207
  setInput,
915
1208
  toggleChunk,
916
1209
  toggleAllChunks,
917
1210
  selectSuggestion,
918
1211
  sendMessage,
919
- clearMessages
1212
+ clearMessages,
1213
+ setIsPinned,
1214
+ clearRateLimitNotice: () => setRateLimitNotice(null)
920
1215
  };
921
1216
  }
922
1217
 
923
1218
  // src/QuickChat/QuickChat.tsx
924
1219
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
925
- var PINNED_STATE_KEY2 = "anyclick-quick-chat-pinned";
926
- function getStoredPinnedState() {
927
- if (typeof window === "undefined") return false;
928
- try {
929
- return sessionStorage.getItem(PINNED_STATE_KEY2) === "true";
930
- } catch {
931
- return false;
932
- }
933
- }
934
- function setStoredPinnedState(pinned) {
935
- if (typeof window === "undefined") return;
936
- try {
937
- if (pinned) {
938
- sessionStorage.setItem(PINNED_STATE_KEY2, "true");
939
- } else {
940
- sessionStorage.removeItem(PINNED_STATE_KEY2);
941
- }
942
- } catch {
943
- }
944
- }
945
1220
  var stylesInjected = false;
946
1221
  function injectStyles() {
947
1222
  if (stylesInjected || typeof document === "undefined") return;
@@ -982,39 +1257,41 @@ function QuickChat({
982
1257
  const [hoveredSuggestion, setHoveredSuggestion] = useState2(
983
1258
  null
984
1259
  );
985
- const [localPinned, setLocalPinned] = useState2(() => getStoredPinnedState());
986
- const isPinned = isPinnedProp || localPinned;
987
- const handlePinToggle = useCallback2(() => {
988
- const newPinned = !isPinned;
989
- setLocalPinned(newPinned);
990
- setStoredPinnedState(newPinned);
991
- onPin?.(newPinned);
992
- }, [isPinned, onPin]);
993
- const handleClose = useCallback2(() => {
994
- if (isPinned) {
995
- setLocalPinned(false);
996
- setStoredPinnedState(false);
997
- onPin?.(false);
998
- }
999
- onClose();
1000
- }, [isPinned, onPin, onClose]);
1001
1260
  const {
1002
1261
  input,
1003
1262
  messages,
1004
1263
  isLoadingSuggestions,
1005
1264
  isSending,
1006
1265
  isStreaming,
1266
+ debugInfo,
1267
+ rateLimitNotice,
1007
1268
  suggestedPrompts,
1008
1269
  contextChunks,
1009
1270
  error,
1271
+ isPinned: storePinned,
1010
1272
  setInput,
1011
1273
  toggleChunk,
1012
1274
  toggleAllChunks,
1013
1275
  selectSuggestion,
1014
1276
  sendMessage,
1015
1277
  clearMessages,
1278
+ setIsPinned,
1279
+ clearRateLimitNotice,
1016
1280
  config: mergedConfig
1017
1281
  } = useQuickChat(targetElement, containerElement, config);
1282
+ const isPinned = isPinnedProp || storePinned;
1283
+ const handlePinToggle = useCallback2(() => {
1284
+ const newPinned = !isPinned;
1285
+ setIsPinned(newPinned);
1286
+ onPin?.(newPinned);
1287
+ }, [isPinned, setIsPinned, onPin]);
1288
+ const handleClose = useCallback2(() => {
1289
+ if (isPinned) {
1290
+ setIsPinned(false);
1291
+ onPin?.(false);
1292
+ }
1293
+ onClose();
1294
+ }, [isPinned, setIsPinned, onPin, onClose]);
1018
1295
  useEffect2(() => {
1019
1296
  injectStyles();
1020
1297
  }, []);
@@ -1077,10 +1354,65 @@ function QuickChat({
1077
1354
  const handleCopy = useCallback2((text) => {
1078
1355
  navigator.clipboard.writeText(text);
1079
1356
  }, []);
1080
- const includedCount = useMemo(
1357
+ const includedCount = useMemo2(
1081
1358
  () => contextChunks.filter((c) => c.included).length,
1082
1359
  [contextChunks]
1083
1360
  );
1361
+ const [rateLimitExpanded, setRateLimitExpanded] = useState2(false);
1362
+ const [reportStatus, setReportStatus] = useState2("idle");
1363
+ const [reportUrl, setReportUrl] = useState2(null);
1364
+ const [reportError, setReportError] = useState2(null);
1365
+ useEffect2(() => {
1366
+ if (rateLimitNotice) {
1367
+ setReportStatus("idle");
1368
+ setReportUrl(null);
1369
+ setReportError(null);
1370
+ setRateLimitExpanded(false);
1371
+ }
1372
+ }, [rateLimitNotice]);
1373
+ const handleReportIssue = useCallback2(async () => {
1374
+ if (!rateLimitNotice) return;
1375
+ if (!targetElement) {
1376
+ setReportStatus("error");
1377
+ setReportError("No target element available to report.");
1378
+ return;
1379
+ }
1380
+ setReportStatus("sending");
1381
+ setReportError(null);
1382
+ try {
1383
+ const payload = buildAnyclickPayload(targetElement, "issue", {
1384
+ comment: `QuickChat: ${rateLimitNotice.message}`,
1385
+ metadata: {
1386
+ source: "quickchat",
1387
+ kind: "rate_limit",
1388
+ endpoint: rateLimitNotice.endpoint ?? mergedConfig.endpoint,
1389
+ retryAt: rateLimitNotice.retryAt,
1390
+ retryAfterSeconds: rateLimitNotice.retryAfterSeconds,
1391
+ requestId: rateLimitNotice.requestId,
1392
+ debugInfo: debugInfo ?? void 0,
1393
+ raw: rateLimitNotice.raw ?? void 0
1394
+ }
1395
+ });
1396
+ const res = await fetch("/api/feedback", {
1397
+ method: "POST",
1398
+ headers: { "Content-Type": "application/json" },
1399
+ body: JSON.stringify(payload)
1400
+ });
1401
+ const json = await res.json().catch(() => null);
1402
+ if (!res.ok || !json?.success) {
1403
+ const msg = json?.error || (res.status ? `Failed to create issue (${res.status}).` : "Failed to create issue.");
1404
+ throw new Error(msg);
1405
+ }
1406
+ const firstUrl = json.results?.find(
1407
+ (r) => typeof r.url === "string"
1408
+ )?.url;
1409
+ setReportUrl(firstUrl ?? null);
1410
+ setReportStatus("sent");
1411
+ } catch (e) {
1412
+ setReportStatus("error");
1413
+ setReportError(e instanceof Error ? e.message : String(e));
1414
+ }
1415
+ }, [rateLimitNotice, targetElement, mergedConfig.endpoint, debugInfo]);
1084
1416
  if (!visible) return null;
1085
1417
  const containerStyles = isPinned ? {
1086
1418
  ...quickChatStyles.pinnedContainer,
@@ -1239,7 +1571,7 @@ function QuickChat({
1239
1571
  {
1240
1572
  style: isPinned ? quickChatStyles.pinnedMessagesArea : quickChatStyles.messagesArea,
1241
1573
  children: [
1242
- error && /* @__PURE__ */ jsxs("div", { style: quickChatStyles.errorContainer, children: [
1574
+ error && !rateLimitNotice && /* @__PURE__ */ jsxs("div", { style: quickChatStyles.errorContainer, children: [
1243
1575
  /* @__PURE__ */ jsx(AlertCircle, { size: 20, style: quickChatStyles.errorIcon }),
1244
1576
  /* @__PURE__ */ jsx("span", { style: quickChatStyles.errorText, children: error }),
1245
1577
  /* @__PURE__ */ jsxs(
@@ -1255,6 +1587,55 @@ function QuickChat({
1255
1587
  }
1256
1588
  )
1257
1589
  ] }),
1590
+ debugInfo && /* @__PURE__ */ jsxs(
1591
+ "div",
1592
+ {
1593
+ style: {
1594
+ backgroundColor: "#0f172a",
1595
+ color: "#e2e8f0",
1596
+ border: "1px solid #334155",
1597
+ borderRadius: "8px",
1598
+ padding: "8px",
1599
+ margin: "0 0 8px",
1600
+ fontSize: "12px",
1601
+ lineHeight: 1.4,
1602
+ wordBreak: "break-word"
1603
+ },
1604
+ children: [
1605
+ /* @__PURE__ */ jsxs(
1606
+ "div",
1607
+ {
1608
+ style: {
1609
+ display: "flex",
1610
+ justifyContent: "space-between",
1611
+ gap: "8px"
1612
+ },
1613
+ children: [
1614
+ /* @__PURE__ */ jsxs("span", { children: [
1615
+ "Last request: ",
1616
+ debugInfo.status,
1617
+ " ",
1618
+ debugInfo.ok ? "(ok)" : "(error)"
1619
+ ] }),
1620
+ /* @__PURE__ */ jsx("span", { style: { opacity: 0.7 }, children: new Date(debugInfo.timestamp).toLocaleTimeString() })
1621
+ ]
1622
+ }
1623
+ ),
1624
+ debugInfo.error && /* @__PURE__ */ jsxs("div", { style: { color: "#f87171", marginTop: "4px" }, children: [
1625
+ "Error: ",
1626
+ debugInfo.error
1627
+ ] }),
1628
+ debugInfo.contentPreview && /* @__PURE__ */ jsxs("div", { style: { marginTop: "4px" }, children: [
1629
+ "Content: ",
1630
+ debugInfo.contentPreview
1631
+ ] }),
1632
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: "4px", opacity: 0.8 }, children: [
1633
+ "Raw: ",
1634
+ debugInfo.rawTextPreview || "(empty)"
1635
+ ] })
1636
+ ]
1637
+ }
1638
+ ),
1258
1639
  messages.length > 0 && messages.map((message) => /* @__PURE__ */ jsxs(
1259
1640
  "div",
1260
1641
  {
@@ -1309,6 +1690,153 @@ function QuickChat({
1309
1690
  ]
1310
1691
  }
1311
1692
  ),
1693
+ rateLimitNotice && /* @__PURE__ */ jsxs(
1694
+ "div",
1695
+ {
1696
+ style: {
1697
+ borderTop: "1px solid rgba(148, 163, 184, 0.25)",
1698
+ background: "linear-gradient(180deg, rgba(15, 23, 42, 0.92), rgba(15, 23, 42, 0.96))",
1699
+ color: "#e2e8f0",
1700
+ padding: "8px 10px"
1701
+ },
1702
+ children: [
1703
+ /* @__PURE__ */ jsxs(
1704
+ "div",
1705
+ {
1706
+ style: {
1707
+ display: "flex",
1708
+ alignItems: "center",
1709
+ justifyContent: "space-between",
1710
+ gap: "8px"
1711
+ },
1712
+ children: [
1713
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1714
+ /* @__PURE__ */ jsx(AlertCircle, { size: 16, style: { color: "#fbbf24" } }),
1715
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "13px", lineHeight: 1.2 }, children: rateLimitNotice.message })
1716
+ ] }),
1717
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "6px" }, children: [
1718
+ /* @__PURE__ */ jsx(
1719
+ "button",
1720
+ {
1721
+ type: "button",
1722
+ onClick: () => setRateLimitExpanded((v) => !v),
1723
+ style: {
1724
+ border: "1px solid rgba(148, 163, 184, 0.25)",
1725
+ background: "rgba(30, 41, 59, 0.6)",
1726
+ color: "#e2e8f0",
1727
+ borderRadius: "6px",
1728
+ padding: "4px 8px",
1729
+ fontSize: "12px",
1730
+ cursor: "pointer"
1731
+ },
1732
+ children: rateLimitExpanded ? "Hide" : "Details"
1733
+ }
1734
+ ),
1735
+ /* @__PURE__ */ jsx(
1736
+ "button",
1737
+ {
1738
+ type: "button",
1739
+ onClick: handleReportIssue,
1740
+ disabled: reportStatus === "sending" || reportStatus === "sent",
1741
+ style: {
1742
+ border: "1px solid rgba(148, 163, 184, 0.25)",
1743
+ background: reportStatus === "sent" ? "rgba(34, 197, 94, 0.22)" : "rgba(30, 41, 59, 0.6)",
1744
+ color: "#e2e8f0",
1745
+ borderRadius: "6px",
1746
+ padding: "4px 8px",
1747
+ fontSize: "12px",
1748
+ cursor: reportStatus === "sending" || reportStatus === "sent" ? "not-allowed" : "pointer",
1749
+ opacity: reportStatus === "sending" ? 0.7 : 1
1750
+ },
1751
+ title: "Create a GitHub issue via /api/feedback",
1752
+ children: reportStatus === "sending" ? "Reporting..." : reportStatus === "sent" ? "Reported" : "Report"
1753
+ }
1754
+ ),
1755
+ /* @__PURE__ */ jsx(
1756
+ "button",
1757
+ {
1758
+ type: "button",
1759
+ onClick: () => {
1760
+ clearRateLimitNotice();
1761
+ setRateLimitExpanded(false);
1762
+ },
1763
+ style: {
1764
+ border: "none",
1765
+ background: "transparent",
1766
+ color: "rgba(226, 232, 240, 0.8)",
1767
+ padding: "4px",
1768
+ cursor: "pointer"
1769
+ },
1770
+ title: "Dismiss",
1771
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
1772
+ }
1773
+ )
1774
+ ] })
1775
+ ]
1776
+ }
1777
+ ),
1778
+ reportUrl && /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px", fontSize: "12px" }, children: [
1779
+ "Created:",
1780
+ " ",
1781
+ /* @__PURE__ */ jsx(
1782
+ "a",
1783
+ {
1784
+ href: reportUrl,
1785
+ target: "_blank",
1786
+ rel: "noopener noreferrer",
1787
+ style: { color: "#93c5fd" },
1788
+ children: "Open issue"
1789
+ }
1790
+ )
1791
+ ] }),
1792
+ reportError && /* @__PURE__ */ jsx(
1793
+ "div",
1794
+ {
1795
+ style: { marginTop: "6px", fontSize: "12px", color: "#fca5a5" },
1796
+ children: reportError
1797
+ }
1798
+ ),
1799
+ rateLimitExpanded && /* @__PURE__ */ jsxs(
1800
+ "div",
1801
+ {
1802
+ style: {
1803
+ marginTop: "8px",
1804
+ fontSize: "12px",
1805
+ lineHeight: 1.4,
1806
+ backgroundColor: "rgba(2, 6, 23, 0.55)",
1807
+ border: "1px solid rgba(148, 163, 184, 0.25)",
1808
+ borderRadius: "8px",
1809
+ padding: "8px",
1810
+ wordBreak: "break-word"
1811
+ },
1812
+ children: [
1813
+ /* @__PURE__ */ jsxs("div", { style: { opacity: 0.85 }, children: [
1814
+ "Status: ",
1815
+ rateLimitNotice.status,
1816
+ rateLimitNotice.requestId ? ` \u2022 Request: ${rateLimitNotice.requestId}` : ""
1817
+ ] }),
1818
+ rateLimitNotice.endpoint && /* @__PURE__ */ jsxs("div", { style: { opacity: 0.75, marginTop: "4px" }, children: [
1819
+ "Endpoint: ",
1820
+ rateLimitNotice.endpoint
1821
+ ] }),
1822
+ rateLimitNotice.retryAt && /* @__PURE__ */ jsxs("div", { style: { opacity: 0.75, marginTop: "4px" }, children: [
1823
+ "RetryAt: ",
1824
+ new Date(rateLimitNotice.retryAt).toLocaleString()
1825
+ ] }),
1826
+ rateLimitNotice.raw && /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px", opacity: 0.85 }, children: [
1827
+ "Raw: ",
1828
+ rateLimitNotice.raw
1829
+ ] }),
1830
+ debugInfo && /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px", opacity: 0.85 }, children: [
1831
+ "Debug: ",
1832
+ debugInfo.rawTextPreview || "(empty)"
1833
+ ] })
1834
+ ]
1835
+ }
1836
+ )
1837
+ ]
1838
+ }
1839
+ ),
1312
1840
  /* @__PURE__ */ jsxs("div", { style: quickChatStyles.inputContainer, children: [
1313
1841
  /* @__PURE__ */ jsx(
1314
1842
  "textarea",
@@ -1375,7 +1903,7 @@ function QuickChat({
1375
1903
  }
1376
1904
 
1377
1905
  // src/ScreenshotPreview.tsx
1378
- import React2, { useMemo as useMemo2, useState as useState3 } from "react";
1906
+ import React2, { useMemo as useMemo3, useState as useState3 } from "react";
1379
1907
  import { estimateTotalSize, formatBytes } from "@ewjdev/anyclick-core";
1380
1908
  import {
1381
1909
  AlertCircleIcon,
@@ -1875,7 +2403,7 @@ var ScreenshotPreview = React2.memo(function ScreenshotPreview2({
1875
2403
  const getError = (key) => {
1876
2404
  return screenshots?.errors?.[key];
1877
2405
  };
1878
- const tabs = useMemo2(() => {
2406
+ const tabs = useMemo3(() => {
1879
2407
  if (!screenshots) return [];
1880
2408
  const allTabs = [
1881
2409
  {
@@ -1899,7 +2427,7 @@ var ScreenshotPreview = React2.memo(function ScreenshotPreview2({
1899
2427
  ];
1900
2428
  return allTabs.filter((tab) => tab.data || tab.error);
1901
2429
  }, [screenshots]);
1902
- const totalSize = useMemo2(
2430
+ const totalSize = useMemo3(
1903
2431
  () => screenshots ? estimateTotalSize(screenshots) : 0,
1904
2432
  [screenshots]
1905
2433
  );
@@ -2917,12 +3445,12 @@ function useFeedback() {
2917
3445
  }
2918
3446
 
2919
3447
  // src/store.ts
2920
- import { create } from "zustand";
3448
+ import { create as create2 } from "zustand";
2921
3449
  var providerIdCounter = 0;
2922
3450
  function generateProviderId() {
2923
3451
  return `anyclick-provider-${++providerIdCounter}`;
2924
3452
  }
2925
- var useProviderStore = create((set, get) => ({
3453
+ var useProviderStore = create2((set, get) => ({
2926
3454
  providers: /* @__PURE__ */ new Map(),
2927
3455
  findProvidersForElement: (element) => {
2928
3456
  const { providers } = get();
@@ -3150,7 +3678,7 @@ function AnyclickProvider({
3150
3678
  updateProvider
3151
3679
  } = useProviderStore();
3152
3680
  const parentId = parentContext?.providerId ?? null;
3153
- const actualDepth = useMemo3(() => {
3681
+ const actualDepth = useMemo4(() => {
3154
3682
  if (!parentContext) return 0;
3155
3683
  let d = 0;
3156
3684
  let currentId = parentId;
@@ -3164,7 +3692,7 @@ function AnyclickProvider({
3164
3692
  }, [parentContext, parentId]);
3165
3693
  const isDisabledByTheme = theme === null || theme?.disabled === true;
3166
3694
  const effectiveDisabled = disabled || isDisabledByTheme;
3167
- const localTheme = useMemo3(() => {
3695
+ const localTheme = useMemo4(() => {
3168
3696
  if (theme === null) {
3169
3697
  return { disabled: true };
3170
3698
  }
@@ -3344,7 +3872,7 @@ function AnyclickProvider({
3344
3872
  [submitAnyclick, targetElement]
3345
3873
  );
3346
3874
  const inheritedTheme = getMergedTheme(providerId);
3347
- const mergedTheme = useMemo3(
3875
+ const mergedTheme = useMemo4(
3348
3876
  () => ({
3349
3877
  ...inheritedTheme,
3350
3878
  ...localTheme,
@@ -3367,7 +3895,7 @@ function AnyclickProvider({
3367
3895
  const effectiveMenuClassName = mergedTheme.menuClassName ?? menuClassName;
3368
3896
  const effectiveHighlightConfig = mergedTheme.highlightConfig ?? highlightConfig;
3369
3897
  const effectiveScreenshotConfig = mergedTheme.screenshotConfig ?? screenshotConfig;
3370
- const contextValue = useMemo3(
3898
+ const contextValue = useMemo4(
3371
3899
  () => ({
3372
3900
  closeMenu,
3373
3901
  isEnabled: !effectiveDisabled && !isDisabledByAncestor(providerId),
@@ -3399,7 +3927,7 @@ function AnyclickProvider({
3399
3927
  children
3400
3928
  }
3401
3929
  ) : children;
3402
- return /* @__PURE__ */ jsxs4(AnyclickContext.Provider, { value: contextValue, children: [
3930
+ return /* @__PURE__ */ jsx4(AnyclickContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs4("div", { "data-anyclick-root": true, children: [
3403
3931
  content,
3404
3932
  /* @__PURE__ */ jsx4(
3405
3933
  ContextMenu,
@@ -3420,12 +3948,12 @@ function AnyclickProvider({
3420
3948
  visible: menuVisible && !effectiveDisabled
3421
3949
  }
3422
3950
  )
3423
- ] });
3951
+ ] }) });
3424
3952
  }
3425
3953
  var FeedbackProvider = AnyclickProvider;
3426
3954
 
3427
3955
  // src/FunModeBridge.tsx
3428
- import { useEffect as useEffect5, useMemo as useMemo4, useRef as useRef5 } from "react";
3956
+ import { useEffect as useEffect5, useMemo as useMemo5, useRef as useRef5 } from "react";
3429
3957
  import {
3430
3958
  usePointer
3431
3959
  } from "@ewjdev/anyclick-pointer";
@@ -3476,7 +4004,7 @@ function FunModeBridge() {
3476
4004
  const providerStore = useProviderStore();
3477
4005
  const activeProviderRef = useRef5(null);
3478
4006
  const cachedConfigs = useRef5({});
3479
- const findActiveFunProvider = useMemo4(() => {
4007
+ const findActiveFunProvider = useMemo5(() => {
3480
4008
  return (el) => {
3481
4009
  if (!el) return null;
3482
4010
  const providers = providerStore.findProvidersForElement(el);
@@ -3522,11 +4050,71 @@ function FunModeBridge() {
3522
4050
  return null;
3523
4051
  }
3524
4052
 
4053
+ // src/ui/button.tsx
4054
+ import { forwardRef } from "react";
4055
+
4056
+ // src/utils/cn.ts
4057
+ function cn(...classes) {
4058
+ return classes.filter(Boolean).join(" ");
4059
+ }
4060
+
4061
+ // src/ui/button.tsx
4062
+ import { jsx as jsx5 } from "react/jsx-runtime";
4063
+ var variantClasses = {
4064
+ default: "ac-bg-accent ac-text-accent-foreground hover:ac-bg-accent-muted ac-border ac-border-border",
4065
+ ghost: "ac-bg-transparent hover:ac-bg-surface-muted ac-text-text",
4066
+ outline: "ac-bg-transparent ac-text-text ac-border ac-border-border hover:ac-bg-surface-muted",
4067
+ destructive: "ac-bg-destructive ac-text-accent-foreground hover:ac-bg-destructive/80"
4068
+ };
4069
+ var sizeClasses = {
4070
+ sm: "ac-h-8 ac-px-3 ac-text-sm",
4071
+ md: "ac-h-10 ac-px-4 ac-text-sm",
4072
+ lg: "ac-h-11 ac-px-5 ac-text-base"
4073
+ };
4074
+ var Button = forwardRef(
4075
+ ({ className, variant = "default", size = "md", ...props }, ref) => {
4076
+ return /* @__PURE__ */ jsx5(
4077
+ "button",
4078
+ {
4079
+ ref,
4080
+ className: cn(
4081
+ "ac-inline-flex ac-items-center ac-justify-center ac-gap-2 ac-rounded-md ac-font-medium ac-transition-colors focus-visible:ac-outline focus-visible:ac-outline-2 focus-visible:ac-outline-offset-2 focus-visible:ac-outline-accent disabled:ac-opacity-50 disabled:ac-cursor-not-allowed",
4082
+ variantClasses[variant],
4083
+ sizeClasses[size],
4084
+ className
4085
+ ),
4086
+ ...props
4087
+ }
4088
+ );
4089
+ }
4090
+ );
4091
+ Button.displayName = "Button";
4092
+
4093
+ // src/ui/input.tsx
4094
+ import { forwardRef as forwardRef2 } from "react";
4095
+ import { jsx as jsx6 } from "react/jsx-runtime";
4096
+ var Input = forwardRef2(
4097
+ ({ className, ...props }, ref) => {
4098
+ return /* @__PURE__ */ jsx6(
4099
+ "input",
4100
+ {
4101
+ ref,
4102
+ className: cn(
4103
+ "ac-h-10 ac-w-full ac-rounded-md ac-border ac-border-border ac-bg-surface ac-text-text ac-placeholder-text-muted ac-px-3 ac-py-2 ac-text-sm focus-visible:ac-outline focus-visible:ac-outline-2 focus-visible:ac-outline-offset-2 focus-visible:ac-outline-accent disabled:ac-opacity-50 disabled:ac-cursor-not-allowed",
4104
+ className
4105
+ ),
4106
+ ...props
4107
+ }
4108
+ );
4109
+ }
4110
+ );
4111
+ Input.displayName = "Input";
4112
+
3525
4113
  // src/InspectDialog/InspectDialogManager.tsx
3526
4114
  import { useCallback as useCallback5, useEffect as useEffect7, useState as useState7 } from "react";
3527
4115
 
3528
4116
  // src/InspectDialog/InspectSimple.tsx
3529
- import { useEffect as useEffect6, useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
4117
+ import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef6, useState as useState6 } from "react";
3530
4118
  import {
3531
4119
  captureScreenshot,
3532
4120
  getElementInspectInfo as getElementInspectInfo2
@@ -3633,7 +4221,7 @@ function formatSourceLocation(location) {
3633
4221
  }
3634
4222
 
3635
4223
  // src/InspectDialog/InspectSimple.tsx
3636
- import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
4224
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
3637
4225
  var DEFAULT_COMPACT_CONFIG = {
3638
4226
  scale: 0.5,
3639
4227
  fonts: {
@@ -3760,7 +4348,7 @@ function InspectSimple({
3760
4348
  document.removeEventListener("mousedown", handleClickOutside);
3761
4349
  };
3762
4350
  }, [visible, onClose]);
3763
- const identityLabel = useMemo5(() => {
4351
+ const identityLabel = useMemo6(() => {
3764
4352
  if (!info) return "Select an element";
3765
4353
  const classes = info.classNames[0] ? `.${info.classNames[0]}` : "";
3766
4354
  const id = info.id ? `#${info.id}` : "";
@@ -3847,7 +4435,7 @@ function InspectSimple({
3847
4435
  ...style
3848
4436
  };
3849
4437
  return /* @__PURE__ */ jsxs5(Fragment4, { children: [
3850
- /* @__PURE__ */ jsx5(
4438
+ /* @__PURE__ */ jsx7(
3851
4439
  "div",
3852
4440
  {
3853
4441
  style: {
@@ -3881,7 +4469,7 @@ function InspectSimple({
3881
4469
  borderBottom: "1px solid #1e293b"
3882
4470
  },
3883
4471
  children: [
3884
- isMobile && /* @__PURE__ */ jsx5(
4472
+ isMobile && /* @__PURE__ */ jsx7(
3885
4473
  "div",
3886
4474
  {
3887
4475
  style: {
@@ -3897,7 +4485,7 @@ function InspectSimple({
3897
4485
  }
3898
4486
  ),
3899
4487
  /* @__PURE__ */ jsxs5("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
3900
- /* @__PURE__ */ jsx5(
4488
+ /* @__PURE__ */ jsx7(
3901
4489
  "span",
3902
4490
  {
3903
4491
  style: {
@@ -3912,25 +4500,25 @@ function InspectSimple({
3912
4500
  children: identityLabel
3913
4501
  }
3914
4502
  ),
3915
- sourceLocation && /* @__PURE__ */ jsx5(
4503
+ sourceLocation && /* @__PURE__ */ jsx7(
3916
4504
  "button",
3917
4505
  {
3918
4506
  type: "button",
3919
4507
  onClick: handleOpenIDE,
3920
4508
  title: formatSourceLocation(sourceLocation),
3921
4509
  style: iconBtnStyle,
3922
- children: /* @__PURE__ */ jsx5(ExternalLink2, { size: 14 })
4510
+ children: /* @__PURE__ */ jsx7(ExternalLink2, { size: 14 })
3923
4511
  }
3924
4512
  )
3925
4513
  ] }),
3926
- /* @__PURE__ */ jsx5(
4514
+ /* @__PURE__ */ jsx7(
3927
4515
  "button",
3928
4516
  {
3929
4517
  type: "button",
3930
4518
  onClick: onClose,
3931
4519
  style: iconBtnStyle,
3932
4520
  "aria-label": "Close inspector",
3933
- children: /* @__PURE__ */ jsx5(X2, { size: 16 })
4521
+ children: /* @__PURE__ */ jsx7(X2, { size: 16 })
3934
4522
  }
3935
4523
  )
3936
4524
  ]
@@ -3946,7 +4534,7 @@ function InspectSimple({
3946
4534
  gap: 8
3947
4535
  },
3948
4536
  children: [
3949
- info?.selector && /* @__PURE__ */ jsx5(
4537
+ info?.selector && /* @__PURE__ */ jsx7(
3950
4538
  "code",
3951
4539
  {
3952
4540
  style: {
@@ -3961,7 +4549,7 @@ function InspectSimple({
3961
4549
  children: info.selector
3962
4550
  }
3963
4551
  ),
3964
- status && /* @__PURE__ */ jsx5(
4552
+ status && /* @__PURE__ */ jsx7(
3965
4553
  "div",
3966
4554
  {
3967
4555
  style: {
@@ -3982,7 +4570,7 @@ function InspectSimple({
3982
4570
  gap: 6
3983
4571
  },
3984
4572
  children: [
3985
- /* @__PURE__ */ jsx5(
4573
+ /* @__PURE__ */ jsx7(
3986
4574
  "button",
3987
4575
  {
3988
4576
  type: "button",
@@ -3990,10 +4578,10 @@ function InspectSimple({
3990
4578
  style: iconActionStyle,
3991
4579
  title: "Copy CSS selector",
3992
4580
  "aria-label": "Copy CSS selector",
3993
- children: /* @__PURE__ */ jsx5(Copy2, { size: 15 })
4581
+ children: /* @__PURE__ */ jsx7(Copy2, { size: 15 })
3994
4582
  }
3995
4583
  ),
3996
- /* @__PURE__ */ jsx5(
4584
+ /* @__PURE__ */ jsx7(
3997
4585
  "button",
3998
4586
  {
3999
4587
  type: "button",
@@ -4001,10 +4589,10 @@ function InspectSimple({
4001
4589
  style: iconActionStyle,
4002
4590
  title: "Copy text content",
4003
4591
  "aria-label": "Copy text content",
4004
- children: /* @__PURE__ */ jsx5(FileText, { size: 15 })
4592
+ children: /* @__PURE__ */ jsx7(FileText, { size: 15 })
4005
4593
  }
4006
4594
  ),
4007
- /* @__PURE__ */ jsx5(
4595
+ /* @__PURE__ */ jsx7(
4008
4596
  "button",
4009
4597
  {
4010
4598
  type: "button",
@@ -4012,10 +4600,10 @@ function InspectSimple({
4012
4600
  style: iconActionStyle,
4013
4601
  title: "Copy HTML markup",
4014
4602
  "aria-label": "Copy HTML markup",
4015
- children: /* @__PURE__ */ jsx5(Code, { size: 15 })
4603
+ children: /* @__PURE__ */ jsx7(Code, { size: 15 })
4016
4604
  }
4017
4605
  ),
4018
- /* @__PURE__ */ jsx5(
4606
+ /* @__PURE__ */ jsx7(
4019
4607
  "button",
4020
4608
  {
4021
4609
  type: "button",
@@ -4027,7 +4615,7 @@ function InspectSimple({
4027
4615
  disabled: saving,
4028
4616
  title: "Save screenshot",
4029
4617
  "aria-label": "Save screenshot",
4030
- children: /* @__PURE__ */ jsx5(Camera, { size: 15 })
4618
+ children: /* @__PURE__ */ jsx7(Camera, { size: 15 })
4031
4619
  }
4032
4620
  )
4033
4621
  ]
@@ -4070,7 +4658,7 @@ var iconActionStyle = {
4070
4658
  };
4071
4659
 
4072
4660
  // src/InspectDialog/InspectDialogManager.tsx
4073
- import { jsx as jsx6 } from "react/jsx-runtime";
4661
+ import { jsx as jsx8 } from "react/jsx-runtime";
4074
4662
  var INSPECT_DIALOG_EVENT = "anyclick:inspect";
4075
4663
  function openInspectDialog(targetElement) {
4076
4664
  if (typeof window === "undefined") return;
@@ -4115,7 +4703,7 @@ function InspectDialogManager({
4115
4703
  window.removeEventListener(INSPECT_DIALOG_EVENT, handleInspectEvent);
4116
4704
  };
4117
4705
  }, []);
4118
- return /* @__PURE__ */ jsx6(
4706
+ return /* @__PURE__ */ jsx8(
4119
4707
  InspectSimple,
4120
4708
  {
4121
4709
  visible,
@@ -4655,7 +5243,7 @@ import {
4655
5243
  } from "@ewjdev/anyclick-core";
4656
5244
 
4657
5245
  // src/AnyclickLogo.tsx
4658
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
5246
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
4659
5247
  function AnyclickLogo({
4660
5248
  size = 64,
4661
5249
  borderWidth = 2,
@@ -4681,7 +5269,7 @@ function AnyclickLogo({
4681
5269
  role: onClick ? "button" : "img",
4682
5270
  "aria-label": "Anyclick Logo",
4683
5271
  children: [
4684
- /* @__PURE__ */ jsx7(
5272
+ /* @__PURE__ */ jsx9(
4685
5273
  "circle",
4686
5274
  {
4687
5275
  cx: size / 2,
@@ -4692,11 +5280,11 @@ function AnyclickLogo({
4692
5280
  strokeWidth: borderWidth
4693
5281
  }
4694
5282
  ),
4695
- /* @__PURE__ */ jsx7(
5283
+ /* @__PURE__ */ jsx9(
4696
5284
  "g",
4697
5285
  {
4698
5286
  transform: `translate(${(size - cursorSize) / 2}, ${(size - cursorSize) / 2})`,
4699
- children: /* @__PURE__ */ jsx7(
5287
+ children: /* @__PURE__ */ jsx9(
4700
5288
  "path",
4701
5289
  {
4702
5290
  d: `
@@ -4740,6 +5328,7 @@ export {
4740
5328
  AnyclickContext,
4741
5329
  AnyclickLogo,
4742
5330
  AnyclickProvider,
5331
+ Button,
4743
5332
  CURATED_STYLE_PROPERTIES,
4744
5333
  ContextMenu,
4745
5334
  DEFAULT_COMPACT_CONFIG,
@@ -4750,6 +5339,7 @@ export {
4750
5339
  FeedbackProvider,
4751
5340
  FunModeBridge,
4752
5341
  INSPECT_DIALOG_EVENT,
5342
+ Input,
4753
5343
  InspectDialogManager,
4754
5344
  InspectSimple,
4755
5345
  QuickChat,