@tetrascience-npm/tetrascience-react-ui 0.5.0-beta.40.1 → 0.5.0-beta.42.1

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.
Files changed (99) hide show
  1. package/dist/components/ai/attachments.cjs +2 -0
  2. package/dist/components/ai/attachments.cjs.map +1 -0
  3. package/dist/components/ai/attachments.js +224 -0
  4. package/dist/components/ai/attachments.js.map +1 -0
  5. package/dist/components/ai/chain-of-thought.cjs +2 -0
  6. package/dist/components/ai/chain-of-thought.cjs.map +1 -0
  7. package/dist/components/ai/chain-of-thought.js +145 -0
  8. package/dist/components/ai/chain-of-thought.js.map +1 -0
  9. package/dist/components/ai/confirmation.cjs +2 -0
  10. package/dist/components/ai/confirmation.cjs.map +1 -0
  11. package/dist/components/ai/confirmation.js +109 -0
  12. package/dist/components/ai/confirmation.js.map +1 -0
  13. package/dist/components/ai/context.cjs +2 -0
  14. package/dist/components/ai/context.cjs.map +1 -0
  15. package/dist/components/ai/context.js +266 -0
  16. package/dist/components/ai/context.js.map +1 -0
  17. package/dist/components/ai/conversation.cjs +4 -0
  18. package/dist/components/ai/conversation.cjs.map +1 -0
  19. package/dist/components/ai/conversation.js +108 -0
  20. package/dist/components/ai/conversation.js.map +1 -0
  21. package/dist/components/ai/inline-citation.cjs +2 -0
  22. package/dist/components/ai/inline-citation.cjs.map +1 -0
  23. package/dist/components/ai/inline-citation.js +182 -0
  24. package/dist/components/ai/inline-citation.js.map +1 -0
  25. package/dist/components/ai/message.cjs +2 -0
  26. package/dist/components/ai/message.cjs.map +1 -0
  27. package/dist/components/ai/message.js +237 -0
  28. package/dist/components/ai/message.js.map +1 -0
  29. package/dist/components/ai/model-selector.cjs +2 -0
  30. package/dist/components/ai/model-selector.cjs.map +1 -0
  31. package/dist/components/ai/model-selector.js +77 -0
  32. package/dist/components/ai/model-selector.js.map +1 -0
  33. package/dist/components/ai/prompt-input.cjs +2 -0
  34. package/dist/components/ai/prompt-input.cjs.map +1 -0
  35. package/dist/components/ai/prompt-input.js +774 -0
  36. package/dist/components/ai/prompt-input.js.map +1 -0
  37. package/dist/components/ai/queue.cjs +2 -0
  38. package/dist/components/ai/queue.cjs.map +1 -0
  39. package/dist/components/ai/queue.js +209 -0
  40. package/dist/components/ai/queue.js.map +1 -0
  41. package/dist/components/ai/reasoning.cjs +2 -0
  42. package/dist/components/ai/reasoning.cjs.map +1 -0
  43. package/dist/components/ai/reasoning.js +129 -0
  44. package/dist/components/ai/reasoning.js.map +1 -0
  45. package/dist/components/ai/shimmer.cjs +2 -0
  46. package/dist/components/ai/shimmer.cjs.map +1 -0
  47. package/dist/components/ai/shimmer.js +49 -0
  48. package/dist/components/ai/shimmer.js.map +1 -0
  49. package/dist/components/ai/sources.cjs +2 -0
  50. package/dist/components/ai/sources.cjs.map +1 -0
  51. package/dist/components/ai/sources.js +54 -0
  52. package/dist/components/ai/sources.js.map +1 -0
  53. package/dist/components/ai/speech-input.cjs +2 -0
  54. package/dist/components/ai/speech-input.cjs.map +1 -0
  55. package/dist/components/ai/speech-input.js +123 -0
  56. package/dist/components/ai/speech-input.js.map +1 -0
  57. package/dist/components/ai/stream-status.cjs +2 -0
  58. package/dist/components/ai/stream-status.cjs.map +1 -0
  59. package/dist/components/ai/stream-status.js +106 -0
  60. package/dist/components/ai/stream-status.js.map +1 -0
  61. package/dist/components/ai/suggestion.cjs +2 -0
  62. package/dist/components/ai/suggestion.cjs.map +1 -0
  63. package/dist/components/ai/suggestion.js +38 -0
  64. package/dist/components/ai/suggestion.js.map +1 -0
  65. package/dist/components/ai/task.cjs +2 -0
  66. package/dist/components/ai/task.cjs.map +1 -0
  67. package/dist/components/ai/task.js +94 -0
  68. package/dist/components/ai/task.js.map +1 -0
  69. package/dist/components/ai/tool.cjs +2 -0
  70. package/dist/components/ai/tool.cjs.map +1 -0
  71. package/dist/components/ai/tool.js +143 -0
  72. package/dist/components/ai/tool.js.map +1 -0
  73. package/dist/components/composed/Chat/Chat.cjs +2 -0
  74. package/dist/components/composed/Chat/Chat.cjs.map +1 -0
  75. package/dist/components/composed/Chat/Chat.js +167 -0
  76. package/dist/components/composed/Chat/Chat.js.map +1 -0
  77. package/dist/components/ui/code-block.cjs +4 -0
  78. package/dist/components/ui/code-block.cjs.map +1 -0
  79. package/dist/components/ui/code-block.js +306 -0
  80. package/dist/components/ui/code-block.js.map +1 -0
  81. package/dist/components/ui/data-table/data-table-filter.cjs +2 -0
  82. package/dist/components/ui/data-table/data-table-filter.cjs.map +1 -0
  83. package/dist/components/ui/data-table/data-table-filter.js +178 -0
  84. package/dist/components/ui/data-table/data-table-filter.js.map +1 -0
  85. package/dist/components/ui/data-table/data-table.cjs +1 -1
  86. package/dist/components/ui/data-table/data-table.cjs.map +1 -1
  87. package/dist/components/ui/data-table/data-table.js +244 -195
  88. package/dist/components/ui/data-table/data-table.js.map +1 -1
  89. package/dist/components/ui/progress.cjs +2 -0
  90. package/dist/components/ui/progress.cjs.map +1 -0
  91. package/dist/components/ui/progress.js +32 -0
  92. package/dist/components/ui/progress.js.map +1 -0
  93. package/dist/index.cjs +1 -1
  94. package/dist/index.css +1 -1
  95. package/dist/index.d.ts +1182 -1
  96. package/dist/index.js +572 -368
  97. package/dist/index.js.map +1 -1
  98. package/dist/index.tailwind.css +1 -1
  99. package/package.json +12 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning.js","sources":["../../../src/components/ai/reasoning.tsx"],"sourcesContent":["import { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { cjk } from \"@streamdown/cjk\";\nimport { code } from \"@streamdown/code\";\nimport { math } from \"@streamdown/math\";\nimport { mermaid } from \"@streamdown/mermaid\";\nimport { BrainIcon, ChevronDownIcon } from \"lucide-react\";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { Streamdown } from \"streamdown\";\n\nimport { Shimmer } from \"./shimmer\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ReasoningContextValue {\n isStreaming: boolean;\n isOpen: boolean;\n setIsOpen: (open: boolean) => void;\n duration: number | undefined;\n}\n\nconst ReasoningContext = createContext<ReasoningContextValue | null>(null);\n\nexport const useReasoning = () => {\n const context = useContext(ReasoningContext);\n if (!context) {\n throw new Error(\"Reasoning components must be used within Reasoning\");\n }\n return context;\n};\n\nexport type ReasoningProps = ComponentProps<typeof Collapsible> & {\n isStreaming?: boolean;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n duration?: number;\n};\n\nconst AUTO_CLOSE_DELAY = 1000;\nconst MS_IN_S = 1000;\n\nexport const Reasoning = memo(\n ({\n className,\n isStreaming = false,\n open,\n defaultOpen,\n onOpenChange,\n duration: durationProp,\n children,\n ...props\n }: ReasoningProps) => {\n const resolvedDefaultOpen = defaultOpen ?? isStreaming;\n // Track if defaultOpen was explicitly set to false (to prevent auto-open)\n const isExplicitlyClosed = defaultOpen === false;\n\n const [isOpen, setIsOpen] = useControllableState<boolean>({\n defaultProp: resolvedDefaultOpen,\n onChange: onOpenChange,\n prop: open,\n });\n const [duration, setDuration] = useControllableState<number | undefined>({\n defaultProp: undefined,\n prop: durationProp,\n });\n\n const hasEverStreamedRef = useRef(isStreaming);\n const [hasAutoClosed, setHasAutoClosed] = useState(false);\n const startTimeRef = useRef<number | null>(null);\n\n // Track when streaming starts and compute duration\n useEffect(() => {\n if (isStreaming) {\n hasEverStreamedRef.current = true;\n if (startTimeRef.current === null) {\n startTimeRef.current = Date.now();\n }\n } else if (startTimeRef.current !== null) {\n setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));\n startTimeRef.current = null;\n }\n }, [isStreaming, setDuration]);\n\n // Auto-open when streaming starts (unless explicitly closed)\n useEffect(() => {\n if (isStreaming && !isOpen && !isExplicitlyClosed) {\n setIsOpen(true);\n }\n }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);\n\n // Auto-close when streaming ends (once only, and only if it ever streamed)\n useEffect(() => {\n if (\n hasEverStreamedRef.current &&\n !isStreaming &&\n isOpen &&\n !hasAutoClosed\n ) {\n const timer = setTimeout(() => {\n setIsOpen(false);\n setHasAutoClosed(true);\n }, AUTO_CLOSE_DELAY);\n\n return () => clearTimeout(timer);\n }\n }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);\n\n const handleOpenChange = useCallback(\n (newOpen: boolean) => {\n setIsOpen(newOpen);\n },\n [setIsOpen]\n );\n\n const contextValue = useMemo(\n () => ({ duration, isOpen, isStreaming, setIsOpen }),\n [duration, isOpen, isStreaming, setIsOpen]\n );\n\n return (\n <ReasoningContext.Provider value={contextValue}>\n <Collapsible\n className={cn(\"not-prose mb-4\", className)}\n onOpenChange={handleOpenChange}\n open={isOpen}\n {...props}\n >\n {children}\n </Collapsible>\n </ReasoningContext.Provider>\n );\n }\n);\n\nexport type ReasoningTriggerProps = ComponentProps<\n typeof CollapsibleTrigger\n> & {\n getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;\n};\n\nconst defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {\n if (isStreaming || duration === 0) {\n return <Shimmer duration={1}>Thinking...</Shimmer>;\n }\n if (duration === undefined) {\n return <p>Thought for a few seconds</p>;\n }\n return <p>Thought for {duration} seconds</p>;\n};\n\nexport const ReasoningTrigger = memo(\n ({\n className,\n children,\n getThinkingMessage = defaultGetThinkingMessage,\n ...props\n }: ReasoningTriggerProps) => {\n const { isStreaming, isOpen, duration } = useReasoning();\n\n return (\n <CollapsibleTrigger\n className={cn(\n \"group flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground\",\n className\n )}\n {...props}\n >\n {children ?? (\n <>\n <BrainIcon className=\"size-4\" />\n {getThinkingMessage(isStreaming, duration)}\n <ChevronDownIcon\n className={cn(\n \"size-4 opacity-0 transition-all group-focus-visible:opacity-100 group-hover:opacity-100\",\n isOpen ? \"rotate-180 opacity-100\" : \"rotate-0\"\n )}\n data-slot=\"collapsible-chevron\"\n />\n </>\n )}\n </CollapsibleTrigger>\n );\n }\n);\n\nexport type ReasoningContentProps = ComponentProps<\n typeof CollapsibleContent\n> & {\n children: string;\n};\n\nconst streamdownPlugins = { cjk, code, math, mermaid };\n\nexport const ReasoningContent = memo(\n ({ className, children, ...props }: ReasoningContentProps) => (\n <CollapsibleContent\n className={cn(\n \"mt-4 text-sm\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className\n )}\n {...props}\n >\n <Streamdown plugins={streamdownPlugins}>{children}</Streamdown>\n </CollapsibleContent>\n )\n);\n\nReasoning.displayName = \"Reasoning\";\nReasoningTrigger.displayName = \"ReasoningTrigger\";\nReasoningContent.displayName = \"ReasoningContent\";\n"],"names":["ReasoningContext","createContext","useReasoning","context","useContext","AUTO_CLOSE_DELAY","MS_IN_S","Reasoning","memo","className","isStreaming","open","defaultOpen","onOpenChange","durationProp","children","props","resolvedDefaultOpen","isExplicitlyClosed","isOpen","setIsOpen","useControllableState","duration","setDuration","hasEverStreamedRef","useRef","hasAutoClosed","setHasAutoClosed","useState","startTimeRef","useEffect","timer","handleOpenChange","useCallback","newOpen","contextValue","useMemo","jsx","Collapsible","cn","defaultGetThinkingMessage","Shimmer","ReasoningTrigger","getThinkingMessage","CollapsibleTrigger","jsxs","Fragment","BrainIcon","ChevronDownIcon","streamdownPlugins","cjk","code","math","mermaid","ReasoningContent","CollapsibleContent","Streamdown"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("react/jsx-runtime"),d=require("motion/react"),a=require("react"),l=require("../../lib/utils.cjs"),r=new Map,p=e=>{let t=r.get(e);return t||(t=d.motion.create(e),r.set(e,t)),t},b="linear-gradient(90deg, #549DFF, #8243BA, #9665F4)",S=({children:e,as:t="p",className:i,duration:c=2,spread:n=2,gradient:o})=>{const s=p(t),g=a.useMemo(()=>(e?.length??0)*n,[e,n]),m=o||"linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))";return u.jsx(s,{animate:{backgroundPosition:"0% center"},className:l.cn("relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent","[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",i),initial:{backgroundPosition:"100% center"},style:{"--spread":`${g}px`,backgroundImage:`var(--bg), ${m}`},transition:{duration:c,ease:"linear",repeat:Number.POSITIVE_INFINITY},children:e})},I=a.memo(S);exports.Shimmer=I;exports.TS_SHIMMER_GRADIENT=b;
2
+ //# sourceMappingURL=shimmer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shimmer.cjs","sources":["../../../src/components/ai/shimmer.tsx"],"sourcesContent":["import { motion } from \"motion/react\";\nimport { memo, useMemo } from \"react\";\n\nimport type { MotionProps } from \"motion/react\";\nimport type { CSSProperties, ElementType, JSX } from \"react\";\n\n\nimport { cn } from \"@/lib/utils\";\n\ntype MotionHTMLProps = MotionProps & Record<string, unknown>;\n\n// Cache motion components at module level to avoid creating during render\nconst motionComponentCache = new Map<\n keyof JSX.IntrinsicElements,\n React.ComponentType<MotionHTMLProps>\n>();\n\nconst getMotionComponent = (element: keyof JSX.IntrinsicElements) => {\n let component = motionComponentCache.get(element);\n if (!component) {\n component = motion.create(element);\n motionComponentCache.set(element, component);\n }\n return component;\n};\n\n/** TetraScience brand gradient — Light Blue 300 → Purple 500 → Violet Marble */\nexport const TS_SHIMMER_GRADIENT =\n \"linear-gradient(90deg, #549DFF, #8243BA, #9665F4)\";\n\nexport interface TextShimmerProps {\n children: string;\n as?: ElementType;\n className?: string;\n duration?: number;\n spread?: number;\n /**\n * CSS gradient used as the base text colour. Defaults to `muted-foreground`.\n * Pass `TS_SHIMMER_GRADIENT` (or any custom gradient) to colour the text\n * with the brand blue→purple sweep.\n */\n gradient?: string;\n}\n\nconst ShimmerComponent = ({\n children,\n as: Component = \"p\",\n className,\n duration = 2,\n spread = 2,\n gradient,\n}: TextShimmerProps) => {\n const MotionComponent = getMotionComponent(\n Component as keyof JSX.IntrinsicElements\n );\n\n const dynamicSpread = useMemo(\n () => (children?.length ?? 0) * spread,\n [children, spread]\n );\n\n const baseGradient = gradient\n ? gradient\n : \"linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))\";\n\n return (\n <MotionComponent\n animate={{ backgroundPosition: \"0% center\" }}\n className={cn(\n \"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent\",\n \"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]\",\n className\n )}\n initial={{ backgroundPosition: \"100% center\" }}\n style={\n {\n \"--spread\": `${dynamicSpread}px`,\n backgroundImage: `var(--bg), ${baseGradient}`,\n } as CSSProperties\n }\n transition={{\n duration,\n ease: \"linear\",\n repeat: Number.POSITIVE_INFINITY,\n }}\n >\n {children}\n </MotionComponent>\n );\n};\n\nexport const Shimmer = memo(ShimmerComponent);\n"],"names":["motionComponentCache","getMotionComponent","element","component","motion","TS_SHIMMER_GRADIENT","ShimmerComponent","children","Component","className","duration","spread","gradient","MotionComponent","dynamicSpread","useMemo","baseGradient","jsx","cn","Shimmer","memo"],"mappings":"mMAYMA,MAA2B,IAK3BC,EAAsBC,GAAyC,CACnE,IAAIC,EAAYH,EAAqB,IAAIE,CAAO,EAChD,OAAKC,IACHA,EAAYC,EAAAA,OAAO,OAAOF,CAAO,EACjCF,EAAqB,IAAIE,EAASC,CAAS,GAEtCA,CACT,EAGaE,EACX,oDAgBIC,EAAmB,CAAC,CACxB,SAAAC,EACA,GAAIC,EAAY,IAChB,UAAAC,EACA,SAAAC,EAAW,EACX,OAAAC,EAAS,EACT,SAAAC,CACF,IAAwB,CACtB,MAAMC,EAAkBZ,EACtBO,CAAA,EAGIM,EAAgBC,EAAAA,QACpB,KAAOR,GAAU,QAAU,GAAKI,EAChC,CAACJ,EAAUI,CAAM,CAAA,EAGbK,EAAeJ,GAEjB,gFAEJ,OACEK,EAAAA,IAACJ,EAAA,CACC,QAAS,CAAE,mBAAoB,WAAA,EAC/B,UAAWK,EAAAA,GACT,iFACA,8JACAT,CAAA,EAEF,QAAS,CAAE,mBAAoB,aAAA,EAC/B,MACE,CACE,WAAY,GAAGK,CAAa,KAC5B,gBAAiB,cAAcE,CAAY,EAAA,EAG/C,WAAY,CACV,SAAAN,EACA,KAAM,SACN,OAAQ,OAAO,iBAAA,EAGhB,SAAAH,CAAA,CAAA,CAGP,EAEaY,EAAUC,EAAAA,KAAKd,CAAgB"}
@@ -0,0 +1,49 @@
1
+ import { jsx as p } from "react/jsx-runtime";
2
+ import { motion as g } from "motion/react";
3
+ import { memo as d, useMemo as l } from "react";
4
+ import { cn as u } from "../../lib/utils.js";
5
+ const r = /* @__PURE__ */ new Map(), b = (o) => {
6
+ let e = r.get(o);
7
+ return e || (e = g.create(o), r.set(o, e)), e;
8
+ }, M = "linear-gradient(90deg, #549DFF, #8243BA, #9665F4)", I = ({
9
+ children: o,
10
+ as: e = "p",
11
+ className: a,
12
+ duration: i = 2,
13
+ spread: t = 2,
14
+ gradient: n
15
+ }) => {
16
+ const c = b(
17
+ e
18
+ ), m = l(
19
+ () => (o?.length ?? 0) * t,
20
+ [o, t]
21
+ ), s = n || "linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))";
22
+ return /* @__PURE__ */ p(
23
+ c,
24
+ {
25
+ animate: { backgroundPosition: "0% center" },
26
+ className: u(
27
+ "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
28
+ "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
29
+ a
30
+ ),
31
+ initial: { backgroundPosition: "100% center" },
32
+ style: {
33
+ "--spread": `${m}px`,
34
+ backgroundImage: `var(--bg), ${s}`
35
+ },
36
+ transition: {
37
+ duration: i,
38
+ ease: "linear",
39
+ repeat: Number.POSITIVE_INFINITY
40
+ },
41
+ children: o
42
+ }
43
+ );
44
+ }, S = d(I);
45
+ export {
46
+ S as Shimmer,
47
+ M as TS_SHIMMER_GRADIENT
48
+ };
49
+ //# sourceMappingURL=shimmer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shimmer.js","sources":["../../../src/components/ai/shimmer.tsx"],"sourcesContent":["import { motion } from \"motion/react\";\nimport { memo, useMemo } from \"react\";\n\nimport type { MotionProps } from \"motion/react\";\nimport type { CSSProperties, ElementType, JSX } from \"react\";\n\n\nimport { cn } from \"@/lib/utils\";\n\ntype MotionHTMLProps = MotionProps & Record<string, unknown>;\n\n// Cache motion components at module level to avoid creating during render\nconst motionComponentCache = new Map<\n keyof JSX.IntrinsicElements,\n React.ComponentType<MotionHTMLProps>\n>();\n\nconst getMotionComponent = (element: keyof JSX.IntrinsicElements) => {\n let component = motionComponentCache.get(element);\n if (!component) {\n component = motion.create(element);\n motionComponentCache.set(element, component);\n }\n return component;\n};\n\n/** TetraScience brand gradient — Light Blue 300 → Purple 500 → Violet Marble */\nexport const TS_SHIMMER_GRADIENT =\n \"linear-gradient(90deg, #549DFF, #8243BA, #9665F4)\";\n\nexport interface TextShimmerProps {\n children: string;\n as?: ElementType;\n className?: string;\n duration?: number;\n spread?: number;\n /**\n * CSS gradient used as the base text colour. Defaults to `muted-foreground`.\n * Pass `TS_SHIMMER_GRADIENT` (or any custom gradient) to colour the text\n * with the brand blue→purple sweep.\n */\n gradient?: string;\n}\n\nconst ShimmerComponent = ({\n children,\n as: Component = \"p\",\n className,\n duration = 2,\n spread = 2,\n gradient,\n}: TextShimmerProps) => {\n const MotionComponent = getMotionComponent(\n Component as keyof JSX.IntrinsicElements\n );\n\n const dynamicSpread = useMemo(\n () => (children?.length ?? 0) * spread,\n [children, spread]\n );\n\n const baseGradient = gradient\n ? gradient\n : \"linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))\";\n\n return (\n <MotionComponent\n animate={{ backgroundPosition: \"0% center\" }}\n className={cn(\n \"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent\",\n \"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]\",\n className\n )}\n initial={{ backgroundPosition: \"100% center\" }}\n style={\n {\n \"--spread\": `${dynamicSpread}px`,\n backgroundImage: `var(--bg), ${baseGradient}`,\n } as CSSProperties\n }\n transition={{\n duration,\n ease: \"linear\",\n repeat: Number.POSITIVE_INFINITY,\n }}\n >\n {children}\n </MotionComponent>\n );\n};\n\nexport const Shimmer = memo(ShimmerComponent);\n"],"names":["motionComponentCache","getMotionComponent","element","component","motion","TS_SHIMMER_GRADIENT","ShimmerComponent","children","Component","className","duration","spread","gradient","MotionComponent","dynamicSpread","useMemo","baseGradient","jsx","cn","Shimmer","memo"],"mappings":";;;;AAYA,MAAMA,wBAA2B,IAAA,GAK3BC,IAAqB,CAACC,MAAyC;AACnE,MAAIC,IAAYH,EAAqB,IAAIE,CAAO;AAChD,SAAKC,MACHA,IAAYC,EAAO,OAAOF,CAAO,GACjCF,EAAqB,IAAIE,GAASC,CAAS,IAEtCA;AACT,GAGaE,IACX,qDAgBIC,IAAmB,CAAC;AAAA,EACxB,UAAAC;AAAA,EACA,IAAIC,IAAY;AAAA,EAChB,WAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,QAAAC,IAAS;AAAA,EACT,UAAAC;AACF,MAAwB;AACtB,QAAMC,IAAkBZ;AAAA,IACtBO;AAAA,EAAA,GAGIM,IAAgBC;AAAA,IACpB,OAAOR,GAAU,UAAU,KAAKI;AAAA,IAChC,CAACJ,GAAUI,CAAM;AAAA,EAAA,GAGbK,IAAeJ,KAEjB;AAEJ,SACE,gBAAAK;AAAA,IAACJ;AAAA,IAAA;AAAA,MACC,SAAS,EAAE,oBAAoB,YAAA;AAAA,MAC/B,WAAWK;AAAA,QACT;AAAA,QACA;AAAA,QACAT;AAAA,MAAA;AAAA,MAEF,SAAS,EAAE,oBAAoB,cAAA;AAAA,MAC/B,OACE;AAAA,QACE,YAAY,GAAGK,CAAa;AAAA,QAC5B,iBAAiB,cAAcE,CAAY;AAAA,MAAA;AAAA,MAG/C,YAAY;AAAA,QACV,UAAAN;AAAA,QACA,MAAM;AAAA,QACN,QAAQ,OAAO;AAAA,MAAA;AAAA,MAGhB,UAAAH;AAAA,IAAA;AAAA,EAAA;AAGP,GAEaY,IAAUC,EAAKd,CAAgB;"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),c=require("lucide-react"),i=require("../ui/button.cjs"),r=require("../ui/collapsible.cjs"),o=require("../../lib/utils.cjs"),u=({className:t,...s})=>e.jsx(r.Collapsible,{className:o.cn("not-prose mb-4 text-primary text-xs",t),...s}),d=({className:t,count:s,children:a,...n})=>e.jsx(r.CollapsibleTrigger,{className:o.cn("flex items-center gap-2",t),...n,children:a??e.jsxs(e.Fragment,{children:[e.jsxs("p",{className:"font-medium",children:["Used ",s," sources"]}),e.jsx(c.ChevronDownIcon,{className:"h-4 w-4"})]})}),m=({className:t,...s})=>e.jsx(r.CollapsibleContent,{className:o.cn("mt-3 flex w-fit flex-col gap-2","data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",t),...s}),x=({href:t,title:s,className:a,children:n,...l})=>e.jsx(i.Button,{asChild:!0,variant:"link",className:o.cn("h-auto gap-1.5 px-0 text-xs font-medium justify-start",a),children:e.jsx("a",{href:t,rel:"noreferrer",target:"_blank",...l,children:n??e.jsxs(e.Fragment,{children:[e.jsx(c.BookIcon,{}),s]})})});exports.Source=x;exports.Sources=u;exports.SourcesContent=m;exports.SourcesTrigger=d;
2
+ //# sourceMappingURL=sources.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.cjs","sources":["../../../src/components/ai/sources.tsx"],"sourcesContent":["import { BookIcon, ChevronDownIcon } from \"lucide-react\";\n\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SourcesProps = ComponentProps<\"div\">;\n\nexport const Sources = ({ className, ...props }: SourcesProps) => (\n <Collapsible\n className={cn(\"not-prose mb-4 text-primary text-xs\", className)}\n {...props}\n />\n);\n\nexport type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {\n count: number;\n};\n\nexport const SourcesTrigger = ({\n className,\n count,\n children,\n ...props\n}: SourcesTriggerProps) => (\n <CollapsibleTrigger\n className={cn(\"flex items-center gap-2\", className)}\n {...props}\n >\n {children ?? (\n <>\n <p className=\"font-medium\">Used {count} sources</p>\n <ChevronDownIcon className=\"h-4 w-4\" />\n </>\n )}\n </CollapsibleTrigger>\n);\n\nexport type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const SourcesContent = ({\n className,\n ...props\n}: SourcesContentProps) => (\n <CollapsibleContent\n className={cn(\n \"mt-3 flex w-fit flex-col gap-2\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className\n )}\n {...props}\n />\n);\n\nexport type SourceProps = ComponentProps<\"a\">;\n\nexport const Source = ({ href, title, className, children, ...props }: SourceProps) => (\n <Button asChild variant=\"link\" className={cn(\"h-auto gap-1.5 px-0 text-xs font-medium justify-start\", className)}>\n <a href={href} rel=\"noreferrer\" target=\"_blank\" {...props}>\n {children ?? (\n <>\n <BookIcon />\n {title}\n </>\n )}\n </a>\n </Button>\n);\n"],"names":["Sources","className","props","jsx","Collapsible","cn","SourcesTrigger","count","children","CollapsibleTrigger","jsxs","Fragment","ChevronDownIcon","SourcesContent","CollapsibleContent","Source","href","title","Button","BookIcon"],"mappings":"iPAcaA,EAAU,CAAC,CAAE,UAAAC,EAAW,GAAGC,KACtCC,EAAAA,IAACC,EAAAA,YAAA,CACC,UAAWC,EAAAA,GAAG,sCAAuCJ,CAAS,EAC7D,GAAGC,CAAA,CACN,EAOWI,EAAiB,CAAC,CAC7B,UAAAL,EACA,MAAAM,EACA,SAAAC,EACA,GAAGN,CACL,IACEC,EAAAA,IAACM,EAAAA,mBAAA,CACC,UAAWJ,EAAAA,GAAG,0BAA2BJ,CAAS,EACjD,GAAGC,EAEH,YACCQ,EAAAA,KAAAC,EAAAA,SAAA,CACE,SAAA,CAAAD,EAAAA,KAAC,IAAA,CAAE,UAAU,cAAc,SAAA,CAAA,QAAMH,EAAM,UAAA,EAAQ,EAC/CJ,EAAAA,IAACS,EAAAA,gBAAA,CAAgB,UAAU,SAAA,CAAU,CAAA,CAAA,CACvC,CAAA,CAEJ,EAKWC,EAAiB,CAAC,CAC7B,UAAAZ,EACA,GAAGC,CACL,IACEC,EAAAA,IAACW,EAAAA,mBAAA,CACC,UAAWT,EAAAA,GACT,iCACA,wLACAJ,CAAA,EAED,GAAGC,CAAA,CACN,EAKWa,EAAS,CAAC,CAAE,KAAAC,EAAM,MAAAC,EAAO,UAAAhB,EAAW,SAAAO,EAAU,GAAGN,CAAA,IAC5DC,EAAAA,IAACe,EAAAA,OAAA,CAAO,QAAO,GAAC,QAAQ,OAAO,UAAWb,KAAG,wDAAyDJ,CAAS,EAC7G,eAAC,IAAA,CAAE,KAAAe,EAAY,IAAI,aAAa,OAAO,SAAU,GAAGd,EACjD,YACCQ,EAAAA,KAAAC,EAAAA,SAAA,CACE,SAAA,CAAAR,EAAAA,IAACgB,EAAAA,SAAA,EAAS,EACTF,CAAA,CAAA,CACH,EAEJ,CAAA,CACF"}
@@ -0,0 +1,54 @@
1
+ import { jsx as o, jsxs as n, Fragment as l } from "react/jsx-runtime";
2
+ import { BookIcon as c, ChevronDownIcon as m } from "lucide-react";
3
+ import { Button as d } from "../ui/button.js";
4
+ import { Collapsible as p, CollapsibleContent as f, CollapsibleTrigger as u } from "../ui/collapsible.js";
5
+ import { cn as a } from "../../lib/utils.js";
6
+ const b = ({ className: e, ...t }) => /* @__PURE__ */ o(
7
+ p,
8
+ {
9
+ className: a("not-prose mb-4 text-primary text-xs", e),
10
+ ...t
11
+ }
12
+ ), S = ({
13
+ className: e,
14
+ count: t,
15
+ children: s,
16
+ ...r
17
+ }) => /* @__PURE__ */ o(
18
+ u,
19
+ {
20
+ className: a("flex items-center gap-2", e),
21
+ ...r,
22
+ children: s ?? /* @__PURE__ */ n(l, { children: [
23
+ /* @__PURE__ */ n("p", { className: "font-medium", children: [
24
+ "Used ",
25
+ t,
26
+ " sources"
27
+ ] }),
28
+ /* @__PURE__ */ o(m, { className: "h-4 w-4" })
29
+ ] })
30
+ }
31
+ ), j = ({
32
+ className: e,
33
+ ...t
34
+ }) => /* @__PURE__ */ o(
35
+ f,
36
+ {
37
+ className: a(
38
+ "mt-3 flex w-fit flex-col gap-2",
39
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
40
+ e
41
+ ),
42
+ ...t
43
+ }
44
+ ), k = ({ href: e, title: t, className: s, children: r, ...i }) => /* @__PURE__ */ o(d, { asChild: !0, variant: "link", className: a("h-auto gap-1.5 px-0 text-xs font-medium justify-start", s), children: /* @__PURE__ */ o("a", { href: e, rel: "noreferrer", target: "_blank", ...i, children: r ?? /* @__PURE__ */ n(l, { children: [
45
+ /* @__PURE__ */ o(c, {}),
46
+ t
47
+ ] }) }) });
48
+ export {
49
+ k as Source,
50
+ b as Sources,
51
+ j as SourcesContent,
52
+ S as SourcesTrigger
53
+ };
54
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.js","sources":["../../../src/components/ai/sources.tsx"],"sourcesContent":["import { BookIcon, ChevronDownIcon } from \"lucide-react\";\n\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SourcesProps = ComponentProps<\"div\">;\n\nexport const Sources = ({ className, ...props }: SourcesProps) => (\n <Collapsible\n className={cn(\"not-prose mb-4 text-primary text-xs\", className)}\n {...props}\n />\n);\n\nexport type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {\n count: number;\n};\n\nexport const SourcesTrigger = ({\n className,\n count,\n children,\n ...props\n}: SourcesTriggerProps) => (\n <CollapsibleTrigger\n className={cn(\"flex items-center gap-2\", className)}\n {...props}\n >\n {children ?? (\n <>\n <p className=\"font-medium\">Used {count} sources</p>\n <ChevronDownIcon className=\"h-4 w-4\" />\n </>\n )}\n </CollapsibleTrigger>\n);\n\nexport type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const SourcesContent = ({\n className,\n ...props\n}: SourcesContentProps) => (\n <CollapsibleContent\n className={cn(\n \"mt-3 flex w-fit flex-col gap-2\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className\n )}\n {...props}\n />\n);\n\nexport type SourceProps = ComponentProps<\"a\">;\n\nexport const Source = ({ href, title, className, children, ...props }: SourceProps) => (\n <Button asChild variant=\"link\" className={cn(\"h-auto gap-1.5 px-0 text-xs font-medium justify-start\", className)}>\n <a href={href} rel=\"noreferrer\" target=\"_blank\" {...props}>\n {children ?? (\n <>\n <BookIcon />\n {title}\n </>\n )}\n </a>\n </Button>\n);\n"],"names":["Sources","className","props","jsx","Collapsible","cn","SourcesTrigger","count","children","CollapsibleTrigger","jsxs","Fragment","ChevronDownIcon","SourcesContent","CollapsibleContent","Source","href","title","Button","BookIcon"],"mappings":";;;;;AAcO,MAAMA,IAAU,CAAC,EAAE,WAAAC,GAAW,GAAGC,QACtC,gBAAAC;AAAA,EAACC;AAAA,EAAA;AAAA,IACC,WAAWC,EAAG,uCAAuCJ,CAAS;AAAA,IAC7D,GAAGC;AAAA,EAAA;AACN,GAOWI,IAAiB,CAAC;AAAA,EAC7B,WAAAL;AAAA,EACA,OAAAM;AAAA,EACA,UAAAC;AAAA,EACA,GAAGN;AACL,MACE,gBAAAC;AAAA,EAACM;AAAA,EAAA;AAAA,IACC,WAAWJ,EAAG,2BAA2BJ,CAAS;AAAA,IACjD,GAAGC;AAAA,IAEH,eACC,gBAAAQ,EAAAC,GAAA,EACE,UAAA;AAAA,MAAA,gBAAAD,EAAC,KAAA,EAAE,WAAU,eAAc,UAAA;AAAA,QAAA;AAAA,QAAMH;AAAA,QAAM;AAAA,MAAA,GAAQ;AAAA,MAC/C,gBAAAJ,EAACS,GAAA,EAAgB,WAAU,UAAA,CAAU;AAAA,IAAA,EAAA,CACvC;AAAA,EAAA;AAEJ,GAKWC,IAAiB,CAAC;AAAA,EAC7B,WAAAZ;AAAA,EACA,GAAGC;AACL,MACE,gBAAAC;AAAA,EAACW;AAAA,EAAA;AAAA,IACC,WAAWT;AAAA,MACT;AAAA,MACA;AAAA,MACAJ;AAAA,IAAA;AAAA,IAED,GAAGC;AAAA,EAAA;AACN,GAKWa,IAAS,CAAC,EAAE,MAAAC,GAAM,OAAAC,GAAO,WAAAhB,GAAW,UAAAO,GAAU,GAAGN,EAAA,MAC5D,gBAAAC,EAACe,GAAA,EAAO,SAAO,IAAC,SAAQ,QAAO,WAAWb,EAAG,yDAAyDJ,CAAS,GAC7G,4BAAC,KAAA,EAAE,MAAAe,GAAY,KAAI,cAAa,QAAO,UAAU,GAAGd,GACjD,eACC,gBAAAQ,EAAAC,GAAA,EACE,UAAA;AAAA,EAAA,gBAAAR,EAACgB,GAAA,EAAS;AAAA,EACTF;AAAA,EAAA,CACH,GAEJ,EAAA,CACF;"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react/jsx-runtime"),q=require("lucide-react"),t=require("react"),B=require("../ui/button.cjs"),P=require("../ui/spinner.cjs"),U=require("../../lib/utils.cjs"),F=()=>typeof window>"u"?"none":"SpeechRecognition"in window||"webkitSpeechRecognition"in window?"speech-recognition":"MediaRecorder"in window&&"mediaDevices"in navigator?"media-recorder":"none",O=({className:D,onTranscriptionChange:E,onAudioRecorded:v,lang:L="en-US",...z})=>{const[s,o]=t.useState(!1),[f,k]=t.useState(!1),[i]=t.useState(F),[C,x]=t.useState(!1),u=t.useRef(null),l=t.useRef(null),d=t.useRef(null),R=t.useRef([]),b=t.useRef(E),p=t.useRef(v);b.current=E,p.current=v,t.useEffect(()=>{if(i!=="speech-recognition")return;const r=window.SpeechRecognition||window.webkitSpeechRecognition,e=new r;e.continuous=!0,e.interimResults=!0,e.lang=L;const g=()=>{o(!0)},h=()=>{o(!1)},m=c=>{const w=c;let S="";for(let y=w.resultIndex;y<w.results.length;y+=1){const M=w.results[y];M.isFinal&&(S+=M[0]?.transcript??"")}S&&b.current?.(S)},n=()=>{o(!1)};return e.addEventListener("start",g),e.addEventListener("end",h),e.addEventListener("result",m),e.addEventListener("error",n),u.current=e,x(!0),()=>{e.removeEventListener("start",g),e.removeEventListener("end",h),e.removeEventListener("result",m),e.removeEventListener("error",n),e.stop(),u.current=null,x(!1)}},[i,L]),t.useEffect(()=>()=>{if(l.current?.state==="recording"&&l.current.stop(),d.current)for(const r of d.current.getTracks())r.stop()},[]);const j=t.useCallback(async()=>{if(p.current)try{const r=await navigator.mediaDevices.getUserMedia({audio:!0});d.current=r;const e=new MediaRecorder(r);R.current=[];const g=n=>{n.data.size>0&&R.current.push(n.data)},h=async()=>{for(const c of r.getTracks())c.stop();d.current=null;const n=new Blob(R.current,{type:"audio/webm"});if(n.size>0&&p.current){k(!0);try{const c=await p.current(n);c&&b.current?.(c)}catch{}finally{k(!1)}}},m=()=>{o(!1);for(const n of r.getTracks())n.stop();d.current=null};e.addEventListener("dataavailable",g),e.addEventListener("stop",h),e.addEventListener("error",m),l.current=e,e.start(),o(!0)}catch{o(!1)}},[]),I=t.useCallback(()=>{l.current?.state==="recording"&&l.current.stop(),o(!1)},[]),N=t.useCallback(()=>{i==="speech-recognition"&&u.current?s?u.current.stop():u.current.start():i==="media-recorder"&&(s?I():j())},[i,s,j,I]),T=i==="none"||i==="speech-recognition"&&!C||i==="media-recorder"&&!v||f;return a.jsxs("div",{className:"relative inline-flex items-center justify-center",children:[s&&[0,1,2].map(r=>a.jsx("div",{className:"absolute inset-0 animate-ping rounded-full border-2 border-red-400/30",style:{animationDelay:`${r*.3}s`,animationDuration:"2s"}},r)),a.jsxs(B.Button,{"aria-label":s?"Stop recording":"Microphone",className:U.cn("relative z-10 rounded-full transition-all duration-300",s?"bg-destructive text-white hover:bg-destructive/80 hover:text-white":"bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground",D),disabled:T,onClick:N,...z,children:[f&&a.jsx(P.Spinner,{}),!f&&s&&a.jsx(q.SquareIcon,{className:"size-4"}),!(f||s)&&a.jsx(q.MicIcon,{className:"size-4"})]})]})};exports.SpeechInput=O;
2
+ //# sourceMappingURL=speech-input.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech-input.cjs","sources":["../../../src/components/ai/speech-input.tsx"],"sourcesContent":["import { MicIcon, SquareIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\n\n\ninterface SpeechRecognition extends EventTarget {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n start(): void;\n stop(): void;\n onstart: ((this: SpeechRecognition, ev: Event) => void) | null;\n onend: ((this: SpeechRecognition, ev: Event) => void) | null;\n onresult:\n | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void)\n | null;\n onerror:\n | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void)\n | null;\n}\n\ninterface SpeechRecognitionEvent extends Event {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionResultList {\n readonly length: number;\n item(index: number): SpeechRecognitionResult;\n [index: number]: SpeechRecognitionResult;\n}\n\ninterface SpeechRecognitionResult {\n readonly length: number;\n item(index: number): SpeechRecognitionAlternative;\n [index: number]: SpeechRecognitionAlternative;\n isFinal: boolean;\n}\n\ninterface SpeechRecognitionAlternative {\n transcript: string;\n confidence: number;\n}\n\ninterface SpeechRecognitionErrorEvent extends Event {\n error: string;\n}\n\ndeclare global {\n interface Window {\n SpeechRecognition: new () => SpeechRecognition;\n webkitSpeechRecognition: new () => SpeechRecognition;\n }\n}\n\ntype SpeechInputMode = \"speech-recognition\" | \"media-recorder\" | \"none\";\n\nexport type SpeechInputProps = ComponentProps<typeof Button> & {\n onTranscriptionChange?: (text: string) => void;\n /**\n * Callback for when audio is recorded using MediaRecorder fallback.\n * This is called in browsers that don't support the Web Speech API (Firefox, Safari).\n * The callback receives an audio Blob that should be sent to a transcription service.\n * Return the transcribed text, which will be passed to onTranscriptionChange.\n */\n onAudioRecorded?: (audioBlob: Blob) => Promise<string>;\n lang?: string;\n};\n\nconst detectSpeechInputMode = (): SpeechInputMode => {\n if (typeof window === \"undefined\") {\n return \"none\";\n }\n\n if (\"SpeechRecognition\" in window || \"webkitSpeechRecognition\" in window) {\n return \"speech-recognition\";\n }\n\n if (\"MediaRecorder\" in window && \"mediaDevices\" in navigator) {\n return \"media-recorder\";\n }\n\n return \"none\";\n};\n\nexport const SpeechInput = ({\n className,\n onTranscriptionChange,\n onAudioRecorded,\n lang = \"en-US\",\n ...props\n}: SpeechInputProps) => {\n const [isListening, setIsListening] = useState(false);\n const [isProcessing, setIsProcessing] = useState(false);\n const [mode] = useState<SpeechInputMode>(detectSpeechInputMode);\n const [isRecognitionReady, setIsRecognitionReady] = useState(false);\n const recognitionRef = useRef<SpeechRecognition | null>(null);\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const onTranscriptionChangeRef = useRef<\n SpeechInputProps[\"onTranscriptionChange\"]\n >(onTranscriptionChange);\n const onAudioRecordedRef =\n useRef<SpeechInputProps[\"onAudioRecorded\"]>(onAudioRecorded);\n\n // Keep refs in sync\n onTranscriptionChangeRef.current = onTranscriptionChange;\n onAudioRecordedRef.current = onAudioRecorded;\n\n // Initialize Speech Recognition when mode is speech-recognition\n useEffect(() => {\n if (mode !== \"speech-recognition\") {\n return;\n }\n\n const SpeechRecognition =\n window.SpeechRecognition || window.webkitSpeechRecognition;\n const speechRecognition = new SpeechRecognition();\n\n speechRecognition.continuous = true;\n speechRecognition.interimResults = true;\n speechRecognition.lang = lang;\n\n const handleStart = () => {\n setIsListening(true);\n };\n\n const handleEnd = () => {\n setIsListening(false);\n };\n\n const handleResult = (event: Event) => {\n const speechEvent = event as SpeechRecognitionEvent;\n let finalTranscript = \"\";\n\n for (\n let i = speechEvent.resultIndex;\n i < speechEvent.results.length;\n i += 1\n ) {\n const result = speechEvent.results[i];\n if (result.isFinal) {\n finalTranscript += result[0]?.transcript ?? \"\";\n }\n }\n\n if (finalTranscript) {\n onTranscriptionChangeRef.current?.(finalTranscript);\n }\n };\n\n const handleError = () => {\n setIsListening(false);\n };\n\n speechRecognition.addEventListener(\"start\", handleStart);\n speechRecognition.addEventListener(\"end\", handleEnd);\n speechRecognition.addEventListener(\"result\", handleResult);\n speechRecognition.addEventListener(\"error\", handleError);\n\n recognitionRef.current = speechRecognition;\n setIsRecognitionReady(true);\n\n return () => {\n speechRecognition.removeEventListener(\"start\", handleStart);\n speechRecognition.removeEventListener(\"end\", handleEnd);\n speechRecognition.removeEventListener(\"result\", handleResult);\n speechRecognition.removeEventListener(\"error\", handleError);\n speechRecognition.stop();\n recognitionRef.current = null;\n setIsRecognitionReady(false);\n };\n }, [mode, lang]);\n\n // Cleanup MediaRecorder and stream on unmount\n useEffect(\n () => () => {\n if (mediaRecorderRef.current?.state === \"recording\") {\n mediaRecorderRef.current.stop();\n }\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n }\n },\n []\n );\n\n // Start MediaRecorder recording\n const startMediaRecorder = useCallback(async () => {\n if (!onAudioRecordedRef.current) {\n return;\n }\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n const mediaRecorder = new MediaRecorder(stream);\n audioChunksRef.current = [];\n\n const handleDataAvailable = (event: BlobEvent) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n const handleStop = async () => {\n for (const track of stream.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n\n const audioBlob = new Blob(audioChunksRef.current, {\n type: \"audio/webm\",\n });\n\n if (audioBlob.size > 0 && onAudioRecordedRef.current) {\n setIsProcessing(true);\n try {\n const transcript = await onAudioRecordedRef.current(audioBlob);\n if (transcript) {\n onTranscriptionChangeRef.current?.(transcript);\n }\n } catch {\n // Error handling delegated to the onAudioRecorded caller\n } finally {\n setIsProcessing(false);\n }\n }\n };\n\n const handleError = () => {\n setIsListening(false);\n for (const track of stream.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n };\n\n mediaRecorder.addEventListener(\"dataavailable\", handleDataAvailable);\n mediaRecorder.addEventListener(\"stop\", handleStop);\n mediaRecorder.addEventListener(\"error\", handleError);\n\n mediaRecorderRef.current = mediaRecorder;\n mediaRecorder.start();\n setIsListening(true);\n } catch {\n setIsListening(false);\n }\n }, []);\n\n // Stop MediaRecorder recording\n const stopMediaRecorder = useCallback(() => {\n if (mediaRecorderRef.current?.state === \"recording\") {\n mediaRecorderRef.current.stop();\n }\n setIsListening(false);\n }, []);\n\n const toggleListening = useCallback(() => {\n if (mode === \"speech-recognition\" && recognitionRef.current) {\n if (isListening) {\n recognitionRef.current.stop();\n } else {\n recognitionRef.current.start();\n }\n } else if (mode === \"media-recorder\") {\n if (isListening) {\n stopMediaRecorder();\n } else {\n startMediaRecorder();\n }\n }\n }, [mode, isListening, startMediaRecorder, stopMediaRecorder]);\n\n // Determine if button should be disabled\n const isDisabled =\n mode === \"none\" ||\n (mode === \"speech-recognition\" && !isRecognitionReady) ||\n (mode === \"media-recorder\" && !onAudioRecorded) ||\n isProcessing;\n\n return (\n <div className=\"relative inline-flex items-center justify-center\">\n {/* Animated pulse rings */}\n {isListening &&\n [0, 1, 2].map((index) => (\n <div\n className=\"absolute inset-0 animate-ping rounded-full border-2 border-red-400/30\"\n key={index}\n style={{\n animationDelay: `${index * 0.3}s`,\n animationDuration: \"2s\",\n }}\n />\n ))}\n\n {/* Main record button */}\n <Button\n aria-label={isListening ? \"Stop recording\" : \"Microphone\"}\n className={cn(\n \"relative z-10 rounded-full transition-all duration-300\",\n isListening\n ? \"bg-destructive text-white hover:bg-destructive/80 hover:text-white\"\n : \"bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground\",\n className\n )}\n disabled={isDisabled}\n onClick={toggleListening}\n {...props}\n >\n {isProcessing && <Spinner />}\n {!isProcessing && isListening && <SquareIcon className=\"size-4\" />}\n {!(isProcessing || isListening) && <MicIcon className=\"size-4\" />}\n </Button>\n </div>\n );\n};\n"],"names":["detectSpeechInputMode","SpeechInput","className","onTranscriptionChange","onAudioRecorded","lang","props","isListening","setIsListening","useState","isProcessing","setIsProcessing","mode","isRecognitionReady","setIsRecognitionReady","recognitionRef","useRef","mediaRecorderRef","streamRef","audioChunksRef","onTranscriptionChangeRef","onAudioRecordedRef","useEffect","SpeechRecognition","speechRecognition","handleStart","handleEnd","handleResult","event","speechEvent","finalTranscript","i","result","handleError","track","startMediaRecorder","useCallback","stream","mediaRecorder","handleDataAvailable","handleStop","audioBlob","transcript","stopMediaRecorder","toggleListening","isDisabled","jsxs","index","jsx","Button","cn","Spinner","SquareIcon","MicIcon"],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ import { jsxs as T, jsx as h } from "react/jsx-runtime";
2
+ import { SquareIcon as F, MicIcon as $ } from "lucide-react";
3
+ import { useState as v, useRef as c, useEffect as j, useCallback as S } from "react";
4
+ import { Button as A } from "../ui/button.js";
5
+ import { Spinner as G } from "../ui/spinner.js";
6
+ import { cn as H } from "../../lib/utils.js";
7
+ const J = () => typeof window > "u" ? "none" : "SpeechRecognition" in window || "webkitSpeechRecognition" in window ? "speech-recognition" : "MediaRecorder" in window && "mediaDevices" in navigator ? "media-recorder" : "none", Y = ({
8
+ className: B,
9
+ onTranscriptionChange: k,
10
+ onAudioRecorded: R,
11
+ lang: x = "en-US",
12
+ ...C
13
+ }) => {
14
+ const [n, o] = v(!1), [l, I] = v(!1), [i] = v(J), [P, M] = v(!1), a = c(null), d = c(null), u = c(null), w = c([]), b = c(k), f = c(R);
15
+ b.current = k, f.current = R, j(() => {
16
+ if (i !== "speech-recognition")
17
+ return;
18
+ const t = window.SpeechRecognition || window.webkitSpeechRecognition, e = new t();
19
+ e.continuous = !0, e.interimResults = !0, e.lang = x;
20
+ const p = () => {
21
+ o(!0);
22
+ }, m = () => {
23
+ o(!1);
24
+ }, g = (s) => {
25
+ const y = s;
26
+ let E = "";
27
+ for (let L = y.resultIndex; L < y.results.length; L += 1) {
28
+ const N = y.results[L];
29
+ N.isFinal && (E += N[0]?.transcript ?? "");
30
+ }
31
+ E && b.current?.(E);
32
+ }, r = () => {
33
+ o(!1);
34
+ };
35
+ return e.addEventListener("start", p), e.addEventListener("end", m), e.addEventListener("result", g), e.addEventListener("error", r), a.current = e, M(!0), () => {
36
+ e.removeEventListener("start", p), e.removeEventListener("end", m), e.removeEventListener("result", g), e.removeEventListener("error", r), e.stop(), a.current = null, M(!1);
37
+ };
38
+ }, [i, x]), j(
39
+ () => () => {
40
+ if (d.current?.state === "recording" && d.current.stop(), u.current)
41
+ for (const t of u.current.getTracks())
42
+ t.stop();
43
+ },
44
+ []
45
+ );
46
+ const D = S(async () => {
47
+ if (f.current)
48
+ try {
49
+ const t = await navigator.mediaDevices.getUserMedia({ audio: !0 });
50
+ u.current = t;
51
+ const e = new MediaRecorder(t);
52
+ w.current = [];
53
+ const p = (r) => {
54
+ r.data.size > 0 && w.current.push(r.data);
55
+ }, m = async () => {
56
+ for (const s of t.getTracks())
57
+ s.stop();
58
+ u.current = null;
59
+ const r = new Blob(w.current, {
60
+ type: "audio/webm"
61
+ });
62
+ if (r.size > 0 && f.current) {
63
+ I(!0);
64
+ try {
65
+ const s = await f.current(r);
66
+ s && b.current?.(s);
67
+ } catch {
68
+ } finally {
69
+ I(!1);
70
+ }
71
+ }
72
+ }, g = () => {
73
+ o(!1);
74
+ for (const r of t.getTracks())
75
+ r.stop();
76
+ u.current = null;
77
+ };
78
+ e.addEventListener("dataavailable", p), e.addEventListener("stop", m), e.addEventListener("error", g), d.current = e, e.start(), o(!0);
79
+ } catch {
80
+ o(!1);
81
+ }
82
+ }, []), z = S(() => {
83
+ d.current?.state === "recording" && d.current.stop(), o(!1);
84
+ }, []), U = S(() => {
85
+ i === "speech-recognition" && a.current ? n ? a.current.stop() : a.current.start() : i === "media-recorder" && (n ? z() : D());
86
+ }, [i, n, D, z]), q = i === "none" || i === "speech-recognition" && !P || i === "media-recorder" && !R || l;
87
+ return /* @__PURE__ */ T("div", { className: "relative inline-flex items-center justify-center", children: [
88
+ n && [0, 1, 2].map((t) => /* @__PURE__ */ h(
89
+ "div",
90
+ {
91
+ className: "absolute inset-0 animate-ping rounded-full border-2 border-red-400/30",
92
+ style: {
93
+ animationDelay: `${t * 0.3}s`,
94
+ animationDuration: "2s"
95
+ }
96
+ },
97
+ t
98
+ )),
99
+ /* @__PURE__ */ T(
100
+ A,
101
+ {
102
+ "aria-label": n ? "Stop recording" : "Microphone",
103
+ className: H(
104
+ "relative z-10 rounded-full transition-all duration-300",
105
+ n ? "bg-destructive text-white hover:bg-destructive/80 hover:text-white" : "bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground",
106
+ B
107
+ ),
108
+ disabled: q,
109
+ onClick: U,
110
+ ...C,
111
+ children: [
112
+ l && /* @__PURE__ */ h(G, {}),
113
+ !l && n && /* @__PURE__ */ h(F, { className: "size-4" }),
114
+ !(l || n) && /* @__PURE__ */ h($, { className: "size-4" })
115
+ ]
116
+ }
117
+ )
118
+ ] });
119
+ };
120
+ export {
121
+ Y as SpeechInput
122
+ };
123
+ //# sourceMappingURL=speech-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech-input.js","sources":["../../../src/components/ai/speech-input.tsx"],"sourcesContent":["import { MicIcon, SquareIcon } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\n\n\ninterface SpeechRecognition extends EventTarget {\n continuous: boolean;\n interimResults: boolean;\n lang: string;\n start(): void;\n stop(): void;\n onstart: ((this: SpeechRecognition, ev: Event) => void) | null;\n onend: ((this: SpeechRecognition, ev: Event) => void) | null;\n onresult:\n | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void)\n | null;\n onerror:\n | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void)\n | null;\n}\n\ninterface SpeechRecognitionEvent extends Event {\n results: SpeechRecognitionResultList;\n resultIndex: number;\n}\n\ninterface SpeechRecognitionResultList {\n readonly length: number;\n item(index: number): SpeechRecognitionResult;\n [index: number]: SpeechRecognitionResult;\n}\n\ninterface SpeechRecognitionResult {\n readonly length: number;\n item(index: number): SpeechRecognitionAlternative;\n [index: number]: SpeechRecognitionAlternative;\n isFinal: boolean;\n}\n\ninterface SpeechRecognitionAlternative {\n transcript: string;\n confidence: number;\n}\n\ninterface SpeechRecognitionErrorEvent extends Event {\n error: string;\n}\n\ndeclare global {\n interface Window {\n SpeechRecognition: new () => SpeechRecognition;\n webkitSpeechRecognition: new () => SpeechRecognition;\n }\n}\n\ntype SpeechInputMode = \"speech-recognition\" | \"media-recorder\" | \"none\";\n\nexport type SpeechInputProps = ComponentProps<typeof Button> & {\n onTranscriptionChange?: (text: string) => void;\n /**\n * Callback for when audio is recorded using MediaRecorder fallback.\n * This is called in browsers that don't support the Web Speech API (Firefox, Safari).\n * The callback receives an audio Blob that should be sent to a transcription service.\n * Return the transcribed text, which will be passed to onTranscriptionChange.\n */\n onAudioRecorded?: (audioBlob: Blob) => Promise<string>;\n lang?: string;\n};\n\nconst detectSpeechInputMode = (): SpeechInputMode => {\n if (typeof window === \"undefined\") {\n return \"none\";\n }\n\n if (\"SpeechRecognition\" in window || \"webkitSpeechRecognition\" in window) {\n return \"speech-recognition\";\n }\n\n if (\"MediaRecorder\" in window && \"mediaDevices\" in navigator) {\n return \"media-recorder\";\n }\n\n return \"none\";\n};\n\nexport const SpeechInput = ({\n className,\n onTranscriptionChange,\n onAudioRecorded,\n lang = \"en-US\",\n ...props\n}: SpeechInputProps) => {\n const [isListening, setIsListening] = useState(false);\n const [isProcessing, setIsProcessing] = useState(false);\n const [mode] = useState<SpeechInputMode>(detectSpeechInputMode);\n const [isRecognitionReady, setIsRecognitionReady] = useState(false);\n const recognitionRef = useRef<SpeechRecognition | null>(null);\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const audioChunksRef = useRef<Blob[]>([]);\n const onTranscriptionChangeRef = useRef<\n SpeechInputProps[\"onTranscriptionChange\"]\n >(onTranscriptionChange);\n const onAudioRecordedRef =\n useRef<SpeechInputProps[\"onAudioRecorded\"]>(onAudioRecorded);\n\n // Keep refs in sync\n onTranscriptionChangeRef.current = onTranscriptionChange;\n onAudioRecordedRef.current = onAudioRecorded;\n\n // Initialize Speech Recognition when mode is speech-recognition\n useEffect(() => {\n if (mode !== \"speech-recognition\") {\n return;\n }\n\n const SpeechRecognition =\n window.SpeechRecognition || window.webkitSpeechRecognition;\n const speechRecognition = new SpeechRecognition();\n\n speechRecognition.continuous = true;\n speechRecognition.interimResults = true;\n speechRecognition.lang = lang;\n\n const handleStart = () => {\n setIsListening(true);\n };\n\n const handleEnd = () => {\n setIsListening(false);\n };\n\n const handleResult = (event: Event) => {\n const speechEvent = event as SpeechRecognitionEvent;\n let finalTranscript = \"\";\n\n for (\n let i = speechEvent.resultIndex;\n i < speechEvent.results.length;\n i += 1\n ) {\n const result = speechEvent.results[i];\n if (result.isFinal) {\n finalTranscript += result[0]?.transcript ?? \"\";\n }\n }\n\n if (finalTranscript) {\n onTranscriptionChangeRef.current?.(finalTranscript);\n }\n };\n\n const handleError = () => {\n setIsListening(false);\n };\n\n speechRecognition.addEventListener(\"start\", handleStart);\n speechRecognition.addEventListener(\"end\", handleEnd);\n speechRecognition.addEventListener(\"result\", handleResult);\n speechRecognition.addEventListener(\"error\", handleError);\n\n recognitionRef.current = speechRecognition;\n setIsRecognitionReady(true);\n\n return () => {\n speechRecognition.removeEventListener(\"start\", handleStart);\n speechRecognition.removeEventListener(\"end\", handleEnd);\n speechRecognition.removeEventListener(\"result\", handleResult);\n speechRecognition.removeEventListener(\"error\", handleError);\n speechRecognition.stop();\n recognitionRef.current = null;\n setIsRecognitionReady(false);\n };\n }, [mode, lang]);\n\n // Cleanup MediaRecorder and stream on unmount\n useEffect(\n () => () => {\n if (mediaRecorderRef.current?.state === \"recording\") {\n mediaRecorderRef.current.stop();\n }\n if (streamRef.current) {\n for (const track of streamRef.current.getTracks()) {\n track.stop();\n }\n }\n },\n []\n );\n\n // Start MediaRecorder recording\n const startMediaRecorder = useCallback(async () => {\n if (!onAudioRecordedRef.current) {\n return;\n }\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n const mediaRecorder = new MediaRecorder(stream);\n audioChunksRef.current = [];\n\n const handleDataAvailable = (event: BlobEvent) => {\n if (event.data.size > 0) {\n audioChunksRef.current.push(event.data);\n }\n };\n\n const handleStop = async () => {\n for (const track of stream.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n\n const audioBlob = new Blob(audioChunksRef.current, {\n type: \"audio/webm\",\n });\n\n if (audioBlob.size > 0 && onAudioRecordedRef.current) {\n setIsProcessing(true);\n try {\n const transcript = await onAudioRecordedRef.current(audioBlob);\n if (transcript) {\n onTranscriptionChangeRef.current?.(transcript);\n }\n } catch {\n // Error handling delegated to the onAudioRecorded caller\n } finally {\n setIsProcessing(false);\n }\n }\n };\n\n const handleError = () => {\n setIsListening(false);\n for (const track of stream.getTracks()) {\n track.stop();\n }\n streamRef.current = null;\n };\n\n mediaRecorder.addEventListener(\"dataavailable\", handleDataAvailable);\n mediaRecorder.addEventListener(\"stop\", handleStop);\n mediaRecorder.addEventListener(\"error\", handleError);\n\n mediaRecorderRef.current = mediaRecorder;\n mediaRecorder.start();\n setIsListening(true);\n } catch {\n setIsListening(false);\n }\n }, []);\n\n // Stop MediaRecorder recording\n const stopMediaRecorder = useCallback(() => {\n if (mediaRecorderRef.current?.state === \"recording\") {\n mediaRecorderRef.current.stop();\n }\n setIsListening(false);\n }, []);\n\n const toggleListening = useCallback(() => {\n if (mode === \"speech-recognition\" && recognitionRef.current) {\n if (isListening) {\n recognitionRef.current.stop();\n } else {\n recognitionRef.current.start();\n }\n } else if (mode === \"media-recorder\") {\n if (isListening) {\n stopMediaRecorder();\n } else {\n startMediaRecorder();\n }\n }\n }, [mode, isListening, startMediaRecorder, stopMediaRecorder]);\n\n // Determine if button should be disabled\n const isDisabled =\n mode === \"none\" ||\n (mode === \"speech-recognition\" && !isRecognitionReady) ||\n (mode === \"media-recorder\" && !onAudioRecorded) ||\n isProcessing;\n\n return (\n <div className=\"relative inline-flex items-center justify-center\">\n {/* Animated pulse rings */}\n {isListening &&\n [0, 1, 2].map((index) => (\n <div\n className=\"absolute inset-0 animate-ping rounded-full border-2 border-red-400/30\"\n key={index}\n style={{\n animationDelay: `${index * 0.3}s`,\n animationDuration: \"2s\",\n }}\n />\n ))}\n\n {/* Main record button */}\n <Button\n aria-label={isListening ? \"Stop recording\" : \"Microphone\"}\n className={cn(\n \"relative z-10 rounded-full transition-all duration-300\",\n isListening\n ? \"bg-destructive text-white hover:bg-destructive/80 hover:text-white\"\n : \"bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground\",\n className\n )}\n disabled={isDisabled}\n onClick={toggleListening}\n {...props}\n >\n {isProcessing && <Spinner />}\n {!isProcessing && isListening && <SquareIcon className=\"size-4\" />}\n {!(isProcessing || isListening) && <MicIcon className=\"size-4\" />}\n </Button>\n </div>\n );\n};\n"],"names":["detectSpeechInputMode","SpeechInput","className","onTranscriptionChange","onAudioRecorded","lang","props","isListening","setIsListening","useState","isProcessing","setIsProcessing","mode","isRecognitionReady","setIsRecognitionReady","recognitionRef","useRef","mediaRecorderRef","streamRef","audioChunksRef","onTranscriptionChangeRef","onAudioRecordedRef","useEffect","SpeechRecognition","speechRecognition","handleStart","handleEnd","handleResult","event","speechEvent","finalTranscript","i","result","handleError","track","startMediaRecorder","useCallback","stream","mediaRecorder","handleDataAvailable","handleStop","audioBlob","transcript","stopMediaRecorder","toggleListening","isDisabled","jsxs","index","jsx","Button","cn","Spinner","SquareIcon","MicIcon"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),c=require("lucide-react"),n=require("react"),l=require("../../lib/utils.cjs"),i=1e3,h=60,y=700,v=1e3,g=1e6,j={loader:s.jsx(c.LoaderIcon,{}),"loader-circle":s.jsx(c.LoaderCircleIcon,{}),"loader-pinwheel":s.jsx(c.LoaderPinwheelIcon,{}),"disc-3":s.jsx(c.Disc3Icon,{})},$={streaming:"animate-pulse bg-[#549DFF]",idle:"bg-muted-foreground/40",done:"bg-[#038599]",error:"bg-[#E15759]"};function A(e){const t=Math.floor(e/h),r=e%h;return t>0?`${t}m ${r.toString().padStart(2,"0")}s`:`${r}s`}function D(e){return e>=g?`${(e/g).toFixed(1).replace(/\.0$/,"")}m`:e>=v?`${(e/v).toFixed(1).replace(/\.0$/,"")}k`:String(e)}const P=({startTime:e,isStreaming:t=e!==void 0,state:r,showIndicator:b=!1,tokenCount:u,tokenLabel:d="↓",icon:f,iconVariant:m,className:I})=>{const M=r??(t?"streaming":"idle"),p=f===void 0?m===void 0?void 0:j[m]:f,[R,E]=n.useState(()=>{if(e===void 0)return 0;const o=typeof e=="number"?e:e.getTime();return Math.max(0,Math.floor((Date.now()-o)/i))}),[_,S]=n.useState(!1),x=n.useRef(t);n.useEffect(()=>{const o=x.current;if(x.current=t,o&&!t){S(!0);const a=setTimeout(()=>S(!1),y);return()=>clearTimeout(a)}},[t]),n.useEffect(()=>{if(!t||e===void 0)return;const o=typeof e=="number"?e:e.getTime(),a=()=>E(Math.max(0,Math.floor((Date.now()-o)/i)));a();const w=setInterval(a,i);return()=>clearInterval(w)},[t,e]);const C=e!==void 0,F=u!==void 0;return s.jsxs("div",{"aria-live":"polite",className:l.cn("flex items-center gap-2 text-sm text-muted-foreground",I),children:[p!==void 0&&s.jsx("span",{className:l.cn("shrink-0 [&>svg]:size-3.5",t?"ts-spin-pulse":"opacity-40"),children:p}),C&&s.jsx("span",{className:"tabular-nums",children:A(R)}),F&&s.jsxs(s.Fragment,{children:[s.jsx("span",{"aria-hidden":!0,className:"select-none text-muted-foreground/40",children:"·"}),s.jsxs("span",{className:"tabular-nums",children:[d!=null&&s.jsx("span",{className:"mr-0.5",children:d}),D(u)," tokens"]})]}),b&&s.jsxs("span",{className:"relative flex size-3 shrink-0 items-center justify-center",children:[_&&s.jsx("span",{className:"ts-bubble-confirm absolute size-2 rounded-full bg-[#549DFF]"}),s.jsx("span",{className:l.cn("size-1.5 rounded-full transition-colors duration-500",$[M])})]})]})},N=n.memo(P);N.displayName="StreamStatus";exports.STREAM_STATUS_ICONS=j;exports.StreamStatus=N;
2
+ //# sourceMappingURL=stream-status.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-status.cjs","sources":["../../../src/components/ai/stream-status.tsx"],"sourcesContent":["import {\n Disc3Icon,\n LoaderCircleIcon,\n LoaderIcon,\n LoaderPinwheelIcon,\n} from \"lucide-react\";\nimport { memo, useEffect, useRef, useState } from \"react\";\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst MS_PER_S = 1000;\nconst SECS_PER_MIN = 60;\nconst CONFIRM_RIPPLE_MS = 700;\nconst K = 1_000;\nconst M = 1_000_000;\n\n// ---------------------------------------------------------------------------\n// Icon variants\n// ---------------------------------------------------------------------------\n\n/** Built-in Lucide spinner icons that pair well with ts-spin-pulse animation. */\nexport const STREAM_STATUS_ICONS = {\n loader: <LoaderIcon />,\n \"loader-circle\": <LoaderCircleIcon />,\n \"loader-pinwheel\": <LoaderPinwheelIcon />,\n \"disc-3\": <Disc3Icon />,\n} as const;\n\nexport type StreamStatusIconVariant = keyof typeof STREAM_STATUS_ICONS;\n\n// ---------------------------------------------------------------------------\n// State → indicator dot colour\n// ---------------------------------------------------------------------------\n\nexport type StreamStatusState = \"streaming\" | \"idle\" | \"done\" | \"error\";\n\nconst INDICATOR_CLASS: Record<StreamStatusState, string> = {\n streaming: \"animate-pulse bg-[#549DFF]\", // TS Light Blue 300\n idle: \"bg-muted-foreground/40\",\n done: \"bg-[#038599]\", // TS Forest Green 300\n error: \"bg-[#E15759]\", // TS Imperial Red\n};\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport interface StreamStatusProps {\n /**\n * Timestamp when streaming started (Date or epoch ms).\n * The component manages an internal 1-second ticker when set.\n */\n startTime?: Date | number;\n /** Whether streaming is currently active. Defaults to `true` when `startTime` is set. */\n isStreaming?: boolean;\n /**\n * Explicit state for the right-end indicator dot colour.\n * If omitted, derived from `isStreaming`: `true` → \"streaming\", `false` → \"idle\".\n *\n * | state | colour |\n * |-------------|----------------------------|\n * | streaming | TS Light Blue 300 (pulsing)|\n * | idle | muted gray |\n * | done | TS Forest Green 300 |\n * | error | TS Imperial Red |\n */\n state?: StreamStatusState;\n /**\n * Show the coloured status dot at the right end of the component.\n * Defaults to `false`.\n */\n showIndicator?: boolean;\n /** Token count shown after the separator, e.g. `8700` → \"↓ 8.7k tokens\". */\n tokenCount?: number;\n /** Prefix before the formatted token count. Defaults to `↓`. */\n tokenLabel?: ReactNode;\n /**\n * Custom icon (ReactNode). Takes precedence over `iconVariant`.\n * While streaming, the icon wrapper receives `ts-spin-pulse` — a continuous\n * rotation that occasionally surges to 1.3× scale.\n */\n icon?: ReactNode;\n /**\n * Convenience shorthand to pick a built-in Lucide spinner.\n * Ignored when `icon` is also provided.\n *\n * Options: `\"loader\"` | `\"loader-circle\"` | `\"loader-pinwheel\"` | `\"disc-3\"`\n */\n iconVariant?: StreamStatusIconVariant;\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatElapsed(seconds: number): string {\n const m = Math.floor(seconds / SECS_PER_MIN);\n const s = seconds % SECS_PER_MIN;\n return m > 0 ? `${m}m ${s.toString().padStart(2, \"0\")}s` : `${s}s`;\n}\n\nfunction formatTokens(count: number): string {\n if (count >= M) return `${(count / M).toFixed(1).replace(/\\.0$/, \"\")}m`;\n if (count >= K) return `${(count / K).toFixed(1).replace(/\\.0$/, \"\")}k`;\n return String(count);\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nconst StreamStatusComponent = ({\n startTime,\n isStreaming = startTime !== undefined,\n state: stateProp,\n showIndicator = false,\n tokenCount,\n tokenLabel = \"↓\",\n icon,\n iconVariant,\n className,\n}: StreamStatusProps) => {\n const resolvedState: StreamStatusState =\n stateProp ?? (isStreaming ? \"streaming\" : \"idle\");\n\n const resolvedIcon =\n icon === undefined\n ? iconVariant === undefined\n ? undefined\n : STREAM_STATUS_ICONS[iconVariant]\n : icon;\n\n const [elapsed, setElapsed] = useState(() => {\n if (startTime === undefined) return 0;\n const origin =\n typeof startTime === \"number\" ? startTime : startTime.getTime();\n return Math.max(0, Math.floor((Date.now() - origin) / MS_PER_S));\n });\n\n // Bubble-confirm ripple — fires once when isStreaming transitions true → false\n const [confirming, setConfirming] = useState(false);\n const prevStreamingRef = useRef(isStreaming);\n\n useEffect(() => {\n const was = prevStreamingRef.current;\n prevStreamingRef.current = isStreaming;\n if (was && !isStreaming) {\n setConfirming(true);\n const t = setTimeout(() => setConfirming(false), CONFIRM_RIPPLE_MS);\n return () => clearTimeout(t);\n }\n }, [isStreaming]);\n\n // Elapsed-time ticker — only active while streaming\n useEffect(() => {\n if (!isStreaming || startTime === undefined) return;\n const origin =\n typeof startTime === \"number\" ? startTime : startTime.getTime();\n const tick = () =>\n setElapsed(Math.max(0, Math.floor((Date.now() - origin) / MS_PER_S)));\n tick();\n const id = setInterval(tick, MS_PER_S);\n return () => clearInterval(id);\n }, [isStreaming, startTime]);\n\n const showTime = startTime !== undefined;\n const showTokens = tokenCount !== undefined;\n\n return (\n <div\n aria-live=\"polite\"\n className={cn(\n \"flex items-center gap-2 text-sm text-muted-foreground\",\n className\n )}\n >\n {/* Spinning icon — spins + pulse-surges while streaming */}\n {resolvedIcon !== undefined && (\n <span\n className={cn(\n \"shrink-0 [&>svg]:size-3.5\",\n isStreaming ? \"ts-spin-pulse\" : \"opacity-40\"\n )}\n >\n {resolvedIcon}\n </span>\n )}\n\n {/* Elapsed time */}\n {showTime && (\n <span className=\"tabular-nums\">{formatElapsed(elapsed)}</span>\n )}\n\n {/* Token count */}\n {showTokens && (\n <>\n <span aria-hidden className=\"select-none text-muted-foreground/40\">\n ·\n </span>\n <span className=\"tabular-nums\">\n {tokenLabel != null && (\n <span className=\"mr-0.5\">{tokenLabel}</span>\n )}\n {formatTokens(tokenCount)} tokens\n </span>\n </>\n )}\n\n {/* Right-end indicator dot — opt-in, colour driven by `state` */}\n {showIndicator && (\n <span className=\"relative flex size-3 shrink-0 items-center justify-center\">\n {confirming && (\n <span className=\"ts-bubble-confirm absolute size-2 rounded-full bg-[#549DFF]\" />\n )}\n <span\n className={cn(\n \"size-1.5 rounded-full transition-colors duration-500\",\n INDICATOR_CLASS[resolvedState]\n )}\n />\n </span>\n )}\n </div>\n );\n};\n\nexport const StreamStatus = memo(StreamStatusComponent);\nStreamStatus.displayName = \"StreamStatus\";\n"],"names":["MS_PER_S","SECS_PER_MIN","CONFIRM_RIPPLE_MS","K","M","STREAM_STATUS_ICONS","LoaderIcon","LoaderCircleIcon","LoaderPinwheelIcon","Disc3Icon","INDICATOR_CLASS","formatElapsed","seconds","m","s","formatTokens","count","StreamStatusComponent","startTime","isStreaming","stateProp","showIndicator","tokenCount","tokenLabel","icon","iconVariant","className","resolvedState","resolvedIcon","elapsed","setElapsed","useState","origin","confirming","setConfirming","prevStreamingRef","useRef","useEffect","was","t","tick","id","showTime","showTokens","jsxs","cn","jsx","Fragment","StreamStatus","memo"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import { jsxs as a, jsx as n, Fragment as $ } from "react/jsx-runtime";
2
+ import { Disc3Icon as D, LoaderPinwheelIcon as k, LoaderCircleIcon as y, LoaderIcon as P } from "lucide-react";
3
+ import { memo as z, useState as v, useRef as A, useEffect as S } from "react";
4
+ import { cn as c } from "../../lib/utils.js";
5
+ const l = 1e3, g = 60, L = 700, N = 1e3, I = 1e6, j = {
6
+ loader: /* @__PURE__ */ n(P, {}),
7
+ "loader-circle": /* @__PURE__ */ n(y, {}),
8
+ "loader-pinwheel": /* @__PURE__ */ n(k, {}),
9
+ "disc-3": /* @__PURE__ */ n(D, {})
10
+ }, O = {
11
+ streaming: "animate-pulse bg-[#549DFF]",
12
+ // TS Light Blue 300
13
+ idle: "bg-muted-foreground/40",
14
+ done: "bg-[#038599]",
15
+ // TS Forest Green 300
16
+ error: "bg-[#E15759]"
17
+ // TS Imperial Red
18
+ };
19
+ function K(e) {
20
+ const o = Math.floor(e / g), r = e % g;
21
+ return o > 0 ? `${o}m ${r.toString().padStart(2, "0")}s` : `${r}s`;
22
+ }
23
+ function T(e) {
24
+ return e >= I ? `${(e / I).toFixed(1).replace(/\.0$/, "")}m` : e >= N ? `${(e / N).toFixed(1).replace(/\.0$/, "")}k` : String(e);
25
+ }
26
+ const U = ({
27
+ startTime: e,
28
+ isStreaming: o = e !== void 0,
29
+ state: r,
30
+ showIndicator: b = !1,
31
+ tokenCount: i,
32
+ tokenLabel: d = "↓",
33
+ icon: u,
34
+ iconVariant: m,
35
+ className: x
36
+ }) => {
37
+ const M = r ?? (o ? "streaming" : "idle"), f = u === void 0 ? m === void 0 ? void 0 : j[m] : u, [E, _] = v(() => {
38
+ if (e === void 0) return 0;
39
+ const t = typeof e == "number" ? e : e.getTime();
40
+ return Math.max(0, Math.floor((Date.now() - t) / l));
41
+ }), [F, p] = v(!1), h = A(o);
42
+ S(() => {
43
+ const t = h.current;
44
+ if (h.current = o, t && !o) {
45
+ p(!0);
46
+ const s = setTimeout(() => p(!1), L);
47
+ return () => clearTimeout(s);
48
+ }
49
+ }, [o]), S(() => {
50
+ if (!o || e === void 0) return;
51
+ const t = typeof e == "number" ? e : e.getTime(), s = () => _(Math.max(0, Math.floor((Date.now() - t) / l)));
52
+ s();
53
+ const C = setInterval(s, l);
54
+ return () => clearInterval(C);
55
+ }, [o, e]);
56
+ const R = e !== void 0, w = i !== void 0;
57
+ return /* @__PURE__ */ a(
58
+ "div",
59
+ {
60
+ "aria-live": "polite",
61
+ className: c(
62
+ "flex items-center gap-2 text-sm text-muted-foreground",
63
+ x
64
+ ),
65
+ children: [
66
+ f !== void 0 && /* @__PURE__ */ n(
67
+ "span",
68
+ {
69
+ className: c(
70
+ "shrink-0 [&>svg]:size-3.5",
71
+ o ? "ts-spin-pulse" : "opacity-40"
72
+ ),
73
+ children: f
74
+ }
75
+ ),
76
+ R && /* @__PURE__ */ n("span", { className: "tabular-nums", children: K(E) }),
77
+ w && /* @__PURE__ */ a($, { children: [
78
+ /* @__PURE__ */ n("span", { "aria-hidden": !0, className: "select-none text-muted-foreground/40", children: "·" }),
79
+ /* @__PURE__ */ a("span", { className: "tabular-nums", children: [
80
+ d != null && /* @__PURE__ */ n("span", { className: "mr-0.5", children: d }),
81
+ T(i),
82
+ " tokens"
83
+ ] })
84
+ ] }),
85
+ b && /* @__PURE__ */ a("span", { className: "relative flex size-3 shrink-0 items-center justify-center", children: [
86
+ F && /* @__PURE__ */ n("span", { className: "ts-bubble-confirm absolute size-2 rounded-full bg-[#549DFF]" }),
87
+ /* @__PURE__ */ n(
88
+ "span",
89
+ {
90
+ className: c(
91
+ "size-1.5 rounded-full transition-colors duration-500",
92
+ O[M]
93
+ )
94
+ }
95
+ )
96
+ ] })
97
+ ]
98
+ }
99
+ );
100
+ }, q = z(U);
101
+ q.displayName = "StreamStatus";
102
+ export {
103
+ j as STREAM_STATUS_ICONS,
104
+ q as StreamStatus
105
+ };
106
+ //# sourceMappingURL=stream-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-status.js","sources":["../../../src/components/ai/stream-status.tsx"],"sourcesContent":["import {\n Disc3Icon,\n LoaderCircleIcon,\n LoaderIcon,\n LoaderPinwheelIcon,\n} from \"lucide-react\";\nimport { memo, useEffect, useRef, useState } from \"react\";\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst MS_PER_S = 1000;\nconst SECS_PER_MIN = 60;\nconst CONFIRM_RIPPLE_MS = 700;\nconst K = 1_000;\nconst M = 1_000_000;\n\n// ---------------------------------------------------------------------------\n// Icon variants\n// ---------------------------------------------------------------------------\n\n/** Built-in Lucide spinner icons that pair well with ts-spin-pulse animation. */\nexport const STREAM_STATUS_ICONS = {\n loader: <LoaderIcon />,\n \"loader-circle\": <LoaderCircleIcon />,\n \"loader-pinwheel\": <LoaderPinwheelIcon />,\n \"disc-3\": <Disc3Icon />,\n} as const;\n\nexport type StreamStatusIconVariant = keyof typeof STREAM_STATUS_ICONS;\n\n// ---------------------------------------------------------------------------\n// State → indicator dot colour\n// ---------------------------------------------------------------------------\n\nexport type StreamStatusState = \"streaming\" | \"idle\" | \"done\" | \"error\";\n\nconst INDICATOR_CLASS: Record<StreamStatusState, string> = {\n streaming: \"animate-pulse bg-[#549DFF]\", // TS Light Blue 300\n idle: \"bg-muted-foreground/40\",\n done: \"bg-[#038599]\", // TS Forest Green 300\n error: \"bg-[#E15759]\", // TS Imperial Red\n};\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport interface StreamStatusProps {\n /**\n * Timestamp when streaming started (Date or epoch ms).\n * The component manages an internal 1-second ticker when set.\n */\n startTime?: Date | number;\n /** Whether streaming is currently active. Defaults to `true` when `startTime` is set. */\n isStreaming?: boolean;\n /**\n * Explicit state for the right-end indicator dot colour.\n * If omitted, derived from `isStreaming`: `true` → \"streaming\", `false` → \"idle\".\n *\n * | state | colour |\n * |-------------|----------------------------|\n * | streaming | TS Light Blue 300 (pulsing)|\n * | idle | muted gray |\n * | done | TS Forest Green 300 |\n * | error | TS Imperial Red |\n */\n state?: StreamStatusState;\n /**\n * Show the coloured status dot at the right end of the component.\n * Defaults to `false`.\n */\n showIndicator?: boolean;\n /** Token count shown after the separator, e.g. `8700` → \"↓ 8.7k tokens\". */\n tokenCount?: number;\n /** Prefix before the formatted token count. Defaults to `↓`. */\n tokenLabel?: ReactNode;\n /**\n * Custom icon (ReactNode). Takes precedence over `iconVariant`.\n * While streaming, the icon wrapper receives `ts-spin-pulse` — a continuous\n * rotation that occasionally surges to 1.3× scale.\n */\n icon?: ReactNode;\n /**\n * Convenience shorthand to pick a built-in Lucide spinner.\n * Ignored when `icon` is also provided.\n *\n * Options: `\"loader\"` | `\"loader-circle\"` | `\"loader-pinwheel\"` | `\"disc-3\"`\n */\n iconVariant?: StreamStatusIconVariant;\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatElapsed(seconds: number): string {\n const m = Math.floor(seconds / SECS_PER_MIN);\n const s = seconds % SECS_PER_MIN;\n return m > 0 ? `${m}m ${s.toString().padStart(2, \"0\")}s` : `${s}s`;\n}\n\nfunction formatTokens(count: number): string {\n if (count >= M) return `${(count / M).toFixed(1).replace(/\\.0$/, \"\")}m`;\n if (count >= K) return `${(count / K).toFixed(1).replace(/\\.0$/, \"\")}k`;\n return String(count);\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nconst StreamStatusComponent = ({\n startTime,\n isStreaming = startTime !== undefined,\n state: stateProp,\n showIndicator = false,\n tokenCount,\n tokenLabel = \"↓\",\n icon,\n iconVariant,\n className,\n}: StreamStatusProps) => {\n const resolvedState: StreamStatusState =\n stateProp ?? (isStreaming ? \"streaming\" : \"idle\");\n\n const resolvedIcon =\n icon === undefined\n ? iconVariant === undefined\n ? undefined\n : STREAM_STATUS_ICONS[iconVariant]\n : icon;\n\n const [elapsed, setElapsed] = useState(() => {\n if (startTime === undefined) return 0;\n const origin =\n typeof startTime === \"number\" ? startTime : startTime.getTime();\n return Math.max(0, Math.floor((Date.now() - origin) / MS_PER_S));\n });\n\n // Bubble-confirm ripple — fires once when isStreaming transitions true → false\n const [confirming, setConfirming] = useState(false);\n const prevStreamingRef = useRef(isStreaming);\n\n useEffect(() => {\n const was = prevStreamingRef.current;\n prevStreamingRef.current = isStreaming;\n if (was && !isStreaming) {\n setConfirming(true);\n const t = setTimeout(() => setConfirming(false), CONFIRM_RIPPLE_MS);\n return () => clearTimeout(t);\n }\n }, [isStreaming]);\n\n // Elapsed-time ticker — only active while streaming\n useEffect(() => {\n if (!isStreaming || startTime === undefined) return;\n const origin =\n typeof startTime === \"number\" ? startTime : startTime.getTime();\n const tick = () =>\n setElapsed(Math.max(0, Math.floor((Date.now() - origin) / MS_PER_S)));\n tick();\n const id = setInterval(tick, MS_PER_S);\n return () => clearInterval(id);\n }, [isStreaming, startTime]);\n\n const showTime = startTime !== undefined;\n const showTokens = tokenCount !== undefined;\n\n return (\n <div\n aria-live=\"polite\"\n className={cn(\n \"flex items-center gap-2 text-sm text-muted-foreground\",\n className\n )}\n >\n {/* Spinning icon — spins + pulse-surges while streaming */}\n {resolvedIcon !== undefined && (\n <span\n className={cn(\n \"shrink-0 [&>svg]:size-3.5\",\n isStreaming ? \"ts-spin-pulse\" : \"opacity-40\"\n )}\n >\n {resolvedIcon}\n </span>\n )}\n\n {/* Elapsed time */}\n {showTime && (\n <span className=\"tabular-nums\">{formatElapsed(elapsed)}</span>\n )}\n\n {/* Token count */}\n {showTokens && (\n <>\n <span aria-hidden className=\"select-none text-muted-foreground/40\">\n ·\n </span>\n <span className=\"tabular-nums\">\n {tokenLabel != null && (\n <span className=\"mr-0.5\">{tokenLabel}</span>\n )}\n {formatTokens(tokenCount)} tokens\n </span>\n </>\n )}\n\n {/* Right-end indicator dot — opt-in, colour driven by `state` */}\n {showIndicator && (\n <span className=\"relative flex size-3 shrink-0 items-center justify-center\">\n {confirming && (\n <span className=\"ts-bubble-confirm absolute size-2 rounded-full bg-[#549DFF]\" />\n )}\n <span\n className={cn(\n \"size-1.5 rounded-full transition-colors duration-500\",\n INDICATOR_CLASS[resolvedState]\n )}\n />\n </span>\n )}\n </div>\n );\n};\n\nexport const StreamStatus = memo(StreamStatusComponent);\nStreamStatus.displayName = \"StreamStatus\";\n"],"names":["MS_PER_S","SECS_PER_MIN","CONFIRM_RIPPLE_MS","K","M","STREAM_STATUS_ICONS","LoaderIcon","LoaderCircleIcon","LoaderPinwheelIcon","Disc3Icon","INDICATOR_CLASS","formatElapsed","seconds","m","s","formatTokens","count","StreamStatusComponent","startTime","isStreaming","stateProp","showIndicator","tokenCount","tokenLabel","icon","iconVariant","className","resolvedState","resolvedIcon","elapsed","setElapsed","useState","origin","confirming","setConfirming","prevStreamingRef","useRef","useEffect","was","t","tick","id","showTime","showTokens","jsxs","cn","jsx","Fragment","StreamStatus","memo"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),a=require("react"),x=require("../ui/button.cjs"),o=require("../../lib/utils.cjs"),d=({className:e,children:t,...n})=>s.jsx("div",{className:"w-full overflow-x-auto py-1",...n,children:s.jsx("div",{className:o.cn("flex w-max flex-nowrap items-center gap-2 px-4",e),children:t})}),m=({suggestion:e,onClick:t,className:n,variant:r="outline",size:u="sm",children:c,...l})=>{const i=a.useCallback(()=>{t?.(e)},[t,e]);return s.jsx(x.Button,{className:o.cn("cursor-pointer rounded-full px-4",n),onClick:i,size:u,type:"button",variant:r,...l,children:c||e})};exports.Suggestion=m;exports.Suggestions=d;
2
+ //# sourceMappingURL=suggestion.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggestion.cjs","sources":["../../../src/components/ai/suggestion.tsx"],"sourcesContent":["import { useCallback } from \"react\";\n\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SuggestionsProps = ComponentProps<\"div\">;\n\nexport const Suggestions = ({\n className,\n children,\n ...props\n}: SuggestionsProps) => (\n <div className=\"w-full overflow-x-auto py-1\" {...props}>\n <div className={cn(\"flex w-max flex-nowrap items-center gap-2 px-4\", className)}>\n {children}\n </div>\n </div>\n);\n\nexport type SuggestionProps = Omit<ComponentProps<typeof Button>, \"onClick\"> & {\n suggestion: string;\n onClick?: (suggestion: string) => void;\n};\n\nexport const Suggestion = ({\n suggestion,\n onClick,\n className,\n variant = \"outline\",\n size = \"sm\",\n children,\n ...props\n}: SuggestionProps) => {\n const handleClick = useCallback(() => {\n onClick?.(suggestion);\n }, [onClick, suggestion]);\n\n return (\n <Button\n className={cn(\"cursor-pointer rounded-full px-4\", className)}\n onClick={handleClick}\n size={size}\n type=\"button\"\n variant={variant}\n {...props}\n >\n {children || suggestion}\n </Button>\n );\n};\n"],"names":["Suggestions","className","children","props","jsx","cn","Suggestion","suggestion","onClick","variant","size","handleClick","useCallback","Button"],"mappings":"uMASaA,EAAc,CAAC,CAC1B,UAAAC,EACA,SAAAC,EACA,GAAGC,CACL,IACEC,EAAAA,IAAC,MAAA,CAAI,UAAU,8BAA+B,GAAGD,EAC/C,SAAAC,EAAAA,IAAC,MAAA,CAAI,UAAWC,KAAG,iDAAkDJ,CAAS,EAC3E,SAAAC,EACH,CAAA,CACF,EAQWI,EAAa,CAAC,CACzB,WAAAC,EACA,QAAAC,EACA,UAAAP,EACA,QAAAQ,EAAU,UACV,KAAAC,EAAO,KACP,SAAAR,EACA,GAAGC,CACL,IAAuB,CACrB,MAAMQ,EAAcC,EAAAA,YAAY,IAAM,CACpCJ,IAAUD,CAAU,CACtB,EAAG,CAACC,EAASD,CAAU,CAAC,EAExB,OACEH,EAAAA,IAACS,EAAAA,OAAA,CACC,UAAWR,EAAAA,GAAG,mCAAoCJ,CAAS,EAC3D,QAASU,EACT,KAAAD,EACA,KAAK,SACL,QAAAD,EACC,GAAGN,EAEH,SAAAD,GAAYK,CAAA,CAAA,CAGnB"}