Просмотр исходного кода

Writefull refactor bootstrapping (#29549)

GitOrigin-RevId: f8422d3c3d1d7e588f18352e08c7596b520225e5
Borja 8 месяцев назад
Родитель
Сommit
b1009d3b1f
35 измененных файлов с 33 добавлено и 3079 удалено
  1. 0 21
      libraries/ai/components.json
  2. 0 13
      libraries/ai/components/ai-elements/LICENSE
  3. 0 1
      libraries/ai/components/ai-elements/README.md
  4. 0 97
      libraries/ai/components/ai-elements/conversation.tsx
  5. 0 58
      libraries/ai/components/ai-elements/message.tsx
  6. 0 1272
      libraries/ai/components/ai-elements/prompt-input.tsx
  7. 0 178
      libraries/ai/components/ai-elements/reasoning.tsx
  8. 0 22
      libraries/ai/components/ai-elements/response.tsx
  9. 0 64
      libraries/ai/components/ai-elements/shimmer.tsx
  10. 0 51
      libraries/ai/components/chat-prompt-input.tsx
  11. 0 187
      libraries/ai/components/chat.tsx
  12. 0 32
      libraries/ai/components/shadow-root-portal.tsx
  13. 0 21
      libraries/ai/components/ui/LICENSE.md
  14. 0 1
      libraries/ai/components/ui/README.md
  15. 0 66
      libraries/ai/components/ui/alert.tsx
  16. 0 56
      libraries/ai/components/ui/button.tsx
  17. 0 9
      libraries/ai/components/ui/collapsible.tsx
  18. 0 200
      libraries/ai/components/ui/dropdown-menu.tsx
  19. 0 27
      libraries/ai/components/ui/hover-card.tsx
  20. 0 168
      libraries/ai/components/ui/input-group.tsx
  21. 0 22
      libraries/ai/components/ui/input.tsx
  22. 0 158
      libraries/ai/components/ui/select.tsx
  23. 0 14
      libraries/ai/components/ui/skeleton.tsx
  24. 0 22
      libraries/ai/components/ui/textarea.tsx
  25. 0 10
      libraries/ai/index.ts
  26. 0 37
      libraries/ai/package.json
  27. 0 9
      libraries/ai/postcss.config.js
  28. 0 85
      libraries/ai/tailwind.config.js
  29. 0 157
      libraries/ai/tailwind.css
  30. 0 12
      libraries/ai/tsconfig.json
  31. 0 6
      libraries/ai/utils/index.ts
  32. 2 0
      services/web/config/settings.defaults.js
  33. 22 2
      services/web/frontend/js/features/ide-react/context/react-context-root.tsx
  34. 9 0
      services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx
  35. 0 1
      services/web/frontend/js/shared/context/types/writefull-instance.ts

+ 0 - 21
libraries/ai/components.json

@@ -1,21 +0,0 @@
-{
-  "$schema": "https://ui.shadcn.com/schema.json",
-  "style": "default",
-  "rsc": false,
-  "tsx": true,
-  "tailwind": {
-    "config": "tailwind.config.js",
-    "css": "tailwind.css",
-    "baseColor": "slate",
-    "cssVariables": true
-  },
-  "iconLibrary": "lucide",
-  "aliases": {
-    "components": "@overleaf/ai/components",
-    "utils": "@overleaf/ai/utils",
-    "ui": "@overleaf/ai/components/ui"
-  },
-  "registries": {
-    "@ai-elements": "https://registry.ai-sdk.dev/{name}.json"
-  }
-}

+ 0 - 13
libraries/ai/components/ai-elements/LICENSE

@@ -1,13 +0,0 @@
-Copyright 2023 Vercel, Inc.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.

+ 0 - 1
libraries/ai/components/ai-elements/README.md

@@ -1 +0,0 @@
-AI Elements components are derived from https://github.com/vercel/ai-elements

+ 0 - 97
libraries/ai/components/ai-elements/conversation.tsx

@@ -1,97 +0,0 @@
-'use client'
-
-import { Button } from '../ui/button'
-import { cn } from '../../utils'
-import { ArrowDownIcon } from 'lucide-react'
-import type { ComponentProps } from 'react'
-import React, { useCallback } from 'react'
-import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'
-
-export type ConversationProps = ComponentProps<typeof StickToBottom>
-
-export const Conversation = ({ className, ...props }: ConversationProps) => (
-  <StickToBottom
-    className={cn('relative flex-1 overflow-y-auto', className)}
-    initial="smooth"
-    resize="smooth"
-    role="log"
-    {...props}
-  />
-)
-
-export type ConversationContentProps = ComponentProps<
-  typeof StickToBottom.Content
->
-
-export const ConversationContent = ({
-  className,
-  ...props
-}: ConversationContentProps) => (
-  <StickToBottom.Content className={cn('p-4', className)} {...props} />
-)
-
-export type ConversationEmptyStateProps = ComponentProps<'div'> & {
-  title?: string
-  description?: string
-  icon?: React.ReactNode
-}
-
-export const ConversationEmptyState = ({
-  className,
-  title = 'No messages yet',
-  description = 'Start a conversation to see messages here',
-  icon,
-  children,
-  ...props
-}: ConversationEmptyStateProps) => (
-  <div
-    className={cn(
-      'flex size-full flex-col items-center justify-center gap-3 p-8 text-center',
-      className
-    )}
-    {...props}
-  >
-    {children ?? (
-      <>
-        {icon && <div className="text-muted-foreground">{icon}</div>}
-        <div className="space-y-1">
-          <h3 className="font-medium text-sm">{title}</h3>
-          {description && (
-            <p className="text-muted-foreground text-sm">{description}</p>
-          )}
-        </div>
-      </>
-    )}
-  </div>
-)
-
-export type ConversationScrollButtonProps = ComponentProps<typeof Button>
-
-export const ConversationScrollButton = ({
-  className,
-  ...props
-}: ConversationScrollButtonProps) => {
-  const { isAtBottom, scrollToBottom } = useStickToBottomContext()
-
-  const handleScrollToBottom = useCallback(() => {
-    scrollToBottom()
-  }, [scrollToBottom])
-
-  return (
-    !isAtBottom && (
-      <Button
-        className={cn(
-          'absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full',
-          className
-        )}
-        onClick={handleScrollToBottom}
-        size="icon"
-        type="button"
-        variant="outline"
-        {...props}
-      >
-        <ArrowDownIcon className="size-4" />
-      </Button>
-    )
-  )
-}

+ 0 - 58
libraries/ai/components/ai-elements/message.tsx

@@ -1,58 +0,0 @@
-import { cn } from '../../utils'
-import type { UIMessage } from 'ai'
-import { cva, type VariantProps } from 'class-variance-authority'
-import type { HTMLAttributes } from 'react'
-
-export type MessageProps = HTMLAttributes<HTMLDivElement> & {
-  from: UIMessage['role']
-}
-
-export const Message = ({ className, from, ...props }: MessageProps) => (
-  <div
-    className={cn(
-      'group flex w-full items-end justify-end gap-2 py-4',
-      from === 'user' ? 'is-user' : 'is-assistant flex-row-reverse justify-end',
-      className
-    )}
-    {...props}
-  />
-)
-
-const messageContentVariants = cva(
-  'is-user:dark flex flex-col gap-2 overflow-hidden rounded-lg text-sm',
-  {
-    variants: {
-      variant: {
-        contained: [
-          'max-w-[80%] px-4 py-3',
-          'group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground',
-          'group-[.is-assistant]:bg-secondary group-[.is-assistant]:text-foreground',
-        ],
-        flat: [
-          'group-[.is-user]:max-w-[80%] group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground',
-          'group-[.is-assistant]:text-foreground',
-        ],
-      },
-    },
-    defaultVariants: {
-      variant: 'contained',
-    },
-  }
-)
-
-export type MessageContentProps = HTMLAttributes<HTMLDivElement> &
-  VariantProps<typeof messageContentVariants>
-
-export const MessageContent = ({
-  children,
-  className,
-  variant,
-  ...props
-}: MessageContentProps) => (
-  <div
-    className={cn(messageContentVariants({ variant, className }))}
-    {...props}
-  >
-    {children}
-  </div>
-)

+ 0 - 1272
libraries/ai/components/ai-elements/prompt-input.tsx

@@ -1,1272 +0,0 @@
-'use client'
-
-import { Button } from '../ui/button'
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuTrigger,
-} from '../ui/dropdown-menu'
-import { HoverCard, HoverCardContent, HoverCardTrigger } from '../ui/hover-card'
-import {
-  InputGroup,
-  InputGroupAddon,
-  InputGroupButton,
-  InputGroupTextarea,
-} from '../ui/input-group'
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from '../ui/select'
-import { cn } from '../../utils'
-import type { ChatStatus, FileUIPart } from 'ai'
-import {
-  ImageIcon,
-  Loader2Icon,
-  MicIcon,
-  PaperclipIcon,
-  PlusIcon,
-  SendIcon,
-  SquareIcon,
-  XIcon,
-} from 'lucide-react'
-import { nanoid } from 'nanoid'
-import {
-  type ChangeEvent,
-  type ChangeEventHandler,
-  Children,
-  type ClipboardEventHandler,
-  type ComponentProps,
-  createContext,
-  type FormEvent,
-  type FormEventHandler,
-  Fragment,
-  type HTMLAttributes,
-  type KeyboardEventHandler,
-  type PropsWithChildren,
-  type ReactNode,
-  type RefObject,
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useRef,
-  useState,
-} from 'react'
-// ============================================================================
-// Provider Context & Types
-// ============================================================================
-
-export type AttachmentsContext = {
-  files: (FileUIPart & { id: string })[]
-  add: (files: File[] | FileList) => void
-  remove: (id: string) => void
-  clear: () => void
-  openFileDialog: () => void
-  fileInputRef: RefObject<HTMLInputElement | null>
-}
-
-export type TextInputContext = {
-  value: string
-  setInput: (v: string) => void
-  clear: () => void
-}
-
-export type PromptInputControllerProps = {
-  textInput: TextInputContext
-  attachments: AttachmentsContext
-  /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
-  __registerFileInput: (
-    ref: RefObject<HTMLInputElement | null>,
-    open: () => void
-  ) => void
-}
-
-const PromptInputController = createContext<PromptInputControllerProps | null>(
-  null
-)
-const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(
-  null
-)
-
-export const usePromptInputController = () => {
-  const ctx = useContext(PromptInputController)
-  if (!ctx) {
-    throw new Error(
-      'Wrap your component inside <PromptInputProvider> to use usePromptInputController().'
-    )
-  }
-  return ctx
-}
-
-// Optional variants (do NOT throw). Useful for dual-mode components.
-const useOptionalPromptInputController = () => useContext(PromptInputController)
-
-export const useProviderAttachments = () => {
-  const ctx = useContext(ProviderAttachmentsContext)
-  if (!ctx) {
-    throw new Error(
-      'Wrap your component inside <PromptInputProvider> to use useProviderAttachments().'
-    )
-  }
-  return ctx
-}
-
-const useOptionalProviderAttachments = () =>
-  useContext(ProviderAttachmentsContext)
-
-export type PromptInputProviderProps = PropsWithChildren<{
-  initialInput?: string
-}>
-
-/**
- * Optional global provider that lifts PromptInput state outside of PromptInput.
- * If you don't use it, PromptInput stays fully self-managed.
- */
-export function PromptInputProvider({
-  initialInput: initialTextInput = '',
-  children,
-}: PromptInputProviderProps) {
-  // ----- textInput state
-  const [textInput, setTextInput] = useState(initialTextInput)
-  const clearInput = useCallback(() => setTextInput(''), [])
-
-  // ----- attachments state (global when wrapped)
-  const [attachements, setAttachements] = useState<
-    (FileUIPart & { id: string })[]
-  >([])
-  const fileInputRef = useRef<HTMLInputElement | null>(null)
-  const openRef = useRef<() => void>(() => {})
-
-  const add = useCallback((files: File[] | FileList) => {
-    const incoming = Array.from(files)
-    if (incoming.length === 0) return
-
-    setAttachements(prev =>
-      prev.concat(
-        incoming.map(file => ({
-          id: nanoid(),
-          type: 'file' as const,
-          url: URL.createObjectURL(file),
-          mediaType: file.type,
-          filename: file.name,
-        }))
-      )
-    )
-  }, [])
-
-  const remove = useCallback((id: string) => {
-    setAttachements(prev => {
-      const found = prev.find(f => f.id === id)
-      if (found?.url) URL.revokeObjectURL(found.url)
-      return prev.filter(f => f.id !== id)
-    })
-  }, [])
-
-  const clear = useCallback(() => {
-    setAttachements(prev => {
-      for (const f of prev) if (f.url) URL.revokeObjectURL(f.url)
-      return []
-    })
-  }, [])
-
-  const openFileDialog = useCallback(() => {
-    openRef.current?.()
-  }, [])
-
-  const attachments = useMemo<AttachmentsContext>(
-    () => ({
-      files: attachements,
-      add,
-      remove,
-      clear,
-      openFileDialog,
-      fileInputRef,
-    }),
-    [attachements, add, remove, clear, openFileDialog]
-  )
-
-  const __registerFileInput = useCallback(
-    (ref: RefObject<HTMLInputElement | null>, open: () => void) => {
-      fileInputRef.current = ref.current
-      openRef.current = open
-    },
-    []
-  )
-
-  const controller = useMemo<PromptInputControllerProps>(
-    () => ({
-      textInput: {
-        value: textInput,
-        setInput: setTextInput,
-        clear: clearInput,
-      },
-      attachments,
-      __registerFileInput,
-    }),
-    [textInput, clearInput, attachments, __registerFileInput]
-  )
-
-  return (
-    <PromptInputController.Provider value={controller}>
-      <ProviderAttachmentsContext.Provider value={attachments}>
-        {children}
-      </ProviderAttachmentsContext.Provider>
-    </PromptInputController.Provider>
-  )
-}
-
-// ============================================================================
-// Component Context & Hooks
-// ============================================================================
-
-const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null)
-
-export const usePromptInputAttachments = () => {
-  // Dual-mode: prefer provider if present, otherwise use local
-  const provider = useOptionalProviderAttachments()
-  const local = useContext(LocalAttachmentsContext)
-  const context = provider ?? local
-  if (!context) {
-    throw new Error(
-      'usePromptInputAttachments must be used within a PromptInput or PromptInputProvider'
-    )
-  }
-  return context
-}
-
-export type PromptInputAttachmentProps = HTMLAttributes<HTMLDivElement> & {
-  data: FileUIPart & { id: string }
-  className?: string
-}
-
-export function PromptInputAttachment({
-  data,
-  className,
-  ...props
-}: PromptInputAttachmentProps) {
-  const attachments = usePromptInputAttachments()
-
-  const filename = data.filename || ''
-
-  const mediaType =
-    data.mediaType?.startsWith('image/') && data.url ? 'image' : 'file'
-  const isImage = mediaType === 'image'
-
-  const attachmentLabel = filename || (isImage ? 'Image' : 'Attachment')
-
-  return (
-    <PromptInputHoverCard>
-      <HoverCardTrigger asChild>
-        <div
-          className={cn(
-            'group relative flex h-8 cursor-default select-none items-center gap-1.5 rounded-md border border-border px-1.5 font-medium text-sm transition-all hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
-            className
-          )}
-          key={data.id}
-          {...props}
-        >
-          <div className="relative size-5 shrink-0">
-            <div className="absolute inset-0 flex size-5 items-center justify-center overflow-hidden rounded bg-background transition-opacity group-hover:opacity-0">
-              {isImage ? (
-                <img
-                  alt={filename || 'attachment'}
-                  className="size-5 object-cover"
-                  height={20}
-                  src={data.url}
-                  width={20}
-                />
-              ) : (
-                <div className="flex size-5 items-center justify-center text-muted-foreground">
-                  <PaperclipIcon className="size-3" />
-                </div>
-              )}
-            </div>
-            <Button
-              aria-label="Remove attachment"
-              className="absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5"
-              onClick={e => {
-                e.stopPropagation()
-                attachments.remove(data.id)
-              }}
-              type="button"
-              variant="ghost"
-            >
-              <XIcon />
-              <span className="sr-only">Remove</span>
-            </Button>
-          </div>
-
-          <span className="flex-1 truncate">{attachmentLabel}</span>
-        </div>
-      </HoverCardTrigger>
-      <PromptInputHoverCardContent className="w-auto p-2">
-        <div className="w-auto space-y-3">
-          {isImage && (
-            <div className="flex max-h-96 w-96 items-center justify-center overflow-hidden rounded-md border">
-              <img
-                alt={filename || 'attachment preview'}
-                className="max-h-full max-w-full object-contain"
-                height={384}
-                src={data.url}
-                width={448}
-              />
-            </div>
-          )}
-          <div className="flex items-center gap-2.5">
-            <div className="min-w-0 flex-1 space-y-1 px-0.5">
-              <h4 className="truncate font-semibold text-sm leading-none">
-                {filename || (isImage ? 'Image' : 'Attachment')}
-              </h4>
-              {data.mediaType && (
-                <p className="truncate font-mono text-muted-foreground text-xs">
-                  {data.mediaType}
-                </p>
-              )}
-            </div>
-          </div>
-        </div>
-      </PromptInputHoverCardContent>
-    </PromptInputHoverCard>
-  )
-}
-
-export type PromptInputAttachmentsProps = Omit<
-  HTMLAttributes<HTMLDivElement>,
-  'children'
-> & {
-  children: (attachment: FileUIPart & { id: string }) => ReactNode
-}
-
-export function PromptInputAttachments({
-  children,
-}: PromptInputAttachmentsProps) {
-  const attachments = usePromptInputAttachments()
-
-  if (!attachments.files.length) {
-    return null
-  }
-
-  return attachments.files.map(file => (
-    <Fragment key={file.id}>{children(file)}</Fragment>
-  ))
-}
-
-export type PromptInputActionAddAttachmentsProps = ComponentProps<
-  typeof DropdownMenuItem
-> & {
-  label?: string
-}
-
-export const PromptInputActionAddAttachments = ({
-  label = 'Add photos or files',
-  ...props
-}: PromptInputActionAddAttachmentsProps) => {
-  const attachments = usePromptInputAttachments()
-
-  return (
-    <DropdownMenuItem
-      {...props}
-      onSelect={e => {
-        e.preventDefault()
-        attachments.openFileDialog()
-      }}
-    >
-      <ImageIcon className="mr-2 size-4" /> {label}
-    </DropdownMenuItem>
-  )
-}
-
-export type PromptInputMessage = {
-  text?: string
-  files?: FileUIPart[]
-}
-
-export type PromptInputProps = Omit<
-  HTMLAttributes<HTMLFormElement>,
-  'onSubmit' | 'onError'
-> & {
-  accept?: string // e.g., "image/*" or leave undefined for any
-  multiple?: boolean
-  // When true, accepts drops anywhere on document. Default false (opt-in).
-  globalDrop?: boolean
-  // Render a hidden input with given name and keep it in sync for native form posts. Default false.
-  syncHiddenInput?: boolean
-  // Minimal constraints
-  maxFiles?: number
-  maxFileSize?: number // bytes
-  onError?: (err: {
-    code: 'max_files' | 'max_file_size' | 'accept'
-    message: string
-  }) => void
-  onSubmit: (
-    message: PromptInputMessage,
-    event: FormEvent<HTMLFormElement>
-  ) => void | Promise<void>
-}
-
-export const PromptInput = ({
-  className,
-  accept,
-  multiple,
-  globalDrop,
-  syncHiddenInput,
-  maxFiles,
-  maxFileSize,
-  onError,
-  onSubmit,
-  children,
-  ...props
-}: PromptInputProps) => {
-  // Try to use a provider controller if present
-  const controller = useOptionalPromptInputController()
-  const usingProvider = !!controller
-
-  // Refs
-  const inputRef = useRef<HTMLInputElement | null>(null)
-  const anchorRef = useRef<HTMLSpanElement>(null)
-  const formRef = useRef<HTMLFormElement | null>(null)
-
-  // Find nearest form to scope drag & drop
-  useEffect(() => {
-    const root = anchorRef.current?.closest('form')
-    if (root instanceof HTMLFormElement) {
-      formRef.current = root
-    }
-  }, [])
-
-  // ----- Local attachments (only used when no provider)
-  const [items, setItems] = useState<(FileUIPart & { id: string })[]>([])
-  const files = usingProvider ? controller.attachments.files : items
-
-  const openFileDialogLocal = useCallback(() => {
-    inputRef.current?.click()
-  }, [])
-
-  const matchesAccept = useCallback(
-    (f: File) => {
-      if (!accept || accept.trim() === '') {
-        return true
-      }
-      if (accept.includes('image/*')) {
-        return f.type.startsWith('image/')
-      }
-      // NOTE: keep simple; expand as needed
-      return true
-    },
-    [accept]
-  )
-
-  const addLocal = useCallback(
-    (fileList: File[] | FileList) => {
-      const incoming = Array.from(fileList)
-      const accepted = incoming.filter(f => matchesAccept(f))
-      if (incoming.length && accepted.length === 0) {
-        onError?.({
-          code: 'accept',
-          message: 'No files match the accepted types.',
-        })
-        return
-      }
-      const withinSize = (f: File) =>
-        maxFileSize ? f.size <= maxFileSize : true
-      const sized = accepted.filter(withinSize)
-      if (accepted.length > 0 && sized.length === 0) {
-        onError?.({
-          code: 'max_file_size',
-          message: 'All files exceed the maximum size.',
-        })
-        return
-      }
-
-      setItems(prev => {
-        const capacity =
-          typeof maxFiles === 'number'
-            ? Math.max(0, maxFiles - prev.length)
-            : undefined
-        const capped =
-          typeof capacity === 'number' ? sized.slice(0, capacity) : sized
-        if (typeof capacity === 'number' && sized.length > capacity) {
-          onError?.({
-            code: 'max_files',
-            message: 'Too many files. Some were not added.',
-          })
-        }
-        const next: (FileUIPart & { id: string })[] = []
-        for (const file of capped) {
-          next.push({
-            id: nanoid(),
-            type: 'file',
-            url: URL.createObjectURL(file),
-            mediaType: file.type,
-            filename: file.name,
-          })
-        }
-        return prev.concat(next)
-      })
-    },
-    [matchesAccept, maxFiles, maxFileSize, onError]
-  )
-
-  const add = usingProvider
-    ? (files: File[] | FileList) => controller.attachments.add(files)
-    : addLocal
-
-  const remove = usingProvider
-    ? (id: string) => controller.attachments.remove(id)
-    : (id: string) =>
-        setItems(prev => {
-          const found = prev.find(file => file.id === id)
-          if (found?.url) {
-            URL.revokeObjectURL(found.url)
-          }
-          return prev.filter(file => file.id !== id)
-        })
-
-  const clear = usingProvider
-    ? () => controller.attachments.clear()
-    : () =>
-        setItems(prev => {
-          for (const file of prev) {
-            if (file.url) {
-              URL.revokeObjectURL(file.url)
-            }
-          }
-          return []
-        })
-
-  const openFileDialog = usingProvider
-    ? () => controller.attachments.openFileDialog()
-    : openFileDialogLocal
-
-  // Let provider know about our hidden file input so external menus can call openFileDialog()
-  useEffect(() => {
-    if (!usingProvider) return
-    controller.__registerFileInput(inputRef, () => inputRef.current?.click())
-  }, [usingProvider, controller])
-
-  // Note: File input cannot be programmatically set for security reasons
-  // The syncHiddenInput prop is no longer functional
-  useEffect(() => {
-    if (syncHiddenInput && inputRef.current && files.length === 0) {
-      inputRef.current.value = ''
-    }
-  }, [files, syncHiddenInput])
-
-  // Attach drop handlers on nearest form and document (opt-in)
-  useEffect(() => {
-    const form = formRef.current
-    if (!form) return
-
-    const onDragOver = (e: DragEvent) => {
-      if (e.dataTransfer?.types?.includes('Files')) {
-        e.preventDefault()
-      }
-    }
-    const onDrop = (e: DragEvent) => {
-      if (e.dataTransfer?.types?.includes('Files')) {
-        e.preventDefault()
-      }
-      if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
-        add(e.dataTransfer.files)
-      }
-    }
-    form.addEventListener('dragover', onDragOver)
-    form.addEventListener('drop', onDrop)
-    return () => {
-      form.removeEventListener('dragover', onDragOver)
-      form.removeEventListener('drop', onDrop)
-    }
-  }, [add])
-
-  useEffect(() => {
-    if (!globalDrop) return
-
-    const onDragOver = (e: DragEvent) => {
-      if (e.dataTransfer?.types?.includes('Files')) {
-        e.preventDefault()
-      }
-    }
-    const onDrop = (e: DragEvent) => {
-      if (e.dataTransfer?.types?.includes('Files')) {
-        e.preventDefault()
-      }
-      if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
-        add(e.dataTransfer.files)
-      }
-    }
-    document.addEventListener('dragover', onDragOver)
-    document.addEventListener('drop', onDrop)
-    return () => {
-      document.removeEventListener('dragover', onDragOver)
-      document.removeEventListener('drop', onDrop)
-    }
-  }, [add, globalDrop])
-
-  useEffect(
-    () => () => {
-      if (!usingProvider) {
-        for (const f of files) {
-          if (f.url) URL.revokeObjectURL(f.url)
-        }
-      }
-    },
-    [usingProvider, files]
-  )
-
-  const handleChange: ChangeEventHandler<HTMLInputElement> = event => {
-    if (event.currentTarget.files) {
-      add(event.currentTarget.files)
-    }
-  }
-
-  const convertBlobUrlToDataUrl = async (url: string): Promise<string> => {
-    const response = await fetch(url)
-    const blob = await response.blob()
-    return await new Promise((resolve, reject) => {
-      const reader = new FileReader()
-      reader.onloadend = () => resolve(reader.result as string)
-      reader.onerror = reject
-      reader.readAsDataURL(blob)
-    })
-  }
-
-  const ctx = useMemo<AttachmentsContext>(
-    () => ({
-      files: files.map(item => ({ ...item, id: item.id })),
-      add,
-      remove,
-      clear,
-      openFileDialog,
-      fileInputRef: inputRef,
-    }),
-    [files, add, remove, clear, openFileDialog]
-  )
-
-  const handleSubmit: FormEventHandler<HTMLFormElement> = event => {
-    event.preventDefault()
-
-    const form = event.currentTarget
-    const text = usingProvider
-      ? controller.textInput.value
-      : (() => {
-          const formData = new FormData(form)
-          return (formData.get('message') as string) || ''
-        })()
-
-    // Reset form immediately after capturing text to avoid race condition
-    // where user input during async blob conversion would be lost
-    if (!usingProvider) {
-      form.reset()
-    }
-
-    // Convert blob URLs to data URLs asynchronously
-    Promise.all(
-      files.map(async ({ id, ...item }) => {
-        if (item.url && item.url.startsWith('blob:')) {
-          return {
-            ...item,
-            url: await convertBlobUrlToDataUrl(item.url),
-          }
-        }
-        return item
-      })
-    ).then((convertedFiles: FileUIPart[]) => {
-      try {
-        const result = onSubmit({ text, files: convertedFiles }, event)
-
-        // Handle both sync and async onSubmit
-        if (result instanceof Promise) {
-          result
-            .then(() => {
-              clear()
-              if (usingProvider) {
-                controller.textInput.clear()
-              }
-            })
-            .catch(() => {
-              // Don't clear on error - user may want to retry
-            })
-        } else {
-          // Sync function completed without throwing, clear attachments
-          clear()
-          if (usingProvider) {
-            controller.textInput.clear()
-          }
-        }
-      } catch (error) {
-        // Don't clear on error - user may want to retry
-      }
-    })
-  }
-
-  // Render with or without local provider
-  const inner = (
-    <>
-      <span aria-hidden="true" className="hidden" ref={anchorRef} />
-      <input
-        accept={accept}
-        aria-label="Upload files"
-        className="hidden"
-        multiple={multiple}
-        onChange={handleChange}
-        ref={inputRef}
-        title="Upload files"
-        type="file"
-      />
-      <form
-        className={cn('w-full', className)}
-        onSubmit={handleSubmit}
-        {...props}
-      >
-        <InputGroup>{children}</InputGroup>
-      </form>
-    </>
-  )
-
-  return usingProvider ? (
-    inner
-  ) : (
-    <LocalAttachmentsContext.Provider value={ctx}>
-      {inner}
-    </LocalAttachmentsContext.Provider>
-  )
-}
-
-export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>
-
-export const PromptInputBody = ({
-  className,
-  ...props
-}: PromptInputBodyProps) => (
-  <div className={cn('contents', className)} {...props} />
-)
-
-export type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>
-
-export const PromptInputTextarea = ({
-  onChange,
-  className,
-  placeholder = 'What would you like to do?',
-  ...props
-}: PromptInputTextareaProps) => {
-  const controller = useOptionalPromptInputController()
-  const attachments = usePromptInputAttachments()
-  const [isComposing, setIsComposing] = useState(false)
-
-  const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = e => {
-    if (e.key === 'Enter') {
-      if (isComposing || e.nativeEvent.isComposing) {
-        return
-      }
-      if (e.shiftKey) {
-        return
-      }
-      e.preventDefault()
-      e.currentTarget.form?.requestSubmit()
-    }
-
-    // Remove last attachment when Backspace is pressed and textarea is empty
-    if (
-      e.key === 'Backspace' &&
-      e.currentTarget.value === '' &&
-      attachments.files.length > 0
-    ) {
-      e.preventDefault()
-      const lastAttachment = attachments.files.at(-1)
-      if (lastAttachment) {
-        attachments.remove(lastAttachment.id)
-      }
-    }
-  }
-
-  const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = event => {
-    const items = event.clipboardData?.items
-
-    if (!items) {
-      return
-    }
-
-    const files: File[] = []
-
-    for (const item of items) {
-      if (item.kind === 'file') {
-        const file = item.getAsFile()
-        if (file) {
-          files.push(file)
-        }
-      }
-    }
-
-    if (files.length > 0) {
-      event.preventDefault()
-      attachments.add(files)
-    }
-  }
-
-  const controlledProps = controller
-    ? {
-        value: controller.textInput.value,
-        onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
-          controller.textInput.setInput(e.currentTarget.value)
-          onChange?.(e)
-        },
-      }
-    : {
-        onChange,
-      }
-
-  return (
-    <InputGroupTextarea
-      className={cn('field-sizing-content max-h-48 min-h-16', className)}
-      name="message"
-      onCompositionEnd={() => setIsComposing(false)}
-      onCompositionStart={() => setIsComposing(true)}
-      onKeyDown={handleKeyDown}
-      onPaste={handlePaste}
-      placeholder={placeholder}
-      {...props}
-      {...controlledProps}
-    />
-  )
-}
-
-export type PromptInputHeaderProps = Omit<
-  ComponentProps<typeof InputGroupAddon>,
-  'align'
->
-
-export const PromptInputHeader = ({
-  className,
-  ...props
-}: PromptInputHeaderProps) => (
-  <InputGroupAddon
-    align="block-end"
-    className={cn('order-first flex-wrap gap-1', className)}
-    {...props}
-  />
-)
-
-export type PromptInputFooterProps = Omit<
-  ComponentProps<typeof InputGroupAddon>,
-  'align'
->
-
-export const PromptInputFooter = ({
-  className,
-  ...props
-}: PromptInputFooterProps) => (
-  <InputGroupAddon
-    align="block-end"
-    className={cn('justify-between gap-1', className)}
-    {...props}
-  />
-)
-
-export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>
-
-export const PromptInputTools = ({
-  className,
-  ...props
-}: PromptInputToolsProps) => (
-  <div className={cn('flex items-center gap-1', className)} {...props} />
-)
-
-export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>
-
-export const PromptInputButton = ({
-  variant = 'ghost',
-  className,
-  size,
-  ...props
-}: PromptInputButtonProps) => {
-  const newSize =
-    size ?? (Children.count(props.children) > 1 ? 'sm' : 'icon-sm')
-
-  return (
-    <InputGroupButton
-      className={cn(className)}
-      size={newSize}
-      type="button"
-      variant={variant}
-      {...props}
-    />
-  )
-}
-
-export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>
-export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
-  <DropdownMenu {...props} />
-)
-
-export type PromptInputActionMenuTriggerProps = PromptInputButtonProps
-
-export const PromptInputActionMenuTrigger = ({
-  className,
-  children,
-  ...props
-}: PromptInputActionMenuTriggerProps) => (
-  <DropdownMenuTrigger asChild>
-    <PromptInputButton className={className} {...props}>
-      {children ?? <PlusIcon className="size-4" />}
-    </PromptInputButton>
-  </DropdownMenuTrigger>
-)
-
-export type PromptInputActionMenuContentProps = ComponentProps<
-  typeof DropdownMenuContent
->
-export const PromptInputActionMenuContent = ({
-  className,
-  ...props
-}: PromptInputActionMenuContentProps) => (
-  <DropdownMenuContent align="start" className={cn(className)} {...props} />
-)
-
-export type PromptInputActionMenuItemProps = ComponentProps<
-  typeof DropdownMenuItem
->
-export const PromptInputActionMenuItem = ({
-  className,
-  ...props
-}: PromptInputActionMenuItemProps) => (
-  <DropdownMenuItem className={cn(className)} {...props} />
-)
-
-// Note: Actions that perform side-effects (like opening a file dialog)
-// are provided in opt-in modules (e.g., prompt-input-attachments).
-
-export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
-  status?: ChatStatus
-}
-
-export const PromptInputSubmit = ({
-  className,
-  variant = 'default',
-  size = 'icon-sm',
-  status,
-  children,
-  ...props
-}: PromptInputSubmitProps) => {
-  let Icon = <SendIcon className="size-4" />
-
-  if (status === 'submitted') {
-    Icon = <Loader2Icon className="size-4 animate-spin" />
-  } else if (status === 'streaming') {
-    Icon = <SquareIcon className="size-4" />
-  } else if (status === 'error') {
-    Icon = <XIcon className="size-4" />
-  }
-
-  return (
-    <InputGroupButton
-      aria-label="Submit"
-      className={cn(className)}
-      size={size}
-      type="submit"
-      variant={variant}
-      {...props}
-    >
-      {children ?? Icon}
-    </InputGroupButton>
-  )
-}
-
-interface SpeechRecognition extends EventTarget {
-  continuous: boolean
-  interimResults: boolean
-  lang: string
-  start(): void
-  stop(): void
-  onstart: ((this: SpeechRecognition, ev: Event) => any) | null
-  onend: ((this: SpeechRecognition, ev: Event) => any) | null
-  onresult:
-    | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any)
-    | null
-  onerror:
-    | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any)
-    | null
-}
-
-interface SpeechRecognitionEvent extends Event {
-  results: SpeechRecognitionResultList
-}
-
-type SpeechRecognitionResultList = {
-  readonly length: number
-  item(index: number): SpeechRecognitionResult
-  [index: number]: SpeechRecognitionResult
-}
-
-type SpeechRecognitionResult = {
-  readonly length: number
-  item(index: number): SpeechRecognitionAlternative
-  [index: number]: SpeechRecognitionAlternative
-  isFinal: boolean
-}
-
-type SpeechRecognitionAlternative = {
-  transcript: string
-  confidence: number
-}
-
-interface SpeechRecognitionErrorEvent extends Event {
-  error: string
-}
-
-declare global {
-  // eslint-disable-next-line no-unused-vars
-  interface Window {
-    SpeechRecognition: {
-      new (): SpeechRecognition
-    }
-    webkitSpeechRecognition: {
-      new (): SpeechRecognition
-    }
-  }
-}
-
-export type PromptInputSpeechButtonProps = ComponentProps<
-  typeof PromptInputButton
-> & {
-  textareaRef?: RefObject<HTMLTextAreaElement | null>
-  onTranscriptionChange?: (text: string) => void
-}
-
-export const PromptInputSpeechButton = ({
-  className,
-  textareaRef,
-  onTranscriptionChange,
-  ...props
-}: PromptInputSpeechButtonProps) => {
-  const [isListening, setIsListening] = useState(false)
-  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null)
-  const recognitionRef = useRef<SpeechRecognition | null>(null)
-
-  useEffect(() => {
-    if (
-      typeof window !== 'undefined' &&
-      ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)
-    ) {
-      const SpeechRecognition =
-        window.SpeechRecognition || window.webkitSpeechRecognition
-      const speechRecognition = new SpeechRecognition()
-
-      speechRecognition.continuous = true
-      speechRecognition.interimResults = true
-      speechRecognition.lang = 'en-US'
-
-      speechRecognition.onstart = () => {
-        setIsListening(true)
-      }
-
-      speechRecognition.onend = () => {
-        setIsListening(false)
-      }
-
-      speechRecognition.onresult = event => {
-        let finalTranscript = ''
-
-        const results = Array.from(event.results)
-
-        for (const result of results) {
-          if (result.isFinal) {
-            finalTranscript += result[0]?.transcript ?? ''
-          }
-        }
-
-        if (finalTranscript && textareaRef?.current) {
-          const textarea = textareaRef.current
-          const currentValue = textarea.value
-          const newValue =
-            currentValue + (currentValue ? ' ' : '') + finalTranscript
-
-          textarea.value = newValue
-          textarea.dispatchEvent(new Event('input', { bubbles: true }))
-          onTranscriptionChange?.(newValue)
-        }
-      }
-
-      speechRecognition.onerror = event => {
-        console.error('Speech recognition error:', event.error)
-        setIsListening(false)
-      }
-
-      recognitionRef.current = speechRecognition
-      setRecognition(speechRecognition)
-    }
-
-    return () => {
-      if (recognitionRef.current) {
-        recognitionRef.current.stop()
-      }
-    }
-  }, [textareaRef, onTranscriptionChange])
-
-  const toggleListening = useCallback(() => {
-    if (!recognition) {
-      return
-    }
-
-    if (isListening) {
-      recognition.stop()
-    } else {
-      recognition.start()
-    }
-  }, [recognition, isListening])
-
-  return (
-    <PromptInputButton
-      className={cn(
-        'relative transition-all duration-200',
-        isListening && 'animate-pulse bg-accent text-accent-foreground',
-        className
-      )}
-      disabled={!recognition}
-      onClick={toggleListening}
-      {...props}
-    >
-      <MicIcon className="size-4" />
-    </PromptInputButton>
-  )
-}
-
-export type PromptInputModelSelectProps = ComponentProps<typeof Select>
-
-export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
-  <Select {...props} />
-)
-
-export type PromptInputModelSelectTriggerProps = ComponentProps<
-  typeof SelectTrigger
->
-
-export const PromptInputModelSelectTrigger = ({
-  className,
-  ...props
-}: PromptInputModelSelectTriggerProps) => (
-  <SelectTrigger
-    className={cn(
-      'border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors',
-      'hover:bg-accent hover:text-foreground [&[aria-expanded="true"]]:bg-accent [&[aria-expanded="true"]]:text-foreground',
-      className
-    )}
-    {...props}
-  />
-)
-
-export type PromptInputModelSelectContentProps = ComponentProps<
-  typeof SelectContent
->
-
-export const PromptInputModelSelectContent = ({
-  className,
-  ...props
-}: PromptInputModelSelectContentProps) => (
-  <SelectContent className={cn(className)} {...props} />
-)
-
-export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>
-
-export const PromptInputModelSelectItem = ({
-  className,
-  ...props
-}: PromptInputModelSelectItemProps) => (
-  <SelectItem className={cn(className)} {...props} />
-)
-
-export type PromptInputModelSelectValueProps = ComponentProps<
-  typeof SelectValue
->
-
-export const PromptInputModelSelectValue = ({
-  className,
-  ...props
-}: PromptInputModelSelectValueProps) => (
-  <SelectValue className={cn(className)} {...props} />
-)
-
-export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>
-
-export const PromptInputHoverCard = ({
-  openDelay = 0,
-  closeDelay = 0,
-  ...props
-}: PromptInputHoverCardProps) => (
-  <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
-)
-
-export type PromptInputHoverCardTriggerProps = ComponentProps<
-  typeof HoverCardTrigger
->
-
-export const PromptInputHoverCardTrigger = (
-  props: PromptInputHoverCardTriggerProps
-) => <HoverCardTrigger {...props} />
-
-export type PromptInputHoverCardContentProps = ComponentProps<
-  typeof HoverCardContent
->
-
-export const PromptInputHoverCardContent = ({
-  align = 'start',
-  ...props
-}: PromptInputHoverCardContentProps) => (
-  <HoverCardContent align={align} {...props} />
-)
-
-export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>
-
-export const PromptInputTabsList = ({
-  className,
-  ...props
-}: PromptInputTabsListProps) => <div className={cn(className)} {...props} />
-
-export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>
-
-export const PromptInputTab = ({
-  className,
-  ...props
-}: PromptInputTabProps) => <div className={cn(className)} {...props} />
-
-export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>
-
-export const PromptInputTabLabel = ({
-  className,
-  ...props
-}: PromptInputTabLabelProps) => (
-  <h3
-    className={cn(
-      'mb-2 px-3 font-medium text-muted-foreground text-xs',
-      className
-    )}
-    {...props}
-  />
-)
-
-export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>
-
-export const PromptInputTabBody = ({
-  className,
-  ...props
-}: PromptInputTabBodyProps) => (
-  <div className={cn('space-y-1', className)} {...props} />
-)
-
-export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>
-
-export const PromptInputTabItem = ({
-  className,
-  ...props
-}: PromptInputTabItemProps) => (
-  <div
-    className={cn(
-      'flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent',
-      className
-    )}
-    {...props}
-  />
-)

+ 0 - 178
libraries/ai/components/ai-elements/reasoning.tsx

@@ -1,178 +0,0 @@
-'use client'
-
-import { useControllableState } from '@radix-ui/react-use-controllable-state'
-import {
-  Collapsible,
-  CollapsibleContent,
-  CollapsibleTrigger,
-} from '../ui/collapsible'
-import { cn } from '../../utils'
-import { BrainIcon, ChevronDownIcon } from 'lucide-react'
-import type { ComponentProps } from 'react'
-import { createContext, memo, useContext, useEffect, useState } from 'react'
-import { Response } from './response'
-import { Shimmer } from './shimmer'
-
-type ReasoningContextValue = {
-  isStreaming: boolean
-  isOpen: boolean
-  setIsOpen: (open: boolean) => void
-  duration: number
-}
-
-const ReasoningContext = createContext<ReasoningContextValue | null>(null)
-
-const useReasoning = () => {
-  const context = useContext(ReasoningContext)
-  if (!context) {
-    throw new Error('Reasoning components must be used within Reasoning')
-  }
-  return context
-}
-
-export type ReasoningProps = ComponentProps<typeof Collapsible> & {
-  isStreaming?: boolean
-  open?: boolean
-  defaultOpen?: boolean
-  onOpenChange?: (open: boolean) => void
-  duration?: number
-}
-
-const AUTO_CLOSE_DELAY = 1000
-const MS_IN_S = 1000
-
-export const Reasoning = memo(
-  ({
-    className,
-    isStreaming = false,
-    open,
-    defaultOpen = true,
-    onOpenChange,
-    duration: durationProp,
-    children,
-    ...props
-  }: ReasoningProps) => {
-    const [isOpen, setIsOpen] = useControllableState({
-      prop: open,
-      defaultProp: defaultOpen,
-      onChange: onOpenChange,
-    })
-    const [duration, setDuration] = useControllableState({
-      prop: durationProp,
-      defaultProp: 0,
-    })
-
-    const [hasAutoClosed, setHasAutoClosed] = useState(false)
-    const [startTime, setStartTime] = useState<number | null>(null)
-
-    // Track duration when streaming starts and ends
-    useEffect(() => {
-      if (isStreaming) {
-        if (startTime === null) {
-          setStartTime(Date.now())
-        }
-      } else if (startTime !== null) {
-        setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
-        setStartTime(null)
-      }
-    }, [isStreaming, startTime, setDuration])
-
-    // Auto-open when streaming starts, auto-close when streaming ends (once only)
-    useEffect(() => {
-      if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
-        // Add a small delay before closing to allow user to see the content
-        const timer = setTimeout(() => {
-          setIsOpen(false)
-          setHasAutoClosed(true)
-        }, AUTO_CLOSE_DELAY)
-
-        return () => clearTimeout(timer)
-      }
-    }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed])
-
-    const handleOpenChange = (newOpen: boolean) => {
-      setIsOpen(newOpen)
-    }
-
-    return (
-      <ReasoningContext.Provider
-        value={{ isStreaming, isOpen, setIsOpen, duration }}
-      >
-        <Collapsible
-          className={cn('not-prose mb-4', className)}
-          onOpenChange={handleOpenChange}
-          open={isOpen}
-          {...props}
-        >
-          {children}
-        </Collapsible>
-      </ReasoningContext.Provider>
-    )
-  }
-)
-
-export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>
-
-const getThinkingMessage = (isStreaming: boolean, duration?: number) => {
-  if (isStreaming || duration === 0) {
-    return <Shimmer duration={1}>Thinking...</Shimmer>
-  }
-  if (duration === undefined) {
-    return <p>Thought for a few seconds</p>
-  }
-  return <p>Thought for {duration} seconds</p>
-}
-
-export const ReasoningTrigger = memo(
-  ({ className, children, ...props }: ReasoningTriggerProps) => {
-    const { isStreaming, isOpen, duration } = useReasoning()
-
-    return (
-      <CollapsibleTrigger
-        className={cn(
-          'flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground',
-          className
-        )}
-        {...props}
-      >
-        {children ?? (
-          <>
-            <BrainIcon className="size-4" />
-            {getThinkingMessage(isStreaming, duration)}
-            <ChevronDownIcon
-              className={cn(
-                'size-4 transition-transform',
-                isOpen ? 'rotate-180' : 'rotate-0'
-              )}
-            />
-          </>
-        )}
-      </CollapsibleTrigger>
-    )
-  }
-)
-
-export type ReasoningContentProps = ComponentProps<
-  typeof CollapsibleContent
-> & {
-  children: string
-}
-
-export const ReasoningContent = memo(
-  ({ className, children, ...props }: ReasoningContentProps) => (
-    <CollapsibleContent
-      className={cn(
-        'mt-4 text-sm',
-        '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',
-        className
-      )}
-      {...props}
-    >
-      <Response className="grid gap-2">{children}</Response>
-    </CollapsibleContent>
-  )
-)
-
-Reasoning.displayName = 'Reasoning'
-ReasoningTrigger.displayName = 'ReasoningTrigger'
-ReasoningContent.displayName = 'ReasoningContent'

+ 0 - 22
libraries/ai/components/ai-elements/response.tsx

@@ -1,22 +0,0 @@
-'use client'
-
-import { cn } from '../../utils'
-import { type ComponentProps, memo } from 'react'
-import { Streamdown } from 'streamdown'
-
-type ResponseProps = ComponentProps<typeof Streamdown>
-
-export const Response = memo(
-  ({ className, ...props }: ResponseProps) => (
-    <Streamdown
-      className={cn(
-        'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
-        className
-      )}
-      {...props}
-    />
-  ),
-  (prevProps, nextProps) => prevProps.children === nextProps.children
-)
-
-Response.displayName = 'Response'

+ 0 - 64
libraries/ai/components/ai-elements/shimmer.tsx

@@ -1,64 +0,0 @@
-'use client'
-
-import { cn } from '../../utils'
-import { motion } from 'motion/react'
-import {
-  type CSSProperties,
-  type ElementType,
-  type JSX,
-  memo,
-  useMemo,
-} from 'react'
-
-export type TextShimmerProps = {
-  children: string
-  as?: ElementType
-  className?: string
-  duration?: number
-  spread?: number
-}
-
-const ShimmerComponent = ({
-  children,
-  as: Component = 'p',
-  className,
-  duration = 2,
-  spread = 2,
-}: TextShimmerProps) => {
-  const MotionComponent = motion.create(
-    Component as keyof JSX.IntrinsicElements
-  )
-
-  const dynamicSpread = useMemo(
-    () => (children?.length ?? 0) * spread,
-    [children, spread]
-  )
-
-  return (
-    <MotionComponent
-      animate={{ backgroundPosition: '0% center' }}
-      className={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]',
-        className
-      )}
-      initial={{ backgroundPosition: '100% center' }}
-      style={
-        {
-          '--spread': `${dynamicSpread}px`,
-          backgroundImage:
-            'var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))',
-        } as CSSProperties
-      }
-      transition={{
-        repeat: Number.POSITIVE_INFINITY,
-        duration,
-        ease: 'linear',
-      }}
-    >
-      {children}
-    </MotionComponent>
-  )
-}
-
-export const Shimmer = memo(ShimmerComponent)

+ 0 - 51
libraries/ai/components/chat-prompt-input.tsx

@@ -1,51 +0,0 @@
-import { FC } from 'react'
-import {
-  PromptInput,
-  PromptInputAttachment,
-  PromptInputAttachments,
-  PromptInputBody,
-  PromptInputFooter,
-  PromptInputHeader,
-  PromptInputMessage,
-  PromptInputSubmit,
-  PromptInputSubmitProps,
-  PromptInputTextarea,
-  PromptInputTools,
-  usePromptInputController,
-} from './ai-elements/prompt-input'
-
-export const ChatPromptInput: FC<{
-  status: PromptInputSubmitProps['status']
-  onSubmit: (message: PromptInputMessage) => Promise<boolean>
-}> = ({ status, onSubmit }) => {
-  const controller = usePromptInputController()
-
-  return (
-    <PromptInput
-      onSubmit={async message => {
-        const result = await onSubmit(message)
-        if (result) {
-          controller.textInput.clear()
-        }
-      }}
-      globalDrop
-      multiple
-      className="mt-4"
-    >
-      <PromptInputHeader>
-        <PromptInputAttachments>
-          {attachment => <PromptInputAttachment data={attachment} />}
-        </PromptInputAttachments>
-      </PromptInputHeader>
-
-      <PromptInputBody>
-        <PromptInputTextarea />
-      </PromptInputBody>
-
-      <PromptInputFooter>
-        <PromptInputTools />
-        <PromptInputSubmit status={status} />
-      </PromptInputFooter>
-    </PromptInput>
-  )
-}

+ 0 - 187
libraries/ai/components/chat.tsx

@@ -1,187 +0,0 @@
-import { FC, Fragment, ReactElement, useCallback } from 'react'
-import {
-  Message,
-  MessageContent,
-  Response,
-  Conversation,
-  ConversationContent,
-  ConversationEmptyState,
-  ConversationScrollButton,
-  Reasoning,
-  ReasoningContent,
-  ReasoningTrigger,
-  PromptInputProvider,
-  PromptInputMessage,
-  Skeleton,
-  Alert,
-} from '../'
-import { cn } from '../utils'
-import { ShadowRootPortal } from './shadow-root-portal'
-import {
-  DefaultChatTransport,
-  getToolName,
-  isFileUIPart,
-  isReasoningUIPart,
-  isTextUIPart,
-  isToolUIPart,
-  lastAssistantMessageIsCompleteWithToolCalls,
-  UIMessage,
-  UIMessagePart,
-} from 'ai'
-import { useChat, UseChatHelpers } from '@ai-sdk/react'
-import { ChatPromptInput } from './chat-prompt-input'
-
-export type ChatRunners = Record<string, (input: any) => any | Promise<any>>
-
-export type ChatRenderers = Record<
-  string,
-  (
-    part: UIMessagePart<any, any>,
-    chat: UseChatHelpers<UIMessage>
-  ) => ReactElement
->
-
-export const Chat: FC<{
-  className: string
-  chatId: string
-  api: string
-  headers: Record<string, string>
-  runners: ChatRunners
-  renderers: ChatRenderers
-}> = ({ className, chatId, api, headers, runners, renderers }) => {
-  const chat = useChat({
-    id: chatId,
-    transport: new DefaultChatTransport({ api, headers }),
-    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
-    async onToolCall({ toolCall }) {
-      if (toolCall.dynamic) {
-        return
-      }
-
-      const run = runners[toolCall.toolName]
-
-      if (run) {
-        const output = run(toolCall.input)
-
-        chat.addToolResult({
-          tool: toolCall.toolName,
-          toolCallId: toolCall.toolCallId,
-          output,
-        })
-      }
-    },
-  })
-
-  const handleSubmit = useCallback(
-    async (message: PromptInputMessage) => {
-      if (chat.status === 'streaming' || chat.status === 'submitted') {
-        await chat.stop()
-        return false
-      }
-
-      if (chat.status !== 'ready') {
-        return false
-      }
-
-      const text = message.text?.trim()
-      if (!text || text.length === 0) {
-        return false
-      }
-
-      chat.sendMessage({ text }) // TODO: files, metadata
-      return true
-    },
-    [chat]
-  )
-
-  return (
-    <ShadowRootPortal>
-      <Conversation
-        className={cn('workbench-conversation', 'h-full', className)}
-      >
-        <ConversationContent>
-          {chat.messages.length === 0 ? (
-            <ConversationEmptyState />
-          ) : (
-            chat.messages.map(message => (
-              <div key={message.id}>
-                <Message from={message.role}>
-                  <MessageContent variant="flat">
-                    {message.parts.map((part, i) => {
-                      const key = `${message.role}-${i}`
-
-                      if (isTextUIPart(part)) {
-                        return <Response key={key}>{part.text}</Response>
-                      }
-
-                      if (isReasoningUIPart(part)) {
-                        const isStreaming =
-                          chat.status === 'streaming' &&
-                          i === message.parts.length - 1 &&
-                          message.id === chat.messages.at(-1)?.id
-
-                        return (
-                          <Reasoning
-                            key={key}
-                            className="w-full"
-                            isStreaming={isStreaming}
-                            defaultOpen={false}
-                          >
-                            <ReasoningTrigger />
-                            <ReasoningContent>{part.text}</ReasoningContent>
-                          </Reasoning>
-                        )
-                      }
-
-                      if (isFileUIPart(part)) {
-                        return <div>{part.filename}</div>
-                      }
-
-                      if (isToolUIPart(part)) {
-                        switch (part.state) {
-                          case 'input-streaming':
-                            return (
-                              <Skeleton
-                                key={key}
-                                className="h-[40px] w-full rounded-full"
-                              />
-                            )
-
-                          case 'input-available':
-                          case 'output-available': {
-                            const toolName = getToolName(part)
-                            const render = renderers[toolName]
-
-                            if (!render) {
-                              // TODO: error message
-                              return null
-                            }
-
-                            return (
-                              <Fragment key={key}>
-                                {render(part, chat)}
-                              </Fragment>
-                            )
-                          }
-
-                          case 'output-error':
-                            return <Alert key={key}>{part.errorText}</Alert>
-                        }
-                      }
-
-                      return null
-                    })}
-                  </MessageContent>
-                </Message>
-              </div>
-            ))
-          )}
-        </ConversationContent>
-        <ConversationScrollButton />
-      </Conversation>
-      <PromptInputProvider>
-        <ChatPromptInput status={chat.status} onSubmit={handleSubmit} />
-      </PromptInputProvider>
-    </ShadowRootPortal>
-  )
-}

+ 0 - 32
libraries/ai/components/shadow-root-portal.tsx

@@ -1,32 +0,0 @@
-import { FC, PropsWithChildren, useEffect, useRef, useState } from 'react'
-import { createPortal } from 'react-dom'
-// @ts-expect-error this is using exportType: 'css-style-sheet'
-import tailwindCSS from '../tailwind.css'
-
-export const ShadowRootPortal: FC<PropsWithChildren> = ({ children }) => {
-  const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null)
-
-  const hostRef = useRef<HTMLDivElement | null>(null)
-
-  useEffect(() => {
-    if (!shadowRoot && hostRef.current) {
-      const shadowRoot = hostRef.current.attachShadow({ mode: 'open' })
-      shadowRoot.adoptedStyleSheets = [tailwindCSS]
-      setShadowRoot(shadowRoot)
-    }
-  }, [shadowRoot])
-
-  return (
-    <div
-      ref={hostRef}
-      style={{
-        flex: 1,
-        display: 'flex',
-        flexDirection: 'column',
-        overflow: 'auto',
-      }}
-    >
-      {shadowRoot ? createPortal(children, shadowRoot) : null}
-    </div>
-  )
-}

+ 0 - 21
libraries/ai/components/ui/LICENSE.md

@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2023 shadcn
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.

+ 0 - 1
libraries/ai/components/ui/README.md

@@ -1 +0,0 @@
-shadcn/ui components are derived from https://github.com/shadcn-ui/ui

+ 0 - 66
libraries/ai/components/ui/alert.tsx

@@ -1,66 +0,0 @@
-import * as React from 'react'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { cn } from '../../utils'
-
-const alertVariants = cva(
-  'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
-  {
-    variants: {
-      variant: {
-        default: 'bg-card text-card-foreground',
-        destructive:
-          'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
-      },
-    },
-    defaultVariants: {
-      variant: 'default',
-    },
-  }
-)
-
-function Alert({
-  className,
-  variant,
-  ...props
-}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
-  return (
-    <div
-      data-slot="alert"
-      role="alert"
-      className={cn(alertVariants({ variant }), className)}
-      {...props}
-    />
-  )
-}
-
-function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
-  return (
-    <div
-      data-slot="alert-title"
-      className={cn(
-        'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
-        className
-      )}
-      {...props}
-    />
-  )
-}
-
-function AlertDescription({
-  className,
-  ...props
-}: React.ComponentProps<'div'>) {
-  return (
-    <div
-      data-slot="alert-description"
-      className={cn(
-        'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
-        className
-      )}
-      {...props}
-    />
-  )
-}
-
-export { Alert, AlertTitle, AlertDescription }

+ 0 - 56
libraries/ai/components/ui/button.tsx

@@ -1,56 +0,0 @@
-import * as React from 'react'
-import { Slot } from '@radix-ui/react-slot'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { cn } from '../../utils'
-
-const buttonVariants = cva(
-  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
-  {
-    variants: {
-      variant: {
-        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
-        destructive:
-          'bg-destructive text-destructive-foreground hover:bg-destructive/90',
-        outline:
-          'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
-        secondary:
-          'bg-secondary text-secondary-foreground hover:bg-secondary/80',
-        ghost: 'hover:bg-accent hover:text-accent-foreground',
-        link: 'text-primary underline-offset-4 hover:underline',
-      },
-      size: {
-        default: 'h-10 px-4 py-2',
-        sm: 'h-9 rounded-md px-3',
-        lg: 'h-11 rounded-md px-8',
-        icon: 'h-10 w-10',
-      },
-    },
-    defaultVariants: {
-      variant: 'default',
-      size: 'default',
-    },
-  }
-)
-
-export interface ButtonProps
-  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
-    VariantProps<typeof buttonVariants> {
-  asChild?: boolean
-}
-
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ className, variant, size, asChild = false, ...props }, ref) => {
-    const Comp = asChild ? Slot : 'button'
-    return (
-      <Comp
-        className={cn(buttonVariants({ variant, size, className }))}
-        ref={ref}
-        {...props}
-      />
-    )
-  }
-)
-Button.displayName = 'Button'
-
-export { Button, buttonVariants }

+ 0 - 9
libraries/ai/components/ui/collapsible.tsx

@@ -1,9 +0,0 @@
-import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
-
-const Collapsible = CollapsiblePrimitive.Root
-
-const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
-
-const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
-
-export { Collapsible, CollapsibleTrigger, CollapsibleContent }

+ 0 - 200
libraries/ai/components/ui/dropdown-menu.tsx

@@ -1,200 +0,0 @@
-'use client'
-
-import * as React from 'react'
-import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
-import { Check, ChevronRight, Circle } from 'lucide-react'
-
-import { cn } from '../../utils'
-
-const DropdownMenu = DropdownMenuPrimitive.Root
-
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
-
-const DropdownMenuGroup = DropdownMenuPrimitive.Group
-
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal
-
-const DropdownMenuSub = DropdownMenuPrimitive.Sub
-
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
-
-const DropdownMenuSubTrigger = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
-    inset?: boolean
-  }
->(({ className, inset, children, ...props }, ref) => (
-  <DropdownMenuPrimitive.SubTrigger
-    ref={ref}
-    className={cn(
-      'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
-      inset && 'pl-8',
-      className
-    )}
-    {...props}
-  >
-    {children}
-    <ChevronRight className="ml-auto" />
-  </DropdownMenuPrimitive.SubTrigger>
-))
-DropdownMenuSubTrigger.displayName =
-  DropdownMenuPrimitive.SubTrigger.displayName
-
-const DropdownMenuSubContent = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
->(({ className, ...props }, ref) => (
-  <DropdownMenuPrimitive.SubContent
-    ref={ref}
-    className={cn(
-      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
-      className
-    )}
-    {...props}
-  />
-))
-DropdownMenuSubContent.displayName =
-  DropdownMenuPrimitive.SubContent.displayName
-
-const DropdownMenuContent = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
->(({ className, sideOffset = 4, ...props }, ref) => (
-  <DropdownMenuPrimitive.Portal>
-    <DropdownMenuPrimitive.Content
-      ref={ref}
-      sideOffset={sideOffset}
-      className={cn(
-        'z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]',
-        className
-      )}
-      {...props}
-    />
-  </DropdownMenuPrimitive.Portal>
-))
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
-
-const DropdownMenuItem = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
-    inset?: boolean
-  }
->(({ className, inset, ...props }, ref) => (
-  <DropdownMenuPrimitive.Item
-    ref={ref}
-    className={cn(
-      'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
-      inset && 'pl-8',
-      className
-    )}
-    {...props}
-  />
-))
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
-
-const DropdownMenuCheckboxItem = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
->(({ className, children, checked, ...props }, ref) => (
-  <DropdownMenuPrimitive.CheckboxItem
-    ref={ref}
-    className={cn(
-      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
-      className
-    )}
-    checked={checked}
-    {...props}
-  >
-    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
-      <DropdownMenuPrimitive.ItemIndicator>
-        <Check className="h-4 w-4" />
-      </DropdownMenuPrimitive.ItemIndicator>
-    </span>
-    {children}
-  </DropdownMenuPrimitive.CheckboxItem>
-))
-DropdownMenuCheckboxItem.displayName =
-  DropdownMenuPrimitive.CheckboxItem.displayName
-
-const DropdownMenuRadioItem = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
->(({ className, children, ...props }, ref) => (
-  <DropdownMenuPrimitive.RadioItem
-    ref={ref}
-    className={cn(
-      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
-      className
-    )}
-    {...props}
-  >
-    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
-      <DropdownMenuPrimitive.ItemIndicator>
-        <Circle className="h-2 w-2 fill-current" />
-      </DropdownMenuPrimitive.ItemIndicator>
-    </span>
-    {children}
-  </DropdownMenuPrimitive.RadioItem>
-))
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
-
-const DropdownMenuLabel = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
-    inset?: boolean
-  }
->(({ className, inset, ...props }, ref) => (
-  <DropdownMenuPrimitive.Label
-    ref={ref}
-    className={cn(
-      'px-2 py-1.5 text-sm font-semibold',
-      inset && 'pl-8',
-      className
-    )}
-    {...props}
-  />
-))
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
-
-const DropdownMenuSeparator = React.forwardRef<
-  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
-  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
->(({ className, ...props }, ref) => (
-  <DropdownMenuPrimitive.Separator
-    ref={ref}
-    className={cn('-mx-1 my-1 h-px bg-muted', className)}
-    {...props}
-  />
-))
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
-
-const DropdownMenuShortcut = ({
-  className,
-  ...props
-}: React.HTMLAttributes<HTMLSpanElement>) => {
-  return (
-    <span
-      className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
-      {...props}
-    />
-  )
-}
-DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
-
-export {
-  DropdownMenu,
-  DropdownMenuTrigger,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuCheckboxItem,
-  DropdownMenuRadioItem,
-  DropdownMenuLabel,
-  DropdownMenuSeparator,
-  DropdownMenuShortcut,
-  DropdownMenuGroup,
-  DropdownMenuPortal,
-  DropdownMenuSub,
-  DropdownMenuSubContent,
-  DropdownMenuSubTrigger,
-  DropdownMenuRadioGroup,
-}

+ 0 - 27
libraries/ai/components/ui/hover-card.tsx

@@ -1,27 +0,0 @@
-import * as React from 'react'
-import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
-
-import { cn } from '../../utils'
-
-const HoverCard = HoverCardPrimitive.Root
-
-const HoverCardTrigger = HoverCardPrimitive.Trigger
-
-const HoverCardContent = React.forwardRef<
-  React.ElementRef<typeof HoverCardPrimitive.Content>,
-  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
-  <HoverCardPrimitive.Content
-    ref={ref}
-    align={align}
-    sideOffset={sideOffset}
-    className={cn(
-      'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]',
-      className
-    )}
-    {...props}
-  />
-))
-HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
-
-export { HoverCard, HoverCardTrigger, HoverCardContent }

+ 0 - 168
libraries/ai/components/ui/input-group.tsx

@@ -1,168 +0,0 @@
-import * as React from 'react'
-import { cva, type VariantProps } from 'class-variance-authority'
-
-import { cn } from '../../utils'
-import { Button } from '../ui/button'
-import { Input } from '../ui/input'
-import { Textarea } from '../ui/textarea'
-
-function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
-  return (
-    <div
-      data-slot="input-group"
-      role="group"
-      className={cn(
-        'group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]',
-        'h-9 has-[>textarea]:h-auto',
-
-        // Variants based on alignment.
-        'has-[>[data-align=inline-start]]:[&>input]:pl-2',
-        'has-[>[data-align=inline-end]]:[&>input]:pr-2',
-        'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
-        'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
-
-        // Focus state.
-        'has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1',
-
-        // Error state.
-        'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
-
-        className
-      )}
-      {...props}
-    />
-  )
-}
-
-const inputGroupAddonVariants = cva(
-  "text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
-  {
-    variants: {
-      align: {
-        'inline-start':
-          'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
-        'inline-end':
-          'order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]',
-        'block-start':
-          '[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5',
-        'block-end':
-          '[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5',
-      },
-    },
-    defaultVariants: {
-      align: 'inline-start',
-    },
-  }
-)
-
-function InputGroupAddon({
-  className,
-  align = 'inline-start',
-  ...props
-}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
-  return (
-    <div
-      role="group"
-      data-slot="input-group-addon"
-      data-align={align}
-      className={cn(inputGroupAddonVariants({ align }), className)}
-      onClick={e => {
-        if ((e.target as HTMLElement).closest('button')) {
-          return
-        }
-        e.currentTarget.parentElement?.querySelector('input')?.focus()
-      }}
-      {...props}
-    />
-  )
-}
-
-const inputGroupButtonVariants = cva(
-  'flex items-center gap-2 text-sm shadow-none',
-  {
-    variants: {
-      size: {
-        xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
-        sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
-        'icon-xs':
-          'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
-        'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
-      },
-    },
-    defaultVariants: {
-      size: 'xs',
-    },
-  }
-)
-
-function InputGroupButton({
-  className,
-  type = 'button',
-  variant = 'ghost',
-  size = 'xs',
-  ...props
-}: Omit<React.ComponentProps<typeof Button>, 'size'> &
-  VariantProps<typeof inputGroupButtonVariants>) {
-  return (
-    <Button
-      type={type}
-      data-size={size}
-      variant={variant}
-      className={cn(inputGroupButtonVariants({ size }), className)}
-      {...props}
-    />
-  )
-}
-
-function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
-  return (
-    <span
-      className={cn(
-        "text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
-        className
-      )}
-      {...props}
-    />
-  )
-}
-
-function InputGroupInput({
-  className,
-  ...props
-}: React.ComponentProps<'input'>) {
-  return (
-    <Input
-      data-slot="input-group-control"
-      className={cn(
-        'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
-        className
-      )}
-      {...props}
-    />
-  )
-}
-
-function InputGroupTextarea({
-  className,
-  ...props
-}: React.ComponentProps<'textarea'>) {
-  return (
-    <Textarea
-      data-slot="input-group-control"
-      className={cn(
-        'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
-        className
-      )}
-      {...props}
-    />
-  )
-}
-
-export {
-  InputGroup,
-  InputGroupAddon,
-  InputGroupButton,
-  InputGroupText,
-  InputGroupInput,
-  InputGroupTextarea,
-}

+ 0 - 22
libraries/ai/components/ui/input.tsx

@@ -1,22 +0,0 @@
-import * as React from 'react'
-
-import { cn } from '../../utils'
-
-const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
-  ({ className, type, ...props }, ref) => {
-    return (
-      <input
-        type={type}
-        className={cn(
-          'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
-          className
-        )}
-        ref={ref}
-        {...props}
-      />
-    )
-  }
-)
-Input.displayName = 'Input'
-
-export { Input }

+ 0 - 158
libraries/ai/components/ui/select.tsx

@@ -1,158 +0,0 @@
-import * as React from 'react'
-import * as SelectPrimitive from '@radix-ui/react-select'
-import { Check, ChevronDown, ChevronUp } from 'lucide-react'
-
-import { cn } from '../../utils'
-
-const Select = SelectPrimitive.Root
-
-const SelectGroup = SelectPrimitive.Group
-
-const SelectValue = SelectPrimitive.Value
-
-const SelectTrigger = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.Trigger>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
->(({ className, children, ...props }, ref) => (
-  <SelectPrimitive.Trigger
-    ref={ref}
-    className={cn(
-      'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
-      className
-    )}
-    {...props}
-  >
-    {children}
-    <SelectPrimitive.Icon asChild>
-      <ChevronDown className="h-4 w-4 opacity-50" />
-    </SelectPrimitive.Icon>
-  </SelectPrimitive.Trigger>
-))
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
-
-const SelectScrollUpButton = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
->(({ className, ...props }, ref) => (
-  <SelectPrimitive.ScrollUpButton
-    ref={ref}
-    className={cn(
-      'flex cursor-default items-center justify-center py-1',
-      className
-    )}
-    {...props}
-  >
-    <ChevronUp className="h-4 w-4" />
-  </SelectPrimitive.ScrollUpButton>
-))
-SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
-
-const SelectScrollDownButton = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
->(({ className, ...props }, ref) => (
-  <SelectPrimitive.ScrollDownButton
-    ref={ref}
-    className={cn(
-      'flex cursor-default items-center justify-center py-1',
-      className
-    )}
-    {...props}
-  >
-    <ChevronDown className="h-4 w-4" />
-  </SelectPrimitive.ScrollDownButton>
-))
-SelectScrollDownButton.displayName =
-  SelectPrimitive.ScrollDownButton.displayName
-
-const SelectContent = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.Content>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
->(({ className, children, position = 'popper', ...props }, ref) => (
-  <SelectPrimitive.Portal>
-    <SelectPrimitive.Content
-      ref={ref}
-      className={cn(
-        'relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]',
-        position === 'popper' &&
-          'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
-        className
-      )}
-      position={position}
-      {...props}
-    >
-      <SelectScrollUpButton />
-      <SelectPrimitive.Viewport
-        className={cn(
-          'p-1',
-          position === 'popper' &&
-            'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
-        )}
-      >
-        {children}
-      </SelectPrimitive.Viewport>
-      <SelectScrollDownButton />
-    </SelectPrimitive.Content>
-  </SelectPrimitive.Portal>
-))
-SelectContent.displayName = SelectPrimitive.Content.displayName
-
-const SelectLabel = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.Label>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
->(({ className, ...props }, ref) => (
-  <SelectPrimitive.Label
-    ref={ref}
-    className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}
-    {...props}
-  />
-))
-SelectLabel.displayName = SelectPrimitive.Label.displayName
-
-const SelectItem = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.Item>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
->(({ className, children, ...props }, ref) => (
-  <SelectPrimitive.Item
-    ref={ref}
-    className={cn(
-      'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
-      className
-    )}
-    {...props}
-  >
-    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
-      <SelectPrimitive.ItemIndicator>
-        <Check className="h-4 w-4" />
-      </SelectPrimitive.ItemIndicator>
-    </span>
-
-    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
-  </SelectPrimitive.Item>
-))
-SelectItem.displayName = SelectPrimitive.Item.displayName
-
-const SelectSeparator = React.forwardRef<
-  React.ElementRef<typeof SelectPrimitive.Separator>,
-  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
->(({ className, ...props }, ref) => (
-  <SelectPrimitive.Separator
-    ref={ref}
-    className={cn('-mx-1 my-1 h-px bg-muted', className)}
-    {...props}
-  />
-))
-SelectSeparator.displayName = SelectPrimitive.Separator.displayName
-
-export {
-  Select,
-  SelectGroup,
-  SelectValue,
-  SelectTrigger,
-  SelectContent,
-  SelectLabel,
-  SelectItem,
-  SelectSeparator,
-  SelectScrollUpButton,
-  SelectScrollDownButton,
-}

+ 0 - 14
libraries/ai/components/ui/skeleton.tsx

@@ -1,14 +0,0 @@
-import React from 'react'
-import { cn } from '../../utils'
-
-function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
-  return (
-    <div
-      data-slot="skeleton"
-      className={cn('bg-accent animate-pulse rounded-md', className)}
-      {...props}
-    />
-  )
-}
-
-export { Skeleton }

+ 0 - 22
libraries/ai/components/ui/textarea.tsx

@@ -1,22 +0,0 @@
-import * as React from 'react'
-
-import { cn } from '../../utils'
-
-const Textarea = React.forwardRef<
-  HTMLTextAreaElement,
-  React.ComponentProps<'textarea'>
->(({ className, ...props }, ref) => {
-  return (
-    <textarea
-      className={cn(
-        'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
-        className
-      )}
-      ref={ref}
-      {...props}
-    />
-  )
-})
-Textarea.displayName = 'Textarea'
-
-export { Textarea }

+ 0 - 10
libraries/ai/index.ts

@@ -1,10 +0,0 @@
-export * from './components/ai-elements/conversation'
-export * from './components/ai-elements/message'
-export * from './components/ai-elements/prompt-input'
-export * from './components/ai-elements/reasoning'
-export * from './components/ai-elements/response'
-export * from './components/ai-elements/shimmer'
-export * from './components/ui/alert'
-export * from './components/ui/button'
-export * from './components/ui/skeleton'
-export * from './components/chat'

+ 0 - 37
libraries/ai/package.json

@@ -1,37 +0,0 @@
-{
-  "name": "@overleaf/ai",
-  "private": true,
-  "version": "1.0.0",
-  "description": "AI UI",
-  "main": "index.ts",
-  "license": "AGPL-3.0-only",
-  "dependencies": {
-    "@ai-sdk/react": "^3.0.0-beta.93",
-    "@radix-ui/react-avatar": "^1.1.10",
-    "@radix-ui/react-collapsible": "^1.1.12",
-    "@radix-ui/react-dropdown-menu": "^2.1.16",
-    "@radix-ui/react-hover-card": "^1.1.15",
-    "@radix-ui/react-select": "^2.2.6",
-    "@radix-ui/react-slot": "^1.2.3",
-    "@radix-ui/react-use-controllable-state": "^1.2.2",
-    "@tailwindcss/container-queries": "^0.1.1",
-    "@valibot/to-json-schema": "^1.3.0",
-    "ai": "^6.0.0-beta.84",
-    "class-variance-authority": "^0.7.1",
-    "clsx": "^2.1.1",
-    "effect": "^3.19.0",
-    "lucide-react": "^0.548.0",
-    "motion": "^12.23.24",
-    "nanoid": "^5.0.0",
-    "react": "^18.3.1",
-    "react-dom": "^18.3.1",
-    "streamdown": "^1.4.0",
-    "tailwind-merge": "^3.3.1",
-    "use-stick-to-bottom": "^1.1.1"
-  },
-  "peerDependencies": {
-    "react": "^18.3.1",
-    "react-dom": "^18.3.1"
-  },
-  "type": "commonjs"
-}

+ 0 - 9
libraries/ai/postcss.config.js

@@ -1,9 +0,0 @@
-const path = require('node:path')
-module.exports = {
-  plugins: {
-    tailwindcss: {
-      config: path.resolve(__dirname, 'tailwind.config.js'),
-    },
-    autoprefixer: {},
-  },
-}

+ 0 - 85
libraries/ai/tailwind.config.js

@@ -1,85 +0,0 @@
-const { dirname } = require('node:path')
-const containerQueries = require('@tailwindcss/container-queries')
-
-module.exports = {
-  darkMode: ['class'],
-  content: [
-    dirname(require.resolve('@overleaf/ai')) + '/tailwind.css',
-    dirname(require.resolve('@overleaf/ai')) + '/components/**/*.tsx',
-    './modules/workbench/**/*.tsx',
-    require.resolve('streamdown'),
-  ],
-  theme: {
-    extend: {
-      colors: {
-        background: 'var(--background)',
-        foreground: 'var(--foreground)',
-        card: {
-          DEFAULT: 'var(--card)',
-          foreground: 'var(--card-foreground)',
-        },
-        primary: {
-          DEFAULT: 'var(--primary)',
-          foreground: 'var(--primary-foreground)',
-        },
-        secondary: {
-          DEFAULT: 'var(--secondary)',
-          foreground: 'var(--secondary-foreground)',
-        },
-        destructive: {
-          DEFAULT: 'var(--destructive)',
-          foreground: 'var(--destructive-foreground)',
-        },
-        muted: {
-          DEFAULT: 'var(--muted)',
-          foreground: 'var(--muted-foreground)',
-        },
-        accent: {
-          DEFAULT: 'var(--accent)',
-          foreground: 'var(--accent-foreground)',
-        },
-        popover: {
-          DEFAULT: 'var(--popover)',
-          foreground: 'var(--popover-foreground)',
-        },
-        border: 'var(--border)',
-        input: 'var(--input)',
-        ring: 'var(--ring)',
-        'chart-1': 'var(--chart-1)',
-        'chart-2': 'var(--chart-2)',
-        'chart-3': 'var(--chart-3)',
-        'chart-4': 'var(--chart-4)',
-        'chart-5': 'var(--chart-5)',
-        sidebar: 'var(--sidebar)',
-        'sidebar-foreground': 'var(--sidebar-foreground)',
-        'sidebar-primary': 'var(--sidebar-primary)',
-        'sidebar-primary-foreground': 'var(--sidebar-primary-foreground)',
-        'sidebar-accent': 'var(--sidebar-accent)',
-        'sidebar-accent-foreground': 'var(--sidebar-accent-foreground)',
-        'sidebar-border': 'var(--sidebar-border)',
-        'sidebar-ring': 'var(--sidebar-ring)',
-      },
-      borderRadius: {
-        sm: 'calc(var(--radius) - 4px)',
-        md: 'calc(var(--radius) - 2px)',
-        lg: 'var(--radius)',
-        xl: 'calc(var(--radius) + 4px)',
-      },
-      keyframes: {
-        'accordion-down': {
-          from: { height: 0 },
-          to: { height: 'var(--radix-accordion-content-height)' },
-        },
-        'accordion-up': {
-          from: { height: 'var(--radix-accordion-content-height)' },
-          to: { height: 0 },
-        },
-      },
-      animation: {
-        'accordion-down': 'accordion-down 0.2s ease-out',
-        'accordion-up': 'accordion-up 0.2s ease-out',
-      },
-    },
-  },
-  plugins: [containerQueries],
-}

+ 0 - 157
libraries/ai/tailwind.css

@@ -1,157 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-@custom-variant dark (&:is(.dark *));
-
-:host {
-  --background: oklch(100% 0 0deg);
-  --foreground: oklch(14.5% 0 0deg);
-  --card: oklch(100% 0 0deg);
-  --card-foreground: oklch(14.5% 0 0deg);
-  --popover: oklch(100% 0 0deg);
-  --popover-foreground: oklch(14.5% 0 0deg);
-  --primary: oklch(20.5% 0 0deg);
-  --primary-foreground: oklch(98.5% 0 0deg);
-  --secondary: oklch(97% 0 0deg);
-  --secondary-foreground: oklch(20.5% 0 0deg);
-  --muted: oklch(97% 0 0deg);
-  --muted-foreground: oklch(55.6% 0 0deg);
-  --accent: oklch(70.8% 0 0deg);
-  --accent-foreground: oklch(20.5% 0 0deg);
-  --destructive: oklch(57.7% 0.245 27.325deg);
-  --destructive-foreground: oklch(57.7% 0.245 27.325deg);
-  --border: oklch(0.757 0.154 143.872);
-  --input: oklch(92.2% 0 0deg);
-  --ring: oklch(70.8% 0 0deg);
-  --chart-1: oklch(64.6% 0.222 41.116deg);
-  --chart-2: oklch(60% 0.118 184.704deg);
-  --chart-3: oklch(39.8% 0.07 227.392deg);
-  --chart-4: oklch(82.8% 0.189 84.429deg);
-  --chart-5: oklch(76.9% 0.188 70.08deg);
-  --radius: 0.625rem;
-  --sidebar: oklch(98.5% 0 0deg);
-  --sidebar-foreground: oklch(14.5% 0 0deg);
-  --sidebar-primary: oklch(20.5% 0 0deg);
-  --sidebar-primary-foreground: oklch(98.5% 0 0deg);
-  --sidebar-accent: oklch(97% 0 0deg);
-  --sidebar-accent-foreground: oklch(20.5% 0 0deg);
-  --sidebar-border: oklch(92.2% 0 0deg);
-  --sidebar-ring: oklch(70.8% 0 0deg);
-
-  /* @theme inline */
-  --color-background: var(--background);
-  --color-foreground: var(--foreground);
-  --color-card: var(--card);
-  --color-card-foreground: var(--card-foreground);
-  --color-popover: var(--popover);
-  --color-popover-foreground: var(--popover-foreground);
-  --color-primary: var(--primary);
-  --color-primary-foreground: var(--primary-foreground);
-  --color-secondary: var(--secondary);
-  --color-secondary-foreground: var(--secondary-foreground);
-  --color-muted: var(--muted);
-  --color-muted-foreground: var(--muted-foreground);
-  --color-accent: var(--accent);
-  --color-accent-foreground: var(--accent-foreground);
-  --color-destructive: var(--destructive);
-  --color-destructive-foreground: var(--destructive-foreground);
-  --color-border: var(--border);
-  --color-input: var(--input);
-  --color-ring: var(--ring);
-  --color-chart-1: var(--chart-1);
-  --color-chart-2: var(--chart-2);
-  --color-chart-3: var(--chart-3);
-  --color-chart-4: var(--chart-4);
-  --color-chart-5: var(--chart-5);
-  --radius-sm: calc(var(--radius) - 4px);
-  --radius-md: calc(var(--radius) - 2px);
-  --radius-lg: var(--radius);
-  --radius-xl: calc(var(--radius) + 4px);
-  --color-sidebar: var(--sidebar);
-  --color-sidebar-foreground: var(--sidebar-foreground);
-  --color-sidebar-primary: var(--sidebar-primary);
-  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
-  --color-sidebar-accent: var(--sidebar-accent);
-  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
-  --color-sidebar-border: var(--sidebar-border);
-  --color-sidebar-ring: var(--sidebar-ring);
-}
-
-.dark {
-  --background: oklch(14.5% 0 0deg);
-  --foreground: oklch(98.5% 0 0deg);
-  --card: oklch(14.5% 0 0deg);
-  --card-foreground: oklch(98.5% 0 0deg);
-  --popover: oklch(14.5% 0 0deg);
-  --popover-foreground: oklch(98.5% 0 0deg);
-  --primary: oklch(98.5% 0 0deg);
-  --primary-foreground: oklch(20.5% 0 0deg);
-  --secondary: oklch(26.9% 0 0deg);
-  --secondary-foreground: oklch(98.5% 0 0deg);
-  --muted: oklch(26.9% 0 0deg);
-  --muted-foreground: oklch(70.8% 0 0deg);
-  --accent: oklch(26.9% 0 0deg);
-  --accent-foreground: oklch(70.8% 0 0deg);
-  --destructive: oklch(39.6% 0.141 25.723deg);
-  --destructive-foreground: oklch(63.7% 0.237 25.331deg);
-  --border: oklch(26.9% 0 0deg);
-  --input: oklch(26.9% 0 0deg);
-  --ring: oklch(43.9% 0 0deg);
-  --sidebar: oklch(20.5% 0 0deg);
-  --sidebar-foreground: oklch(98.5% 0 0deg);
-  --sidebar-primary: oklch(48.8% 0.243 264.376deg);
-  --sidebar-primary-foreground: oklch(98.5% 0 0deg);
-  --sidebar-accent: oklch(26.9% 0 0deg);
-  --sidebar-accent-foreground: oklch(98.5% 0 0deg);
-  --sidebar-border: oklch(26.9% 0 0deg);
-  --sidebar-ring: oklch(43.9% 0 0deg);
-}
-
-/* @theme inline {
-  --color-background: var(--background);
-  --color-foreground: var(--foreground);
-  --color-card: var(--card);
-  --color-card-foreground: var(--card-foreground);
-  --color-popover: var(--popover);
-  --color-popover-foreground: var(--popover-foreground);
-  --color-primary: var(--primary);
-  --color-primary-foreground: var(--primary-foreground);
-  --color-secondary: var(--secondary);
-  --color-secondary-foreground: var(--secondary-foreground);
-  --color-muted: var(--muted);
-  --color-muted-foreground: var(--muted-foreground);
-  --color-accent: var(--accent);
-  --color-accent-foreground: var(--accent-foreground);
-  --color-destructive: var(--destructive);
-  --color-destructive-foreground: var(--destructive-foreground);
-  --color-border: var(--border);
-  --color-input: var(--input);
-  --color-ring: var(--ring);
-  --color-chart-1: var(--chart-1);
-  --color-chart-2: var(--chart-2);
-  --color-chart-3: var(--chart-3);
-  --color-chart-4: var(--chart-4);
-  --color-chart-5: var(--chart-5);
-  --radius-sm: calc(var(--radius) - 4px);
-  --radius-md: calc(var(--radius) - 2px);
-  --radius-lg: var(--radius);
-  --radius-xl: calc(var(--radius) + 4px);
-  --color-sidebar: var(--sidebar);
-  --color-sidebar-foreground: var(--sidebar-foreground);
-  --color-sidebar-primary: var(--sidebar-primary);
-  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
-  --color-sidebar-accent: var(--sidebar-accent);
-  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
-  --color-sidebar-border: var(--sidebar-border);
-  --color-sidebar-ring: var(--sidebar-ring);
-} */
-
-/* @layer base {
-  * {
-    @apply border-border outline-ring/50;
-  }
-
-  body {
-    @apply bg-background text-foreground;
-  }
-} */

+ 0 - 12
libraries/ai/tsconfig.json

@@ -1,12 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "ES2022",
-    "module": "ES2022",
-    "moduleResolution": "bundler",
-    "esModuleInterop": true,
-    "forceConsistentCasingInFileNames": true,
-    "strict": true,
-    "skipLibCheck": true,
-    "jsx": "preserve"
-  }
-}

+ 0 - 6
libraries/ai/utils/index.ts

@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from 'clsx'
-import { twMerge } from 'tailwind-merge'
-
-export function cn(...inputs: ClassValue[]) {
-  return twMerge(clsx(inputs))
-}

+ 2 - 0
services/web/config/settings.defaults.js

@@ -996,6 +996,8 @@ module.exports = {
     sourceEditorCompletionSources: [],
     sourceEditorSymbolPalette: [],
     sourceEditorToolbarComponents: [],
+    sourceEditorToolbarEndButtons: [],
+    rootContextProviders: [],
     mainEditorLayoutModals: [],
     langFeedbackLinkingWidgets: [],
     labsExperiments: [],

+ 22 - 2
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -1,4 +1,4 @@
-import React, { FC, PropsWithChildren } from 'react'
+import React, { ElementType, FC, PropsWithChildren } from 'react'
 import { ChatProvider } from '@/features/chat/context/chat-context'
 import { ConnectionProvider } from './connection-context'
 import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
@@ -30,6 +30,12 @@ import { UserFeaturesProvider } from '@/shared/context/user-features-context'
 import { UserSettingsProvider } from '@/shared/context/user-settings-context'
 import { IdeRedesignSwitcherProvider } from './ide-redesign-switcher-context'
 import { CommandRegistryProvider } from './command-registry-context'
+import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+
+const rootContextProviders = importOverleafModules('rootContextProviders') as {
+  import: { default: ElementType }
+  path: string
+}[]
 
 export const ReactContextRoot: FC<
   React.PropsWithChildren<{
@@ -71,6 +77,18 @@ export const ReactContextRoot: FC<
     ...providers,
   }
 
+  // Extract dynamic providers from modules
+  const dynamicProviders = rootContextProviders.map(
+    module => module.import.default
+  )
+
+  // Wrap children with all dynamic providers from outside to inside
+  const childrenWrappedWithDynamicProviders =
+    dynamicProviders.reduceRight<React.ReactElement>(
+      (acc, Provider) => <Provider>{acc}</Provider>,
+      <>{children}</>
+    )
+
   return (
     <Providers.SplitTestProvider>
       <Providers.ModalsContextProvider>
@@ -103,7 +121,9 @@ export const ReactContextRoot: FC<
                                                             <Providers.OutlineProvider>
                                                               <Providers.IdeRedesignSwitcherProvider>
                                                                 <Providers.CommandRegistryProvider>
-                                                                  {children}
+                                                                  {
+                                                                    childrenWrappedWithDynamicProviders
+                                                                  }
                                                                 </Providers.CommandRegistryProvider>
                                                               </Providers.IdeRedesignSwitcherProvider>
                                                             </Providers.OutlineProvider>

+ 9 - 0
services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx

@@ -40,6 +40,10 @@ const sourceEditorToolbarComponents = importOverleafModules(
   'sourceEditorToolbarComponents'
 ) as { import: { default: ElementType }; path: string }[]
 
+const sourceEditorToolbarEndButtons = importOverleafModules(
+  'sourceEditorToolbarEndButtons'
+) as { import: { default: ElementType }; path: string }[]
+
 export const CodeMirrorToolbar = () => {
   const view = useCodeMirrorViewContext()
   const panel = getPanel(view, createToolbarPanel)
@@ -204,6 +208,11 @@ const Toolbar = memo(function Toolbar() {
             className="ol-cm-toolbar-button-group ol-cm-toolbar-end"
             ref={handleButtons}
           >
+            {sourceEditorToolbarEndButtons.map(
+              ({ import: { default: Component }, path }) => (
+                <Component key={path} />
+              )
+            )}
             <ToggleSearchButton state={state} />
             <SwitchToPDFButton />
             <DetacherSynctexControl />

+ 0 - 1
services/web/frontend/js/shared/context/types/writefull-instance.ts

@@ -7,7 +7,6 @@ export interface WritefullEvents {
 }
 
 export interface WritefullAPI {
-  init(): void
   addEventListener<eventName extends keyof WritefullEvents>(
     name: eventName,
     callback: (detail: WritefullEvents[eventName]) => void