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*
|
||||
.prettier*
|
||||
libraries/access-token-encryptor/**
|
||||
libraries/ai/**
|
||||
libraries/eslint-plugin/**
|
||||
libraries/fetch-utils/**
|
||||
libraries/logger/**
|
||||
|
||||
@@ -24,6 +24,7 @@ FROM base AS deps-prod
|
||||
|
||||
COPY package.json package-lock.json /overleaf/
|
||||
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/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
|
||||
COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
|
||||
@@ -58,6 +59,7 @@ FROM deps AS dev
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
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/fetch-utils/ /overleaf/libraries/fetch-utils/
|
||||
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
|
||||
FROM deps-prod AS pug
|
||||
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/fetch-utils/ /overleaf/libraries/fetch-utils/
|
||||
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 \
|
||||
$(MONOREPO)/package.json \
|
||||
$(MONOREPO)/package-lock.json \
|
||||
$(MONOREPO)/libraries/ai/package.json \
|
||||
$(MONOREPO)/libraries/access-token-encryptor/package.json \
|
||||
$(MONOREPO)/libraries/eslint-plugin/package.json \
|
||||
$(MONOREPO)/libraries/fetch-utils/package.json \
|
||||
|
||||
@@ -403,6 +403,7 @@ const _ProjectController = {
|
||||
'writefull-frontend-migration',
|
||||
'chat-edit-delete',
|
||||
'compile-timeout-remove-info',
|
||||
'ai-workbench',
|
||||
'compile-timeout-target-plans',
|
||||
].filter(Boolean)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const Usage = new Schema({
|
||||
const UserFeatureUsageSchema = new Schema({
|
||||
features: {
|
||||
aiErrorAssistant: Usage,
|
||||
aiWorkbench: Usage,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1040,6 +1040,7 @@ module.exports = {
|
||||
referenceSearchSetting: [],
|
||||
errorLogsComponents: [],
|
||||
referenceIndices: [],
|
||||
railEntries: [],
|
||||
},
|
||||
|
||||
moduleImportSequence: [
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"ai_feedback_the_suggestion_didnt_fix_the_error": "",
|
||||
"ai_feedback_the_suggestion_wasnt_the_best_fix_available": "",
|
||||
"ai_feedback_there_was_no_code_fix_suggested": "",
|
||||
"ai_workbench": "",
|
||||
"alignment": "",
|
||||
"all_borders": "",
|
||||
"all_events": "",
|
||||
|
||||
Binary file not shown.
@@ -49,6 +49,7 @@ export default /** @type {const} */ ([
|
||||
'shuffle',
|
||||
'smart_toy',
|
||||
'space_dashboard',
|
||||
'star',
|
||||
'strikethrough_s',
|
||||
'table_chart',
|
||||
'table_chart',
|
||||
|
||||
@@ -52,7 +52,9 @@ export default function RailPanel({
|
||||
>
|
||||
<Tab.Content className="ide-rail-tab-content">
|
||||
{railTabs
|
||||
.filter(({ hide }) => !hide)
|
||||
.filter(({ hide }) => {
|
||||
return typeof hide === 'function' ? !hide() : !hide
|
||||
})
|
||||
.map(({ key, component, mountOnFirstLoad }) => (
|
||||
<Tab.Pane
|
||||
eventKey={key}
|
||||
|
||||
@@ -26,6 +26,14 @@ import RailResizeHandle from './rail-resize-handle'
|
||||
import RailModals from './rail-modals'
|
||||
import RailOverflowDropdown from './rail-overflow-dropdown'
|
||||
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 = () => {
|
||||
const { sendEvent } = useEditorAnalytics()
|
||||
@@ -79,6 +87,7 @@ export const RailLayout = () => {
|
||||
title: t('chat'),
|
||||
hide: !getMeta('ol-capabilities')?.includes('chat'),
|
||||
},
|
||||
...moduleRailEntries,
|
||||
],
|
||||
[t, features.trackChangesVisible, view]
|
||||
)
|
||||
@@ -125,7 +134,13 @@ export const RailLayout = () => {
|
||||
} else {
|
||||
// HACK: Apparently the onSelect event is triggered with href attributes
|
||||
// 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
|
||||
return
|
||||
}
|
||||
@@ -150,7 +165,9 @@ export const RailLayout = () => {
|
||||
)
|
||||
|
||||
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 the selected tab is no longer valid (e.g. due to permissions changes),
|
||||
// switch back to the file tree
|
||||
@@ -199,7 +216,9 @@ export const RailLayout = () => {
|
||||
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
||||
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
|
||||
{tabsInRail
|
||||
.filter(({ hide }) => !hide)
|
||||
.filter(({ hide }) =>
|
||||
typeof hide === 'function' ? !hide() : !hide
|
||||
)
|
||||
.map(({ icon, key, indicator, title, disabled }) => (
|
||||
<RailTab
|
||||
open={isOpen && selectedTab === key}
|
||||
|
||||
@@ -23,6 +23,7 @@ export type RailTabKey =
|
||||
| 'review-panel'
|
||||
| 'chat'
|
||||
| 'full-project-search'
|
||||
| 'workbench'
|
||||
|
||||
export type RailModalKey = 'keyboard-shortcuts' | 'contact-us' | 'dictionary'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export type RailElement = {
|
||||
component: ReactElement | null
|
||||
indicator?: ReactElement
|
||||
title: string
|
||||
hide?: boolean
|
||||
hide?: boolean | (() => boolean)
|
||||
disabled?: 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_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_workbench": "AI workbench",
|
||||
"alignment": "Alignment",
|
||||
"all": "All",
|
||||
"all_borders": "All borders",
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
"safari > 14"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ai-sdk/mcp": "^1.0.0-beta.13",
|
||||
"@ai-sdk/openai": "^3.0.0-beta.44",
|
||||
"@aws-sdk/client-ses": "^3.864.0",
|
||||
"@contentful/rich-text-html-renderer": "^16.0.2",
|
||||
"@contentful/rich-text-types": "^16.0.2",
|
||||
@@ -107,6 +109,7 @@
|
||||
"@stripe/stripe-js": "^7.7.0",
|
||||
"@xmldom/xmldom": "^0.7.13",
|
||||
"accepts": "^1.3.7",
|
||||
"ai": "^6.0.0-beta.84",
|
||||
"ajv": "^8.12.0",
|
||||
"archiver": "^5.3.0",
|
||||
"async": "^3.2.5",
|
||||
@@ -217,6 +220,7 @@
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@lezer/markdown": "^1.4.3",
|
||||
"@overleaf/ai": "^1.0.0",
|
||||
"@overleaf/codemirror-tree-view": "^0.1.3",
|
||||
"@overleaf/dictionaries": "https://github.com/overleaf/dictionaries/archive/refs/tags/v0.0.3.tar.gz",
|
||||
"@overleaf/eslint-plugin": "*",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"emitDecoratorMetadata": true /* Allow decorators in writefull - inversify */,
|
||||
"paths": {
|
||||
"@/*": ["./frontend/js/*"],
|
||||
"@modules/*": ["./modules/*"],
|
||||
"@overleaf/o-error": ["../../libraries/o-error"],
|
||||
"@overleaf/ranges-tracker": ["../../libraries/ranges-tracker"],
|
||||
/* 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 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
|
||||
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)
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||
@@ -309,6 +330,7 @@ module.exports = {
|
||||
alias: {
|
||||
// custom prefixes for import paths
|
||||
'@': path.resolve(__dirname, './frontend/js/'),
|
||||
'@modules': path.resolve(__dirname, './modules/'),
|
||||
'@ol-types': path.resolve(__dirname, './types/'),
|
||||
'@wf': path.resolve(
|
||||
__dirname,
|
||||
|
||||
Reference in New Issue
Block a user