{"version":3,"file":"prompt-input-root.cjs","sources":["../../../components/prompt-input/prompt-input-root.tsx"],"sourcesContent":["'use client';\n\nimport { useControlled } from '@base-ui/utils/useControlled';\nimport { cx } from 'class-variance-authority';\nimport {\n  type ComponentProps,\n  type FormEvent,\n  type MouseEvent,\n  type RefObject,\n  useCallback,\n  useImperativeHandle,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState\n} from 'react';\n// Imported from the leaf module, not the barrel: the root must not pull the\n// editor engine into a composer that only ever renders `Textarea`.\nimport { trimDetails } from '../editor/mention';\nimport styles from './prompt-input.module.css';\nimport {\n  isEmptyValue,\n  PromptInputContext,\n  type PromptInputContextValue,\n  type PromptInputInputApi,\n  type PromptInputMention,\n  type PromptInputMessage,\n  type PromptInputPartKind,\n  type PromptInputStatus,\n  type PromptInputValueDetails\n} from './prompt-input-context';\nimport {\n  type PromptInputMentionItem,\n  PromptInputMentionRegistry\n} from './prompt-input-mention-registry';\n\nexport interface PromptInputActions {\n  /** Focuses the input, dropping the caret at the end of the draft. */\n  focus: () => void;\n  /** Clears the composer. */\n  clear: () => void;\n  /**\n   * Inserts a chip at the caret, or at the end of the draft when the editor is\n   * not focused, followed by a trailing space. Requires `PromptInput.Editor`.\n   */\n  insertMention: (\n    item: PromptInputMentionItem,\n    options?: { trigger?: string }\n  ) => void;\n  /** The current value, untrimmed. */\n  getValue: () => PromptInputMessage;\n}\n\nexport interface PromptInputRootProps\n  extends Omit<ComponentProps<'form'>, 'onSubmit'> {\n  /**\n   * Controlled markup string — the same dialect `onValueChange` reports and\n   * `PromptInput.Editor` parses. Most consumers should stay uncontrolled.\n   */\n  value?: string;\n  /**\n   * The initial markup when uncontrolled.\n   * @defaultValue \"\"\n   */\n  defaultValue?: string;\n  /**\n   * Called on every change. The first argument round-trips into `value`\n   * losslessly, chips included.\n   */\n  onValueChange?: (\n    markup: string,\n    details: { text: string; mentions: PromptInputMention[] }\n  ) => void;\n  /**\n   * Called with the trimmed message when the prompt is submitted — Enter in the\n   * input or a click on `PromptInput.Submit`. Call\n   * `event.currentTarget.reset()` to clear the composer after sending.\n   */\n  onSubmit?: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void;\n  /**\n   * Called when `PromptInput.Submit` is pressed while `status` is\n   * `\"submitted\"` or `\"streaming\"`.\n   */\n  onStop?: () => void;\n  /**\n   * The consumer-owned request lifecycle. Drives `PromptInput.Submit`:\n   * `\"idle\"`/`\"error\"` show a send arrow, `\"submitted\"` a spinner and\n   * `\"streaming\"` a stop square (both routed to `onStop`).\n   * @defaultValue \"idle\"\n   */\n  status?: PromptInputStatus;\n  /**\n   * Disables the whole composer.\n   * @defaultValue false\n   */\n  disabled?: boolean;\n  /**\n   * The element the frame focuses when its own padding is clicked.\n   * `PromptInput.Textarea` and `PromptInput.Editor` register themselves here on\n   * mount; pass a ref of your own when you render a custom input instead.\n   * Whichever is set first wins.\n   */\n  inputRef?: RefObject<HTMLElement | null>;\n  /** Imperative handle. `ref` remains the `<form>` element. */\n  actionsRef?: RefObject<PromptInputActions | null>;\n}\n\nconst EMPTY_DETAILS: PromptInputValueDetails = { text: '', mentions: [] };\n\n/**\n * Stand-in until a part registers, and the whole story for a plain string: no\n * markup is interpreted, so a literal `@[x](y:z)` stays literal.\n */\nfunction literalDetails(markup: string): PromptInputValueDetails {\n  return markup === '' ? EMPTY_DETAILS : { text: markup, mentions: [] };\n}\n\nfunction sameDetails(\n  a: PromptInputValueDetails,\n  b: PromptInputValueDetails\n): boolean {\n  if (a === b) return true;\n  if (a.text !== b.text || a.mentions.length !== b.mentions.length) {\n    return false;\n  }\n  return a.mentions.every((mention, index) => {\n    const other = b.mentions[index];\n    return (\n      mention.id === other.id &&\n      mention.type === other.type &&\n      mention.trigger === other.trigger &&\n      mention.start === other.start\n    );\n  });\n}\n\nexport function PromptInputRoot({\n  className,\n  value: valueProp,\n  defaultValue = '',\n  onValueChange,\n  onSubmit,\n  onStop,\n  onReset,\n  onMouseDown,\n  status = 'idle',\n  disabled = false,\n  inputRef: inputRefProp,\n  actionsRef,\n  children,\n  ref,\n  ...props\n}: PromptInputRootProps) {\n  const formRef = useRef<HTMLFormElement | null>(null);\n  const ownInputRef = useRef<HTMLElement | null>(null);\n  const inputRef = inputRefProp ?? ownInputRef;\n  const apiRef = useRef<PromptInputInputApi | null>(null);\n  const partKindRef = useRef<PromptInputPartKind | null>(null);\n  const [editorMounted, setEditorMounted] = useState(false);\n\n  const mentions = useMemo(() => new PromptInputMentionRegistry(), []);\n\n  const registerInput = useCallback(\n    (\n      node: HTMLElement | null,\n      api?: PromptInputInputApi,\n      kind?: PromptInputPartKind\n    ) => {\n      // The part that holds the slot is unmounting: hand it back, so a\n      // replacement can take it, `getMessage` never reads off a destroyed\n      // view, and swapping parts is not mistaken for mounting both. Guarded on\n      // identity, so a consumer-supplied `inputRef` that won the race is left\n      // alone.\n      if (!node) {\n        if (api && apiRef.current === api) {\n          apiRef.current = null;\n          partKindRef.current = null;\n          inputRef.current = null;\n          setEditorMounted(false);\n        }\n        return;\n      }\n\n      if (\n        process.env.NODE_ENV !== 'production' &&\n        kind &&\n        partKindRef.current &&\n        partKindRef.current !== kind\n      ) {\n        console.warn(\n          '[Apsara] PromptInput.Textarea and PromptInput.Editor are mutually ' +\n            'exclusive; the first to mount wins. Render one input part.'\n        );\n      }\n\n      const current = inputRef.current;\n      if (current && current.isConnected) return;\n\n      inputRef.current = node;\n      if (api) apiRef.current = api;\n      if (kind) {\n        partKindRef.current = kind;\n        setEditorMounted(kind === 'editor');\n      }\n    },\n    [inputRef]\n  );\n\n  const [value, setValueUnwrapped] = useControlled({\n    controlled: valueProp,\n    default: defaultValue,\n    name: 'PromptInput',\n    state: 'value'\n  });\n\n  const [details, setDetails] = useState<PromptInputValueDetails>(() =>\n    literalDetails(value)\n  );\n  /** The last markup the mounted part reported, so echoes are not re-applied. */\n  const reportedRef = useRef<string | null>(null);\n\n  const onValueChangeRef = useRef(onValueChange);\n  onValueChangeRef.current = onValueChange;\n\n  /** Fills in `data`, which never survives serialization. */\n  const withData = useCallback(\n    (list: PromptInputMention[]): PromptInputMention[] =>\n      list.map(mention => {\n        const item = mentions.lookup(mention.trigger, mention.type, mention.id);\n        return item && 'data' in item\n          ? { ...mention, data: item.data }\n          : mention;\n      }),\n    [mentions]\n  );\n\n  const setValue = useCallback(\n    (markup: string, next: PromptInputValueDetails) => {\n      const enriched = { text: next.text, mentions: withData(next.mentions) };\n      reportedRef.current = markup;\n      setValueUnwrapped(markup);\n      setDetails(current =>\n        sameDetails(current, enriched) ? current : enriched\n      );\n      onValueChangeRef.current?.(markup, enriched);\n    },\n    [setValueUnwrapped, withData]\n  );\n\n  /** A value Root itself pushed — a form reset, or `actionsRef.clear()`. */\n  const applyValue = useCallback(\n    (markup: string) => {\n      const derived =\n        apiRef.current?.deriveExternal(markup) ?? literalDetails(markup);\n      const enriched = {\n        text: derived.text,\n        mentions: withData(derived.mentions)\n      };\n      reportedRef.current = markup;\n      setValueUnwrapped(markup);\n      setDetails(current =>\n        sameDetails(current, enriched) ? current : enriched\n      );\n      apiRef.current?.setMarkup(markup);\n      onValueChangeRef.current?.(markup, enriched);\n    },\n    [setValueUnwrapped, withData]\n  );\n\n  // Reconciles a value that did not come from the part: a controlled prop the\n  // consumer changed, or a controlled prop they did *not* change after a\n  // keystroke — in which case the part is asked to revert, which is what\n  // `Textarea` has always done by rendering `value` straight through. Runs after\n  // every render, because a controlled prop that stays put still needs it.\n  useLayoutEffect(() => {\n    if (reportedRef.current === value) return;\n    reportedRef.current = value;\n    const derived =\n      apiRef.current?.deriveExternal(value) ?? literalDetails(value);\n    const enriched = {\n      text: derived.text,\n      mentions: withData(derived.mentions)\n    };\n    setDetails(current =>\n      sameDetails(current, enriched) ? current : enriched\n    );\n    apiRef.current?.setMarkup(value);\n  });\n\n  const requestSubmit = useCallback(() => {\n    const form = formRef.current;\n    if (!form) return;\n    if (typeof form.requestSubmit === 'function') {\n      form.requestSubmit();\n    } else {\n      form.dispatchEvent(\n        new Event('submit', { cancelable: true, bubbles: true })\n      );\n    }\n  }, []);\n\n  /** Live, so a submit in the same tick as a keystroke is never a render behind. */\n  const readMessage = useCallback((): PromptInputMessage => {\n    const message = apiRef.current?.getMessage() ?? {\n      markup: value,\n      ...literalDetails(value)\n    };\n    return { ...message, mentions: withData(message.mentions) };\n  }, [value, withData]);\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (disabled || status === 'submitted' || status === 'streaming') return;\n    const message = readMessage();\n    if (isEmptyValue(message)) return;\n    onSubmit?.(trimDetails(message), event);\n  };\n\n  const handleReset = (event: FormEvent<HTMLFormElement>) => {\n    onReset?.(event);\n    if (event.defaultPrevented) return;\n    applyValue('');\n  };\n\n  const focusInput = useCallback(() => {\n    if (apiRef.current) apiRef.current.focus();\n    else inputRef.current?.focus();\n  }, [inputRef]);\n\n  // The frame reads as one field, so a press on its own layout focuses the\n  // input. The header and footer are out of hit testing (see the stylesheet),\n  // so a press on their padding lands on the form itself while their contents\n  // keep their own. Handled on mousedown so focus never leaves the input and\n  // back again — that round trip would dismiss anything anchored to it.\n  const handleMouseDown = (event: MouseEvent<HTMLFormElement>) => {\n    onMouseDown?.(event);\n    if (event.defaultPrevented || disabled || event.button !== 0) return;\n    if (event.target !== event.currentTarget) return;\n    event.preventDefault();\n    focusInput();\n  };\n\n  useImperativeHandle(\n    actionsRef,\n    () => ({\n      focus: focusInput,\n      clear: () => applyValue(''),\n      insertMention: (item, options) => {\n        if (!apiRef.current?.insertMention) {\n          if (process.env.NODE_ENV !== 'production') {\n            console.warn(\n              '[Apsara] PromptInput actions.insertMention requires ' +\n                '<PromptInput.Editor>; a native textarea cannot host chips.'\n            );\n          }\n          return;\n        }\n        apiRef.current.insertMention(item, options);\n      },\n      getValue: readMessage\n    }),\n    [applyValue, focusInput, readMessage]\n  );\n\n  const empty = isEmptyValue(details);\n\n  const contextValue = useMemo<PromptInputContextValue>(\n    () => ({\n      value,\n      details,\n      empty,\n      setValue,\n      status,\n      disabled,\n      onStop,\n      inputRef,\n      frameRef: formRef,\n      registerInput,\n      requestSubmit,\n      mentions,\n      editorMounted\n    }),\n    [\n      value,\n      details,\n      empty,\n      setValue,\n      status,\n      disabled,\n      onStop,\n      inputRef,\n      registerInput,\n      requestSubmit,\n      mentions,\n      editorMounted\n    ]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        ref={node => {\n          formRef.current = node;\n          if (typeof ref === 'function') ref(node);\n          else if (ref) ref.current = node;\n        }}\n        className={cx(styles.root, className)}\n        data-slot='prompt-input'\n        data-status={status}\n        data-disabled={disabled || undefined}\n        data-empty={empty || undefined}\n        onSubmit={handleSubmit}\n        onReset={handleReset}\n        onMouseDown={handleMouseDown}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nPromptInputRoot.displayName = 'PromptInput';\n"],"names":[],"mappings":";;;;;;;;;;;;AA8GA;AAEA;;;AAGG;AACH;AACE;AACF;AAEA;;AAIe;;AAEX;;;;AAIA;AAEE;AACA;AACA;AAEJ;AACF;AAEM;AAiBJ;AACA;AACA;AACA;AACA;;AAGA;;;;;;;;;AAeQ;AACA;AACA;;;;;AAMJ;;AAGE;AACA;;AAII;;AAIN;AACA;;AAEA;AACA;AAAS;;AAEP;AACA;;AAEJ;AAIF;AACE;AACA;AACA;AACA;AACD;AAED;;AAIA;AAEA;AACA;;AAGA;AAGM;AACA;;;AAGF;;AAMA;AACA;;;;AAMF;;AAKF;AAEI;AAEA;;AAEE;;AAEF;;;AAKA;;AAEF;;;;;;;AAUA;;AACA;AACA;AAEA;;AAEE;;;AAKF;AACF;AAEA;AACE;AACA;;AACA;;;;AAGE;;;;AAOJ;;AAEI;;;AAGF;AACF;AAEA;;;;AAGE;;;;AAGF;AAEA;AACE;;;;AAGF;AAEA;;AACsB;;AACf;AACP;;;;;;AAOA;AACE;;;AAEA;;;AAEA;AACF;AAEA;AAGI;AACA;AACA;AACE;;;AAIQ;;;;;;AAOV;;AAKJ;AAEA;;;;;;;;;AAUI;;;;;AAKD;;;;;;;;;;;;;AAcA;AAGH;AAIQ;;;AAEK;AAAS;AAChB;AAeR;AAEA;;"}