mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
[web] Add AI workbench to alpha users (#29417)
Co-authored-by: Alf Eaton <alf.eaton@overleaf.com> GitOrigin-RevId: 79bb329932b1e6fcc88f648bca9cc4bee215cd41
This commit is contained in:
committed by
Copybot
parent
c84cfc815a
commit
8024fe2c58
21
libraries/ai/components.json
Normal file
21
libraries/ai/components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
libraries/ai/components/ai-elements/LICENSE
Normal file
13
libraries/ai/components/ai-elements/LICENSE
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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.
|
||||||
1
libraries/ai/components/ai-elements/README.md
Normal file
1
libraries/ai/components/ai-elements/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
AI Elements components are derived from https://github.com/vercel/ai-elements
|
||||||
97
libraries/ai/components/ai-elements/conversation.tsx
Normal file
97
libraries/ai/components/ai-elements/conversation.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
58
libraries/ai/components/ai-elements/message.tsx
Normal file
58
libraries/ai/components/ai-elements/message.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
1272
libraries/ai/components/ai-elements/prompt-input.tsx
Normal file
1272
libraries/ai/components/ai-elements/prompt-input.tsx
Normal file
File diff suppressed because it is too large
Load Diff
178
libraries/ai/components/ai-elements/reasoning.tsx
Normal file
178
libraries/ai/components/ai-elements/reasoning.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
'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'
|
||||||
22
libraries/ai/components/ai-elements/response.tsx
Normal file
22
libraries/ai/components/ai-elements/response.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'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'
|
||||||
64
libraries/ai/components/ai-elements/shimmer.tsx
Normal file
64
libraries/ai/components/ai-elements/shimmer.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'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)
|
||||||
51
libraries/ai/components/chat-prompt-input.tsx
Normal file
51
libraries/ai/components/chat-prompt-input.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
187
libraries/ai/components/chat.tsx
Normal file
187
libraries/ai/components/chat.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
32
libraries/ai/components/shadow-root-portal.tsx
Normal file
32
libraries/ai/components/shadow-root-portal.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
libraries/ai/components/ui/LICENSE.md
Normal file
21
libraries/ai/components/ui/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
1
libraries/ai/components/ui/README.md
Normal file
1
libraries/ai/components/ui/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
shadcn/ui components are derived from https://github.com/shadcn-ui/ui
|
||||||
66
libraries/ai/components/ui/alert.tsx
Normal file
66
libraries/ai/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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 }
|
||||||
56
libraries/ai/components/ui/button.tsx
Normal file
56
libraries/ai/components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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 }
|
||||||
9
libraries/ai/components/ui/collapsible.tsx
Normal file
9
libraries/ai/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
|
const Collapsible = CollapsiblePrimitive.Root
|
||||||
|
|
||||||
|
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
|
||||||
|
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
200
libraries/ai/components/ui/dropdown-menu.tsx
Normal file
200
libraries/ai/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
'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,
|
||||||
|
}
|
||||||
27
libraries/ai/components/ui/hover-card.tsx
Normal file
27
libraries/ai/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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 }
|
||||||
168
libraries/ai/components/ui/input-group.tsx
Normal file
168
libraries/ai/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
22
libraries/ai/components/ui/input.tsx
Normal file
22
libraries/ai/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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 }
|
||||||
158
libraries/ai/components/ui/select.tsx
Normal file
158
libraries/ai/components/ui/select.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
14
libraries/ai/components/ui/skeleton.tsx
Normal file
14
libraries/ai/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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 }
|
||||||
22
libraries/ai/components/ui/textarea.tsx
Normal file
22
libraries/ai/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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 }
|
||||||
10
libraries/ai/index.ts
Normal file
10
libraries/ai/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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'
|
||||||
37
libraries/ai/package.json
Normal file
37
libraries/ai/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"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": "^4.0.2",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
9
libraries/ai/postcss.config.js
Normal file
9
libraries/ai/postcss.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const path = require('node:path')
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {
|
||||||
|
config: path.resolve(__dirname, 'tailwind.config.js'),
|
||||||
|
},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
85
libraries/ai/tailwind.config.js
Normal file
85
libraries/ai/tailwind.config.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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],
|
||||||
|
}
|
||||||
157
libraries/ai/tailwind.css
Normal file
157
libraries/ai/tailwind.css
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
} */
|
||||||
12
libraries/ai/tsconfig.json
Normal file
12
libraries/ai/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "preserve"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
libraries/ai/utils/index.ts
Normal file
6
libraries/ai/utils/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
5427
package-lock.json
generated
5427
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
.eslint*
|
.eslint*
|
||||||
.prettier*
|
.prettier*
|
||||||
libraries/access-token-encryptor/**
|
libraries/access-token-encryptor/**
|
||||||
|
libraries/ai/**
|
||||||
libraries/eslint-plugin/**
|
libraries/eslint-plugin/**
|
||||||
libraries/fetch-utils/**
|
libraries/fetch-utils/**
|
||||||
libraries/logger/**
|
libraries/logger/**
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ FROM base AS deps-prod
|
|||||||
|
|
||||||
COPY package.json package-lock.json /overleaf/
|
COPY package.json package-lock.json /overleaf/
|
||||||
COPY libraries/access-token-encryptor/package.json /overleaf/libraries/access-token-encryptor/package.json
|
COPY libraries/access-token-encryptor/package.json /overleaf/libraries/access-token-encryptor/package.json
|
||||||
|
COPY libraries/ai/package.json /overleaf/libraries/ai/package.json
|
||||||
COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
|
COPY libraries/eslint-plugin/package.json /overleaf/libraries/eslint-plugin/package.json
|
||||||
COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
|
COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
|
||||||
COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
|
COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
|
||||||
@@ -58,6 +59,7 @@ FROM deps AS dev
|
|||||||
ARG SENTRY_RELEASE
|
ARG SENTRY_RELEASE
|
||||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||||
COPY libraries/access-token-encryptor/ /overleaf/libraries/access-token-encryptor/
|
COPY libraries/access-token-encryptor/ /overleaf/libraries/access-token-encryptor/
|
||||||
|
COPY libraries/ai/ /overleaf/libraries/ai/
|
||||||
COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
|
COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
|
||||||
COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
|
COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
|
||||||
COPY libraries/logger/ /overleaf/libraries/logger/
|
COPY libraries/logger/ /overleaf/libraries/logger/
|
||||||
@@ -95,6 +97,7 @@ RUN nice find /overleaf/services/web/public -name '*.js.map' -delete
|
|||||||
# copy source code and precompile pug images
|
# copy source code and precompile pug images
|
||||||
FROM deps-prod AS pug
|
FROM deps-prod AS pug
|
||||||
COPY libraries/access-token-encryptor/ /overleaf/libraries/access-token-encryptor/
|
COPY libraries/access-token-encryptor/ /overleaf/libraries/access-token-encryptor/
|
||||||
|
COPY libraries/ai/ /overleaf/libraries/ai/
|
||||||
COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
|
COPY libraries/eslint-plugin/ /overleaf/libraries/eslint-plugin/
|
||||||
COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
|
COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
|
||||||
COPY libraries/logger/ /overleaf/libraries/logger/
|
COPY libraries/logger/ /overleaf/libraries/logger/
|
||||||
|
|||||||
@@ -570,6 +570,7 @@ IMAGE_SCRATCH ?= $(IMAGE_REPO):do-not-use-this-tag-for-deploys--it-is-used-for-e
|
|||||||
IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
|
IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
|
||||||
$(MONOREPO)/package.json \
|
$(MONOREPO)/package.json \
|
||||||
$(MONOREPO)/package-lock.json \
|
$(MONOREPO)/package-lock.json \
|
||||||
|
$(MONOREPO)/libraries/ai/package.json \
|
||||||
$(MONOREPO)/libraries/access-token-encryptor/package.json \
|
$(MONOREPO)/libraries/access-token-encryptor/package.json \
|
||||||
$(MONOREPO)/libraries/eslint-plugin/package.json \
|
$(MONOREPO)/libraries/eslint-plugin/package.json \
|
||||||
$(MONOREPO)/libraries/fetch-utils/package.json \
|
$(MONOREPO)/libraries/fetch-utils/package.json \
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ const _ProjectController = {
|
|||||||
'writefull-frontend-migration',
|
'writefull-frontend-migration',
|
||||||
'chat-edit-delete',
|
'chat-edit-delete',
|
||||||
'compile-timeout-remove-info',
|
'compile-timeout-remove-info',
|
||||||
|
'ai-workbench',
|
||||||
'compile-timeout-target-plans',
|
'compile-timeout-target-plans',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const Usage = new Schema({
|
|||||||
const UserFeatureUsageSchema = new Schema({
|
const UserFeatureUsageSchema = new Schema({
|
||||||
features: {
|
features: {
|
||||||
aiErrorAssistant: Usage,
|
aiErrorAssistant: Usage,
|
||||||
|
aiWorkbench: Usage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1040,6 +1040,7 @@ module.exports = {
|
|||||||
referenceSearchSetting: [],
|
referenceSearchSetting: [],
|
||||||
errorLogsComponents: [],
|
errorLogsComponents: [],
|
||||||
referenceIndices: [],
|
referenceIndices: [],
|
||||||
|
railEntries: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
moduleImportSequence: [
|
moduleImportSequence: [
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
"ai_feedback_the_suggestion_didnt_fix_the_error": "",
|
"ai_feedback_the_suggestion_didnt_fix_the_error": "",
|
||||||
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "",
|
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "",
|
||||||
"ai_feedback_there_was_no_code_fix_suggested": "",
|
"ai_feedback_there_was_no_code_fix_suggested": "",
|
||||||
|
"ai_workbench": "",
|
||||||
"alignment": "",
|
"alignment": "",
|
||||||
"all_borders": "",
|
"all_borders": "",
|
||||||
"all_events": "",
|
"all_events": "",
|
||||||
|
|||||||
Binary file not shown.
@@ -49,6 +49,7 @@ export default /** @type {const} */ ([
|
|||||||
'shuffle',
|
'shuffle',
|
||||||
'smart_toy',
|
'smart_toy',
|
||||||
'space_dashboard',
|
'space_dashboard',
|
||||||
|
'star',
|
||||||
'strikethrough_s',
|
'strikethrough_s',
|
||||||
'table_chart',
|
'table_chart',
|
||||||
'table_chart',
|
'table_chart',
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ export default function RailPanel({
|
|||||||
>
|
>
|
||||||
<Tab.Content className="ide-rail-tab-content">
|
<Tab.Content className="ide-rail-tab-content">
|
||||||
{railTabs
|
{railTabs
|
||||||
.filter(({ hide }) => !hide)
|
.filter(({ hide }) => {
|
||||||
|
return typeof hide === 'function' ? !hide() : !hide
|
||||||
|
})
|
||||||
.map(({ key, component, mountOnFirstLoad }) => (
|
.map(({ key, component, mountOnFirstLoad }) => (
|
||||||
<Tab.Pane
|
<Tab.Pane
|
||||||
eventKey={key}
|
eventKey={key}
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ import RailResizeHandle from './rail-resize-handle'
|
|||||||
import RailModals from './rail-modals'
|
import RailModals from './rail-modals'
|
||||||
import RailOverflowDropdown from './rail-overflow-dropdown'
|
import RailOverflowDropdown from './rail-overflow-dropdown'
|
||||||
import useRailOverflow from '../../hooks/use-rail-overflow'
|
import useRailOverflow from '../../hooks/use-rail-overflow'
|
||||||
|
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||||
|
|
||||||
|
const moduleRailEntries = (
|
||||||
|
importOverleafModules('railEntries') as {
|
||||||
|
import: { default: RailElement }
|
||||||
|
path: string
|
||||||
|
}[]
|
||||||
|
).map(({ import: { default: element } }) => element)
|
||||||
|
|
||||||
export const RailLayout = () => {
|
export const RailLayout = () => {
|
||||||
const { sendEvent } = useEditorAnalytics()
|
const { sendEvent } = useEditorAnalytics()
|
||||||
@@ -79,6 +87,7 @@ export const RailLayout = () => {
|
|||||||
title: t('chat'),
|
title: t('chat'),
|
||||||
hide: !getMeta('ol-capabilities')?.includes('chat'),
|
hide: !getMeta('ol-capabilities')?.includes('chat'),
|
||||||
},
|
},
|
||||||
|
...moduleRailEntries,
|
||||||
],
|
],
|
||||||
[t, features.trackChangesVisible, view]
|
[t, features.trackChangesVisible, view]
|
||||||
)
|
)
|
||||||
@@ -125,7 +134,13 @@ export const RailLayout = () => {
|
|||||||
} else {
|
} else {
|
||||||
// HACK: Apparently the onSelect event is triggered with href attributes
|
// HACK: Apparently the onSelect event is triggered with href attributes
|
||||||
// from DropdownItems
|
// from DropdownItems
|
||||||
if (!railTabs.some(tab => !tab.hide && tab.key === key)) {
|
if (
|
||||||
|
!railTabs.some(tab =>
|
||||||
|
typeof tab.hide === 'function'
|
||||||
|
? !tab.hide()
|
||||||
|
: !tab.hide && tab.key === key
|
||||||
|
)
|
||||||
|
) {
|
||||||
// Attempting to open a non-existent tab
|
// Attempting to open a non-existent tab
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -150,7 +165,9 @@ export const RailLayout = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validTabKeys = railTabs.filter(tab => !tab.hide).map(tab => tab.key)
|
const validTabKeys = railTabs
|
||||||
|
.filter(tab => (typeof tab.hide === 'function' ? !tab.hide() : !tab.hide))
|
||||||
|
.map(tab => tab.key)
|
||||||
if (!validTabKeys.includes(selectedTab) && isOpen) {
|
if (!validTabKeys.includes(selectedTab) && isOpen) {
|
||||||
// If the selected tab is no longer valid (e.g. due to permissions changes),
|
// If the selected tab is no longer valid (e.g. due to permissions changes),
|
||||||
// switch back to the file tree
|
// switch back to the file tree
|
||||||
@@ -199,7 +216,9 @@ export const RailLayout = () => {
|
|||||||
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
||||||
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
|
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
|
||||||
{tabsInRail
|
{tabsInRail
|
||||||
.filter(({ hide }) => !hide)
|
.filter(({ hide }) =>
|
||||||
|
typeof hide === 'function' ? !hide() : !hide
|
||||||
|
)
|
||||||
.map(({ icon, key, indicator, title, disabled }) => (
|
.map(({ icon, key, indicator, title, disabled }) => (
|
||||||
<RailTab
|
<RailTab
|
||||||
open={isOpen && selectedTab === key}
|
open={isOpen && selectedTab === key}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type RailTabKey =
|
|||||||
| 'review-panel'
|
| 'review-panel'
|
||||||
| 'chat'
|
| 'chat'
|
||||||
| 'full-project-search'
|
| 'full-project-search'
|
||||||
|
| 'workbench'
|
||||||
|
|
||||||
export type RailModalKey = 'keyboard-shortcuts' | 'contact-us' | 'dictionary'
|
export type RailModalKey = 'keyboard-shortcuts' | 'contact-us' | 'dictionary'
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type RailElement = {
|
|||||||
component: ReactElement | null
|
component: ReactElement | null
|
||||||
indicator?: ReactElement
|
indicator?: ReactElement
|
||||||
title: string
|
title: string
|
||||||
hide?: boolean
|
hide?: boolean | (() => boolean)
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
mountOnFirstLoad?: boolean
|
mountOnFirstLoad?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@
|
|||||||
"ai_feedback_the_suggestion_didnt_fix_the_error": "The suggestion didn’t fix the error",
|
"ai_feedback_the_suggestion_didnt_fix_the_error": "The suggestion didn’t fix the error",
|
||||||
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "The suggestion wasn’t the best fix available",
|
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "The suggestion wasn’t the best fix available",
|
||||||
"ai_feedback_there_was_no_code_fix_suggested": "There was no code fix suggested",
|
"ai_feedback_there_was_no_code_fix_suggested": "There was no code fix suggested",
|
||||||
|
"ai_workbench": "AI workbench",
|
||||||
"alignment": "Alignment",
|
"alignment": "Alignment",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"all_borders": "All borders",
|
"all_borders": "All borders",
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
"safari > 14"
|
"safari > 14"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/mcp": "^1.0.0-beta.13",
|
||||||
|
"@ai-sdk/openai": "^3.0.0-beta.44",
|
||||||
"@aws-sdk/client-ses": "^3.864.0",
|
"@aws-sdk/client-ses": "^3.864.0",
|
||||||
"@contentful/rich-text-html-renderer": "^16.0.2",
|
"@contentful/rich-text-html-renderer": "^16.0.2",
|
||||||
"@contentful/rich-text-types": "^16.0.2",
|
"@contentful/rich-text-types": "^16.0.2",
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
"@stripe/stripe-js": "^7.7.0",
|
"@stripe/stripe-js": "^7.7.0",
|
||||||
"@xmldom/xmldom": "^0.7.13",
|
"@xmldom/xmldom": "^0.7.13",
|
||||||
"accepts": "^1.3.7",
|
"accepts": "^1.3.7",
|
||||||
|
"ai": "^6.0.0-beta.84",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"archiver": "^5.3.0",
|
"archiver": "^5.3.0",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
@@ -217,6 +220,7 @@
|
|||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@lezer/markdown": "^1.4.3",
|
"@lezer/markdown": "^1.4.3",
|
||||||
|
"@overleaf/ai": "^1.0.0",
|
||||||
"@overleaf/codemirror-tree-view": "^0.1.3",
|
"@overleaf/codemirror-tree-view": "^0.1.3",
|
||||||
"@overleaf/dictionaries": "https://github.com/overleaf/dictionaries/archive/refs/tags/v0.0.3.tar.gz",
|
"@overleaf/dictionaries": "https://github.com/overleaf/dictionaries/archive/refs/tags/v0.0.3.tar.gz",
|
||||||
"@overleaf/eslint-plugin": "*",
|
"@overleaf/eslint-plugin": "*",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"emitDecoratorMetadata": true /* Allow decorators in writefull - inversify */,
|
"emitDecoratorMetadata": true /* Allow decorators in writefull - inversify */,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./frontend/js/*"],
|
"@/*": ["./frontend/js/*"],
|
||||||
|
"@modules/*": ["./modules/*"],
|
||||||
"@overleaf/o-error": ["../../libraries/o-error"],
|
"@overleaf/o-error": ["../../libraries/o-error"],
|
||||||
"@overleaf/ranges-tracker": ["../../libraries/ranges-tracker"],
|
"@overleaf/ranges-tracker": ["../../libraries/ranges-tracker"],
|
||||||
/* can't make this entry @types because that conflicts with the "types" entry below */
|
/* can't make this entry @types because that conflicts with the "types" entry below */
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const {
|
|||||||
|
|
||||||
const PackageVersions = require('./app/src/infrastructure/PackageVersions.js')
|
const PackageVersions = require('./app/src/infrastructure/PackageVersions.js')
|
||||||
const invalidateBabelCacheIfNeeded = require('./frontend/macros/invalidate-babel-cache-if-needed')
|
const invalidateBabelCacheIfNeeded = require('./frontend/macros/invalidate-babel-cache-if-needed')
|
||||||
|
const { dirname } = require('node:path')
|
||||||
|
|
||||||
// Make sure that babel-macros are re-evaluated after changing the modules config
|
// Make sure that babel-macros are re-evaluated after changing the modules config
|
||||||
invalidateBabelCacheIfNeeded()
|
invalidateBabelCacheIfNeeded()
|
||||||
@@ -260,6 +261,26 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// CSS from AI module
|
||||||
|
include: dirname(require.resolve('@overleaf/ai')),
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
exportType: 'css-style-sheet',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: {
|
||||||
|
config: require.resolve('@overleaf/ai/postcss.config.js'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Standard CSS processing (extracted into separate file)
|
// Standard CSS processing (extracted into separate file)
|
||||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||||
@@ -309,6 +330,7 @@ module.exports = {
|
|||||||
alias: {
|
alias: {
|
||||||
// custom prefixes for import paths
|
// custom prefixes for import paths
|
||||||
'@': path.resolve(__dirname, './frontend/js/'),
|
'@': path.resolve(__dirname, './frontend/js/'),
|
||||||
|
'@modules': path.resolve(__dirname, './modules/'),
|
||||||
'@ol-types': path.resolve(__dirname, './types/'),
|
'@ol-types': path.resolve(__dirname, './types/'),
|
||||||
'@wf': path.resolve(
|
'@wf': path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
|||||||
Reference in New Issue
Block a user