feat: support canvas (#75)

* use light theme for code block

* enhance codeblock style to compatity with new white UI

* feat: move canvas to chat-ui

* fix: format

* fix imports

* copy markdown styles before run dev

* Create swift-flies-breathe.md

* show ArtifactAnnotations by default

* add canvas demo

* make it responsive
This commit is contained in:
Thuc Pham
2025-04-24 15:13:24 +07:00
committed by GitHub
parent 895a91ddee
commit 3d4fbae32e
23 changed files with 1967 additions and 27 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@llamaindex/chat-ui': patch
'@llamaindex/eslint-config': patch
---
feat: support canvas
+586
View File
@@ -0,0 +1,586 @@
'use client'
import { useCopyToClipboard } from '@/app/use-copy-to-clipboard'
import {
ChatCanvas,
ChatInput,
ChatMessages,
ChatSection,
} from '@llamaindex/chat-ui'
import { Message, useChat } from 'ai/react'
const code = `
import {
ChatCanvas,
ChatInput,
ChatMessages,
ChatSection,
} from '@llamaindex/chat-ui'
import { useChat } from 'ai/react'
export function CustomChat() {
const handler = useChat({ initialMessages: [] })
return (
<ChatSection
handler={handler}
className="block h-screen flex-row gap-4 p-0 md:flex md:p-5"
>
<div className="md:max-w-1/2 mx-auto flex h-full min-w-0 max-w-full flex-1 flex-col gap-4">
<ChatMessages />
<ChatInput />
</div>
<ChatCanvas className="w-full md:w-2/3" />
</ChatSection>
)
}
`
const initialMessages: Message[] = [
{
role: 'user',
content: 'Write binary search algorithm in python',
id: 'aeJ4ZOWlhUA2R4vg',
},
{
id: 'Yqc9VoIR3ANyzTj3',
role: 'assistant',
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
annotations: [
{
userInput: 'Write binary search algorithm in python',
chatHistory: [],
},
{
input: [
{
role: 'user',
content: 'Write binary search algorithm in python',
},
],
currentAgentName: 'Agent',
},
{
input: [
{
role: 'system',
content:
"You are a helpful assistant that generates code artifacts or document artifacts based on the user's request. Do NOT include installation instructions in the response. Please add a short summary about the artifact, don't include any other text or repeat the code or document",
},
{
role: 'user',
content: 'Write binary search algorithm in python',
},
],
currentAgentName: 'Agent',
},
{
agentName: 'Agent',
response: {
role: 'assistant',
content: '',
options: {
toolCall: [
{
name: 'code_artifact_generator',
input: {
requirement:
'Binary search algorithm in Python that searches for a target value in a sorted list.',
},
id: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
},
],
},
},
toolCalls: [
{
data: {
agentName: 'Agent',
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Binary search algorithm in Python that searches for a target value in a sorted list.',
},
toolId: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
},
displayName: 'AgentToolCall',
},
],
},
{
response: {
role: 'assistant',
content: '',
options: {
toolCall: [
{
name: 'code_artifact_generator',
input: {
requirement:
'Binary search algorithm in Python that searches for a target value in a sorted list.',
},
id: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
},
],
},
},
toolCalls: [
{
data: {
agentName: 'Agent',
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Binary search algorithm in Python that searches for a target value in a sorted list.',
},
toolId: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
},
displayName: 'AgentToolCall',
},
],
currentAgentName: 'Agent',
},
{
agentName: 'Agent',
toolCalls: [
{
data: {
agentName: 'Agent',
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Binary search algorithm in Python that searches for a target value in a sorted list.',
},
toolId: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
},
displayName: 'AgentToolCall',
},
],
},
{
type: 'agent',
data: {
agent: 'Agent',
text: 'Using tool: \'code_artifact_generator\' with inputs: \'{"requirement":"Binary search algorithm in Python that searches for a target value in a sorted list."}\'',
type: 'text',
},
},
{
agentName: 'Agent',
results: [
{
data: {
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Binary search algorithm in Python that searches for a target value in a sorted list.',
},
toolId: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
toolOutput: {
id: 'call_2zAd9vJV9wFUDwEBf6aypLyc',
result:
'{\n type: code,\n data: {\n file_name: binary_search.py,\n code: def binary_search(arr, target):\\n left, right = 0, len(arr) - 1\\n while left <= right:\\n mid = left + (right - left) // 2\\n if arr[mid] == target:\\n return mid\\n elif arr[mid] < target:\\n left = mid + 1\\n else:\\n right = mid - 1\\n return -1\\n\\n# Example usage:\\n# sorted_list = [1, 2, 3, 4, 5]\\n# target = 3\\n# result = binary_search(sorted_list, target)\\n# print(result) # Output: 2,\n language: python\n },\n created_at: 1745480281756\n}',
isError: false,
},
returnDirect: false,
raw: {
type: 'code',
data: {
file_name: 'binary_search.py',
code: 'def binary_search(arr, target):\n left, right = 0, len(arr) - 1\n while left <= right:\n mid = left + (right - left) // 2\n if arr[mid] == target:\n return mid\n elif arr[mid] < target:\n left = mid + 1\n else:\n right = mid - 1\n return -1\n\n# Example usage:\n# sorted_list = [1, 2, 3, 4, 5]\n# target = 3\n# result = binary_search(sorted_list, target)\n# print(result) # Output: 2',
language: 'python',
},
created_at: 1745480281756,
},
},
displayName: 'AgentToolCallResult',
},
],
},
{
type: 'artifact',
data: {
type: 'code',
data: {
file_name: 'binary_search.py',
code: 'def binary_search(arr, target):\n left, right = 0, len(arr) - 1\n while left <= right:\n mid = left + (right - left) // 2\n if arr[mid] == target:\n return mid\n elif arr[mid] < target:\n left = mid + 1\n else:\n right = mid - 1\n return -1\n\n# Example usage:\n# sorted_list = [1, 2, 3, 4, 5]\n# target = 3\n# result = binary_search(sorted_list, target)\n# print(result) # Output: 2',
language: 'python',
},
created_at: 1745480281756,
},
},
{
input: [
{
role: 'user',
content: 'Write binary search algorithm in python',
},
],
currentAgentName: 'Agent',
},
{
input: [
{
role: 'system',
content:
"You are a helpful assistant that generates code artifacts or document artifacts based on the user's request. Do NOT include installation instructions in the response. Please add a short summary about the artifact, don't include any other text or repeat the code or document",
},
{
role: 'user',
content: 'Write binary search algorithm in python',
},
],
currentAgentName: 'Agent',
},
{
agentName: 'Agent',
response: {
role: 'assistant',
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
},
toolCalls: [],
},
{
response: {
role: 'assistant',
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
},
toolCalls: [],
currentAgentName: 'Agent',
},
{
result:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
},
{
type: 'suggested_questions',
data: [
'How does the binary search algorithm work?',
'Can you provide an example of how to implement binary search in Python?',
'What are the time and space complexities of the binary search algorithm?',
],
},
],
},
{
role: 'user',
content: 'Add some logs to the code',
annotations: [],
id: 'k2FkjYkhd44CC6xw',
},
{
id: 'yWQHYXgn6WT0oE7J',
role: 'assistant',
content:
'The artifact is a Python implementation of the binary search algorithm that includes logging statements to track the values of the left, right, and mid indices during each step of the search process.',
annotations: [
{
userInput: 'Add some logs to the code',
chatHistory: [
{
content: 'Write binary search algorithm in python',
role: 'user',
},
{
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
role: 'assistant',
},
],
},
{
input: [
{
content: 'Write binary search algorithm in python',
role: 'user',
},
{
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
role: 'assistant',
},
{
role: 'user',
content: 'Add some logs to the code',
},
],
currentAgentName: 'Agent',
},
{
input: [
{
role: 'system',
content:
"You are a helpful assistant that generates code artifacts or document artifacts based on the user's request. Do NOT include installation instructions in the response. Please add a short summary about the artifact, don't include any other text or repeat the code or document",
},
{
content: 'Write binary search algorithm in python',
role: 'user',
},
{
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
role: 'assistant',
},
{
role: 'user',
content: 'Add some logs to the code',
},
],
currentAgentName: 'Agent',
},
{
agentName: 'Agent',
response: {
role: 'assistant',
content: '',
options: {
toolCall: [
{
name: 'code_artifact_generator',
input: {
requirement:
'Python implementation of the binary search algorithm with logging for each step of the search process.',
},
id: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
},
],
},
},
toolCalls: [
{
data: {
agentName: 'Agent',
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Python implementation of the binary search algorithm with logging for each step of the search process.',
},
toolId: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
},
displayName: 'AgentToolCall',
},
],
},
{
response: {
role: 'assistant',
content: '',
options: {
toolCall: [
{
name: 'code_artifact_generator',
input: {
requirement:
'Python implementation of the binary search algorithm with logging for each step of the search process.',
},
id: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
},
],
},
},
toolCalls: [
{
data: {
agentName: 'Agent',
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Python implementation of the binary search algorithm with logging for each step of the search process.',
},
toolId: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
},
displayName: 'AgentToolCall',
},
],
currentAgentName: 'Agent',
},
{
agentName: 'Agent',
toolCalls: [
{
data: {
agentName: 'Agent',
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Python implementation of the binary search algorithm with logging for each step of the search process.',
},
toolId: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
},
displayName: 'AgentToolCall',
},
],
},
{
type: 'agent',
data: {
agent: 'Agent',
text: 'Using tool: \'code_artifact_generator\' with inputs: \'{"requirement":"Python implementation of the binary search algorithm with logging for each step of the search process."}\'',
type: 'text',
},
},
{
agentName: 'Agent',
results: [
{
data: {
toolName: 'code_artifact_generator',
toolKwargs: {
requirement:
'Python implementation of the binary search algorithm with logging for each step of the search process.',
},
toolId: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
toolOutput: {
id: 'call_5vlAF3SDasVgtH3Wk0EgL2oX',
result:
"{\n type: code,\n data: {\n file_name: binary_search.py,\n code: import logging\\n\\nlogging.basicConfig(level=logging.INFO)\\n\\ndef binary_search(arr, target):\\n left, right = 0, len(arr) - 1\\n while left <= right:\\n mid = left + (right - left) // 2\\n logging.info(f'Left: {left}, Right: {right}, Mid: {mid}, Mid Value: {arr[mid]}')\\n if arr[mid] == target:\\n return mid\\n elif arr[mid] < target:\\n left = mid + 1\\n else:\\n right = mid - 1\\n return -1\\n\\n# Example usage\\nif __name__ == '__main__':\\n arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\\n target = 7\\n result = binary_search(arr, target)\\n if result != -1:\\n logging.info(f'Target found at index: {result}')\\n else:\\n logging.info('Target not found'),\n language: python\n },\n created_at: 1745480319651\n}",
isError: false,
},
returnDirect: false,
raw: {
type: 'code',
data: {
file_name: 'binary_search.py',
code: "import logging\n\nlogging.basicConfig(level=logging.INFO)\n\ndef binary_search(arr, target):\n left, right = 0, len(arr) - 1\n while left <= right:\n mid = left + (right - left) // 2\n logging.info(f'Left: {left}, Right: {right}, Mid: {mid}, Mid Value: {arr[mid]}')\n if arr[mid] == target:\n return mid\n elif arr[mid] < target:\n left = mid + 1\n else:\n right = mid - 1\n return -1\n\n# Example usage\nif __name__ == '__main__':\n arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n target = 7\n result = binary_search(arr, target)\n if result != -1:\n logging.info(f'Target found at index: {result}')\n else:\n logging.info('Target not found')",
language: 'python',
},
created_at: 1745480319651,
},
},
displayName: 'AgentToolCallResult',
},
],
},
{
type: 'artifact',
data: {
type: 'code',
data: {
file_name: 'binary_search.py',
code: "import logging\n\nlogging.basicConfig(level=logging.INFO)\n\ndef binary_search(arr, target):\n left, right = 0, len(arr) - 1\n while left <= right:\n mid = left + (right - left) // 2\n logging.info(f'Left: {left}, Right: {right}, Mid: {mid}, Mid Value: {arr[mid]}')\n if arr[mid] == target:\n return mid\n elif arr[mid] < target:\n left = mid + 1\n else:\n right = mid - 1\n return -1\n\n# Example usage\nif __name__ == '__main__':\n arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n target = 7\n result = binary_search(arr, target)\n if result != -1:\n logging.info(f'Target found at index: {result}')\n else:\n logging.info('Target not found')",
language: 'python',
},
created_at: 1745480319651,
},
},
{
input: [
{
content: 'Write binary search algorithm in python',
role: 'user',
},
{
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
role: 'assistant',
},
{
role: 'user',
content: 'Add some logs to the code',
},
],
currentAgentName: 'Agent',
},
{
input: [
{
role: 'system',
content:
"You are a helpful assistant that generates code artifacts or document artifacts based on the user's request. Do NOT include installation instructions in the response. Please add a short summary about the artifact, don't include any other text or repeat the code or document",
},
{
content: 'Write binary search algorithm in python',
role: 'user',
},
{
content:
'The artifact is a Python implementation of the binary search algorithm, which efficiently searches for a target value in a sorted list.',
role: 'assistant',
},
{
role: 'user',
content: 'Add some logs to the code',
},
],
currentAgentName: 'Agent',
},
{
agentName: 'Agent',
response: {
role: 'assistant',
content:
'The artifact is a Python implementation of the binary search algorithm that includes logging statements to track the values of the left, right, and mid indices during each step of the search process.',
},
toolCalls: [],
},
{
response: {
role: 'assistant',
content:
'The artifact is a Python implementation of the binary search algorithm that includes logging statements to track the values of the left, right, and mid indices during each step of the search process.',
},
toolCalls: [],
currentAgentName: 'Agent',
},
{
result:
'The artifact is a Python implementation of the binary search algorithm that includes logging statements to track the values of the left, right, and mid indices during each step of the search process.',
},
{
type: 'suggested_questions',
data: [
'Can you explain how the binary search algorithm works?',
'What are the time and space complexities of the binary search algorithm?',
'How can I modify the binary search algorithm to return the index of the target value?',
],
},
],
},
]
export default function Page(): JSX.Element {
return <CustomChat />
}
function CustomChat() {
const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 2000 })
const handler = useChat({ initialMessages })
return (
<ChatSection
handler={handler}
className="block h-screen flex-row gap-4 p-0 md:flex md:p-5"
>
<div className="md:max-w-1/2 mx-auto flex h-full min-w-0 max-w-full flex-1 flex-col gap-4">
<div className="flex justify-between gap-2 border-b p-4">
<div>
<h1 className="bg-gradient-to-r from-[#e711dd] to-[#0fc1e0] bg-clip-text text-lg font-bold text-transparent">
LlamaIndex ChatUI - Canvas Demo
</h1>
<p className="text-sm text-zinc-400">
Try click to a version to see how it works
</p>
</div>
<button
type="button"
onClick={() => {
copyToClipboard(code)
}}
className={`flex h-10 items-center gap-2 rounded-lg bg-zinc-700 px-2 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-600 ${
isCopied ? 'bg-green-600 hover:bg-green-500' : ''
}`}
>
{isCopied ? 'Copied!' : 'Copy Code'}
</button>
</div>
<ChatMessages />
<ChatInput />
</div>
<ChatCanvas className="w-full md:w-2/3" />
</ChatSection>
)
}
+6
View File
@@ -52,6 +52,12 @@ export default function Page(): JSX.Element {
>
Custom Chat
</Link>
<Link
href="/demo/canvas"
className="rounded-full bg-gradient-to-r from-[#ff6b6b] to-[#4ecdc4] px-6 py-2 font-medium text-white transition hover:opacity-90"
>
Chat with Canvas
</Link>
</div>
</div>
+6 -2
View File
@@ -25,12 +25,14 @@
},
"license": "MIT",
"scripts": {
"build": "pnpm run clean && pnpm run build:js && pnpm run build:css && pnpm run copy-fonts",
"build": "pnpm run clean && pnpm run build:js && pnpm run prepare:style",
"prepare:style": "pnpm run build:css && pnpm run copy-fonts",
"build:css": "postcss \"src/styles/*.css\" --base src/styles --dir dist/styles",
"build:js": "tsup",
"copy-fonts": "cp -r node_modules/katex/dist/fonts dist/styles/fonts",
"lint": "eslint src/",
"dev": "pnpm run build:js --watch",
"predev": "pnpm run prepare:style",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist",
"copy": "cp -r ../../README.md ../../LICENSE .",
@@ -59,6 +61,8 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-accordion": "^1.2.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"highlight.js": "^11.10.0",
@@ -76,4 +80,4 @@
"publishConfig": {
"access": "public"
}
}
}
+46 -1
View File
@@ -1,4 +1,4 @@
import { JSONValue } from './chat.interface'
import { JSONValue, Message } from './chat.interface'
export enum MessageAnnotationType {
IMAGE = 'image',
@@ -7,6 +7,7 @@ export enum MessageAnnotationType {
EVENTS = 'events',
SUGGESTED_QUESTIONS = 'suggested_questions',
AGENT_EVENTS = 'agent',
ARTIFACT = 'artifact',
}
export type ImageData = {
@@ -65,6 +66,24 @@ export type AgentEventData = {
export type SuggestedQuestionsData = string[]
export type Artifact<T = unknown> = {
created_at: number
type: 'code' | 'document'
data: T
}
export type CodeArtifact = Artifact<{
file_name: string
code: string
language: string
}>
export type DocumentArtifact = Artifact<{
title: string
content: string
type: string
}>
export type AnnotationData =
| ImageData
| DocumentFileData
@@ -72,6 +91,7 @@ export type AnnotationData =
| EventData
| AgentEventData
| SuggestedQuestionsData
| Artifact
export type MessageAnnotation = {
type: MessageAnnotationType
@@ -151,3 +171,28 @@ function preprocessSourceNodes(nodes: SourceNode[]): SourceNode[] {
})
return processedNodes
}
export type CodeArtifactError = {
artifact: CodeArtifact
errors: string[]
}
export function extractArtifactsFromMessage(message: Message): Artifact[] {
const artifacts = getChatUIAnnotation<Artifact>(
message.annotations as MessageAnnotation[],
MessageAnnotationType.ARTIFACT
)
return artifacts ?? []
}
// extract artifacts from all messages (sort ascending by created_at)
export function extractArtifactsFromAllMessages(messages: Message[]) {
return messages
.flatMap(extractArtifactsFromMessage)
.sort((a, b) => a.created_at - b.created_at)
}
// check if two artifacts are equal by comparing their type and created time
export function isEqualArtifact(a: Artifact, b: Artifact) {
return a.type === b.type && a.created_at === b.created_at
}
@@ -0,0 +1,218 @@
'use client'
import { Check, Copy, Download, History, X } from 'lucide-react'
import { useState } from 'react'
import { useCopyToClipboard } from '../../hook/use-copy-to-clipboard'
import { cn } from '../../lib/utils'
import { Badge } from '../../ui/badge'
import { Button } from '../../ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'
import {
Artifact,
CodeArtifact,
DocumentArtifact,
isEqualArtifact,
} from '../annotation'
import { useChatCanvas } from './context'
interface ChatCanvasActionsProps {
children?: React.ReactNode
className?: string
}
function ChatCanvasActions(props: ChatCanvasActionsProps) {
const children = props.children ?? (
<>
<ArtifactVersionHistory />
<ArtifactContentCopy />
<ArtifactDownloadButton />
<CanvasCloseButton />
</>
)
return (
<div className={cn('flex items-center gap-1', props.className)}>
{children}
</div>
)
}
function ArtifactVersionHistory() {
const {
getArtifactsByType,
openArtifactInCanvas,
displayedArtifact,
getArtifactVersion,
restoreArtifact,
} = useChatCanvas()
const [isOpen, setIsOpen] = useState(false)
if (!displayedArtifact) return null
const allArtifactsByCurrentType = getArtifactsByType(displayedArtifact.type)
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant="secondary"
className="h-8 cursor-pointer rounded-full text-xs"
>
<History className="mr-1 size-4" />
Version {getArtifactVersion(displayedArtifact).versionNumber}
</Button>
</PopoverTrigger>
<PopoverContent className="w-48 p-0 text-xs" align="end">
<h4 className="border-b p-2 px-3 font-semibold">Version History</h4>
<div className="max-h-80 overflow-y-auto">
{allArtifactsByCurrentType.map((artifact, index) => {
const isCurrent = isEqualArtifact(artifact, displayedArtifact)
const { versionNumber, isLatest } = getArtifactVersion(artifact)
return (
<div
key={index}
className="text-muted-foreground flex cursor-pointer items-center justify-between px-3 py-2 hover:bg-gray-100"
onClick={() => {
openArtifactInCanvas(artifact)
setIsOpen(false)
}}
>
<span className={cn(isCurrent && 'text-blue-500')}>
Version {versionNumber}
</span>
{isLatest ? (
<Badge className="h-6 w-[70px] justify-center bg-blue-500 text-center hover:bg-blue-600">
Latest
</Badge>
) : (
<Button
variant="outline"
size="sm"
className="h-6 w-[70px] shrink-0 cursor-pointer rounded-full text-xs"
onClick={e => {
e.stopPropagation()
restoreArtifact(artifact)
setIsOpen(false)
}}
>
Restore
</Button>
)}
</div>
)
})}
</div>
</PopoverContent>
</Popover>
)
}
function ArtifactContentCopy() {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
const { displayedArtifact } = useChatCanvas()
if (!displayedArtifact) return null
const content = getArtifactContent(displayedArtifact)
if (!content) return null
const handleCopy = () => {
if (isCopied) return
copyToClipboard(content)
}
return (
<Button
variant="ghost"
size="icon"
className="cursor-pointer rounded-full"
onClick={handleCopy}
>
{isCopied ? <Check className="size-4" /> : <Copy className="size-4" />}
</Button>
)
}
function getArtifactContent(artifact: Artifact) {
if (artifact.type === 'code') {
return (artifact as CodeArtifact).data.code
}
if (artifact.type === 'document') {
return (artifact as DocumentArtifact).data.content
}
return null
}
function ArtifactDownloadButton() {
const { displayedArtifact } = useChatCanvas()
if (!displayedArtifact) return null
const content = getArtifactContent(displayedArtifact)
const fileName = getArtifactDownloadFileName(displayedArtifact)
if (!content || !fileName) return null
const handleDownload = () => {
const blob = new Blob([content], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
return (
<Button
variant="ghost"
size="icon"
className="cursor-pointer rounded-full"
onClick={handleDownload}
>
<Download className="size-4" />
</Button>
)
}
function getArtifactDownloadFileName(artifact: Artifact) {
if (artifact.type === 'code') {
return (artifact as CodeArtifact).data.file_name
}
if (artifact.type === 'document') {
return (artifact as DocumentArtifact).data.title
}
return null
}
function CanvasCloseButton() {
const { closeCanvas } = useChatCanvas()
return (
<Button
variant="ghost"
size="icon"
className="cursor-pointer rounded-full"
onClick={closeCanvas}
>
<X className="size-4" />
</Button>
)
}
ChatCanvasActions.History = ArtifactVersionHistory
ChatCanvasActions.Copy = ArtifactContentCopy
ChatCanvasActions.Download = ArtifactDownloadButton
ChatCanvasActions.Close = CanvasCloseButton
export { ChatCanvasActions }
@@ -0,0 +1,63 @@
'use client'
import { cn } from '../../../lib/utils'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../ui/tabs'
import { CodeBlock } from '../../../widgets'
import { CodeArtifact } from '../../annotation'
import { ChatCanvasActions } from '../actions'
import { useChatCanvas } from '../context'
interface CodeArtifactViewerProps {
className?: string
tabs?: Record<string, React.ReactNode>
}
export function CodeArtifactViewer({
className,
tabs,
}: CodeArtifactViewerProps) {
const { displayedArtifact } = useChatCanvas()
if (displayedArtifact?.type !== 'code') return null
const {
created_at,
data: { language, code },
} = displayedArtifact as CodeArtifact
return (
<Tabs
defaultValue="code"
className={cn('flex h-full min-h-0 flex-1 flex-col gap-4 p-4', className)}
>
<div className="flex items-center justify-between">
<TabsList>
<TabsTrigger value="code">Code</TabsTrigger>
{tabs &&
Object.entries(tabs).map(([key]) => (
<TabsTrigger key={key} value={key} className="capitalize">
{key}
</TabsTrigger>
))}
</TabsList>
<ChatCanvasActions />
</div>
<div className="min-h-0 flex-1 overflow-auto pr-2">
<TabsContent value="code" className="h-full">
<CodeBlock
key={created_at} // make the code block re-highlight when changing artifact
language={language}
value={code}
showHeader={false}
/>
</TabsContent>
{tabs &&
Object.entries(tabs).map(([key, value]) => (
<TabsContent key={key} value={key} className="h-full">
{value}
</TabsContent>
))}
</div>
</Tabs>
)
}
@@ -0,0 +1,44 @@
'use client'
import { FileText } from 'lucide-react'
import { cn } from '../../../lib/utils'
import { DocumentArtifact } from '../../annotation'
import { ChatCanvasActions } from '../actions'
import { useChatCanvas } from '../context'
import { Markdown } from '../../../widgets'
interface DocumentArtifactViewerProps {
className?: string
children?: React.ReactNode
}
export function DocumentArtifactViewer({
className,
children,
}: DocumentArtifactViewerProps) {
const { displayedArtifact } = useChatCanvas()
if (displayedArtifact?.type !== 'document') return null
const {
data: { content, title, type },
} = displayedArtifact as DocumentArtifact
return (
<div className={cn('flex min-h-0 flex-1 flex-col', className)}>
<div className="flex items-center justify-between border-b p-4">
<h3 className="flex items-center gap-3 text-gray-600">
<FileText className="size-8 text-blue-500" />
<div className="flex flex-col">
<div className="text font-semibold">{title}</div>
<div className="text-xs text-gray-500">{type}</div>
</div>
</h3>
<ChatCanvasActions />
</div>
<div className="flex min-h-0 flex-1 flex-col items-stretch gap-4 overflow-auto px-20 py-4">
{children ?? <Markdown content={content} />}
</div>
</div>
)
}
+76
View File
@@ -0,0 +1,76 @@
import { FileCode, FileText, LucideIcon } from 'lucide-react'
import { cn } from '../../lib/utils'
import { Badge } from '../../ui/badge'
import { Button } from '../../ui/button'
import {
Artifact,
CodeArtifact,
DocumentArtifact,
isEqualArtifact,
} from '../annotation'
import { useChatCanvas } from './context'
const IconMap: Record<Artifact['type'], LucideIcon> = {
code: FileCode,
document: FileText,
}
export function ArtifactCard({ artifact }: { artifact: Artifact }) {
const {
openArtifactInCanvas,
getArtifactVersion,
restoreArtifact,
displayedArtifact,
} = useChatCanvas()
const { versionNumber, isLatest } = getArtifactVersion(artifact)
const Icon = IconMap[artifact.type]
const title = getCardTitle(artifact)
const isDisplayed =
displayedArtifact && isEqualArtifact(artifact, displayedArtifact)
return (
<div
className={cn(
'border-border flex w-full max-w-72 cursor-pointer items-center justify-between gap-2 rounded-lg border-2 p-2 hover:border-blue-500',
isDisplayed && 'border-blue-500'
)}
onClick={() => openArtifactInCanvas(artifact)}
>
<div className="flex flex-1 items-center gap-2">
<Icon className="size-7 shrink-0 text-blue-500" />
<div className="flex flex-col">
<div className="text-sm font-semibold">Version {versionNumber}</div>
{title && <div className="text-xs text-gray-600">{title}</div>}
</div>
</div>
{isLatest ? (
<Badge className="ml-2 bg-blue-500 hover:bg-blue-600">Latest</Badge>
) : (
<Button
variant="ghost"
size="sm"
className="h-8 shrink-0 cursor-pointer text-xs"
onClick={e => {
e.stopPropagation()
restoreArtifact(artifact)
}}
>
Restore
</Button>
)}
</div>
)
}
const getCardTitle = (artifact: Artifact) => {
if (artifact.type === 'code') {
const { file_name } = artifact.data as CodeArtifact['data']
return file_name
}
if (artifact.type === 'document') {
const { title } = artifact.data as DocumentArtifact['data']
return title
}
return ''
}
@@ -0,0 +1,190 @@
'use client'
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import {
Artifact,
CodeArtifact,
extractArtifactsFromMessage,
CodeArtifactError,
extractArtifactsFromAllMessages,
isEqualArtifact,
} from '../annotation'
import { Message } from '../chat.interface'
import { useChatUI } from '../chat.context'
interface ChatCanvasContextType {
allArtifacts: Artifact[]
getArtifactsByType: (type: Artifact['type']) => Artifact[]
displayedArtifact: Artifact | undefined
isCanvasOpen: boolean
openArtifactInCanvas: (artifact: Artifact) => void
closeCanvas: () => void
appendErrors: (artifact: CodeArtifact, errors: string[]) => void
clearCodeErrors: (artifact: CodeArtifact) => void
getCodeErrors: (artifact: CodeArtifact) => string[]
fixCodeErrors: (artifact: CodeArtifact) => void
getArtifactVersion: (artifact: Artifact) => {
versionNumber: number
isLatest: boolean
}
restoreArtifact: (artifact: Artifact) => void
}
const ChatCanvasContext = createContext<ChatCanvasContextType | undefined>(
undefined
)
export function ChatCanvasProvider({ children }: { children: ReactNode }) {
const { messages, isLoading, append, requestData, setMessages } = useChatUI()
const [isCanvasOpen, setIsCanvasOpen] = useState(false) // whether the canvas is open
const [displayedArtifact, setDisplayedArtifact] = useState<Artifact>() // the artifact currently displayed in the canvas
const [codeErrors, setCodeErrors] = useState<CodeArtifactError[]>([]) // contain all errors when compiling with Babel and runtime
const allArtifacts = useMemo(
() => extractArtifactsFromAllMessages(messages),
[messages]
)
// get all artifacts from the last message, this may not be the latest artifact in case last message doesn't have any artifact
const artifactsFromLastMessage = useMemo(() => {
const lastMessage = messages[messages.length - 1]
if (!lastMessage) return []
const artifacts = extractArtifactsFromMessage(lastMessage)
return artifacts
}, [messages])
useEffect(() => {
// when stream is loading and last message has a artifact, open the canvas with that artifact
if (artifactsFromLastMessage.length > 0 && isLoading) {
setIsCanvasOpen(true)
setDisplayedArtifact(
artifactsFromLastMessage[artifactsFromLastMessage.length - 1]
)
}
}, [artifactsFromLastMessage, isCanvasOpen, isLoading])
const openArtifactInCanvas = (artifact: Artifact) => {
setDisplayedArtifact(artifact)
setIsCanvasOpen(true)
}
const getArtifactsByType = (type: Artifact['type']) => {
return allArtifacts.filter(a => a.type === type)
}
const getArtifactVersion = (artifact: Artifact) => {
const allArtifactsByCurrentType = getArtifactsByType(artifact.type)
const versionNumber =
allArtifactsByCurrentType.findIndex(a => isEqualArtifact(a, artifact)) + 1
return {
versionNumber,
isLatest: versionNumber === allArtifactsByCurrentType.length,
}
}
const restoreArtifact = (artifact: Artifact) => {
if (!setMessages) return
const newArtifact = {
...artifact,
created_at: Date.now(),
}
const newMessages = [
...messages,
{
id: `restore-msg-${Date.now()}`,
role: 'user',
content: `Restore to ${artifact.type} version ${getArtifactVersion(artifact).versionNumber}`,
},
{
id: `restore-success-${Date.now()}`,
role: 'assistant',
content: `Successfully restored to ${artifact.type} version ${getArtifactVersion(artifact).versionNumber}`,
annotations: [
{
type: 'artifact',
data: newArtifact,
},
],
},
] as (Message & { id: string })[]
setMessages(newMessages)
openArtifactInCanvas(newArtifact)
}
const closeCanvas = () => {
setIsCanvasOpen(false)
setDisplayedArtifact(undefined)
}
const appendErrors = (artifact: CodeArtifact, errors: string[]) => {
setIsCanvasOpen(true)
setCodeErrors(prev => [...prev, { artifact, errors }])
}
const clearCodeErrors = (artifact: CodeArtifact) => {
setCodeErrors(prev =>
prev.filter(error => !isEqualArtifact(error.artifact, artifact))
)
}
const getCodeErrors = (artifact: CodeArtifact): string[] => {
const artifactErrors = codeErrors.find(error =>
isEqualArtifact(error.artifact, artifact)
)
const uniqueErrors = Array.from(new Set(artifactErrors?.errors ?? []))
return uniqueErrors
}
const fixCodeErrors = (artifact: CodeArtifact) => {
const errors = getCodeErrors(artifact)
if (errors.length === 0) return
append(
{
role: 'user',
content: `Please fix the following errors: ${errors.join('\n')} happened when running the code.`,
},
{ data: requestData }
)
}
return (
<ChatCanvasContext.Provider
value={{
allArtifacts,
getArtifactsByType,
displayedArtifact,
isCanvasOpen,
openArtifactInCanvas,
closeCanvas,
appendErrors,
clearCodeErrors,
getCodeErrors,
fixCodeErrors,
getArtifactVersion,
restoreArtifact,
}}
>
{children}
</ChatCanvasContext.Provider>
)
}
export function useChatCanvas(): ChatCanvasContextType {
const context = useContext(ChatCanvasContext)
if (context === undefined) {
throw new Error('useChatCanvas must be used within a ChatCanvasProvider')
}
return context
}
@@ -0,0 +1,45 @@
'use client'
import { cn } from '../../lib/utils'
import { ChatCanvasActions } from './actions'
import { CodeArtifactViewer } from './artifacts/code'
import { useChatCanvas } from './context'
import { DocumentArtifactViewer } from './artifacts/document'
interface ChatCanvasProps {
children?: React.ReactNode
className?: string
}
function ChatCanvas({ children, className }: ChatCanvasProps) {
const { isCanvasOpen, displayedArtifact } = useChatCanvas()
if (!isCanvasOpen || !displayedArtifact) return null
return (
<div
className={cn(
'right-0 top-0 flex h-full w-3/5 shrink-0 flex-col border-l bg-white',
className
)}
style={{
animation: isCanvasOpen
? 'slideIn 0.3s ease-out forwards'
: 'slideOut 0.3s ease-out forwards',
}}
>
{children ?? (
<>
<CodeArtifactViewer />
<DocumentArtifactViewer />
</>
)}
</div>
)
}
ChatCanvas.CodeArtifact = CodeArtifactViewer
ChatCanvas.DocumentArtifact = DocumentArtifactViewer
ChatCanvas.Actions = ChatCanvasActions
export default ChatCanvas
@@ -1,3 +1,4 @@
import { useMemo } from 'react'
import {
ChatAgentEvents,
ChatEvents,
@@ -10,6 +11,7 @@ import {
AgentEventData,
DocumentFileData,
EventData,
extractArtifactsFromMessage,
getChatUIAnnotation,
getSourceAnnotationData,
ImageData,
@@ -19,6 +21,7 @@ import {
} from './annotation'
import { useChatMessage } from './chat-message.context.js'
import { useChatUI } from './chat.context.js'
import { ArtifactCard } from './canvas/card.js'
export function EventAnnotations() {
const { message, isLast, isLoading } = useChatMessage()
@@ -112,3 +115,20 @@ export function SuggestedQuestionsAnnotations() {
<SuggestedQuestions questions={suggestedQuestionsData[0]} append={append} />
)
}
export function ArtifactAnnotations() {
const { message } = useChatMessage()
const artifacts = useMemo(
() => extractArtifactsFromMessage(message),
[message]
)
if (!artifacts?.length) return null
return (
<div className="flex items-center gap-2">
{artifacts.map((artifact, index) => (
<ArtifactCard key={index} artifact={artifact} />
))}
</div>
)
}
@@ -7,6 +7,7 @@ import { CitationComponentProps, Markdown } from '../widgets/index.js'
import { getSourceAnnotationData, MessageAnnotation } from './annotation'
import {
AgentEventAnnotations,
ArtifactAnnotations,
DocumentFileAnnotations,
EventAnnotations,
ImageAnnotations,
@@ -119,6 +120,7 @@ function ChatMessageContent(props: ChatMessageContentProps) {
<DocumentFileAnnotations />
<SourceAnnotations />
<SuggestedQuestionsAnnotations />
<ArtifactAnnotations />
</>
)
@@ -204,6 +206,7 @@ type ComposibleChatMessageContent = typeof ChatMessageContent & {
DocumentFile: typeof DocumentFileAnnotations
Source: typeof SourceAnnotations
SuggestedQuestions: typeof SuggestedQuestionsAnnotations
Artifact: typeof ArtifactAnnotations
}
type ComposibleChatMessage = typeof ChatMessage & {
@@ -230,6 +233,7 @@ PrimiviteChatMessage.Content.Markdown = ChatMarkdown
PrimiviteChatMessage.Content.DocumentFile = DocumentFileAnnotations
PrimiviteChatMessage.Content.Source = SourceAnnotations
PrimiviteChatMessage.Content.SuggestedQuestions = SuggestedQuestionsAnnotations
PrimiviteChatMessage.Content.Artifact = ArtifactAnnotations
PrimiviteChatMessage.Avatar = ChatMessageAvatar
PrimiviteChatMessage.Actions = ChatMessageActions
+2 -1
View File
@@ -4,6 +4,7 @@ import ChatInput from './chat-input'
import ChatMessages from './chat-messages'
import { ChatProvider } from './chat.context'
import { type ChatHandler } from './chat.interface'
import { ChatCanvasProvider } from './canvas/context'
export interface ChatSectionProps extends React.PropsWithChildren {
handler: ChatHandler
@@ -24,7 +25,7 @@ export default function ChatSection(props: ChatSectionProps) {
return (
<ChatProvider value={{ ...handler, requestData, setRequestData }}>
<div className={cn('flex h-full w-full flex-col gap-4 p-5', className)}>
{children}
<ChatCanvasProvider>{children}</ChatCanvasProvider>
</div>
</ChatProvider>
)
@@ -27,6 +27,10 @@ export type ChatHandler = {
message: Message,
chatRequestOptions?: { data?: any }
) => Promise<string | null | undefined>
// TODO: (Message & { id: string }) is a quick fix for compatibility with Message from ai/react
// We should make Message type in ChatHandler more flexible. Eg: ChatHandler<T extends Message = Message>
setMessages?: (messages: (Message & { id: string })[]) => void
}
export type ChatContext = ChatHandler & {
+2
View File
@@ -2,12 +2,14 @@
export * from './chat/chat.interface'
export * from './chat/annotation'
export { default as ChatSection } from './chat/chat-section'
export { default as ChatCanvas } from './chat/canvas'
export { default as ChatInput } from './chat/chat-input'
export { default as ChatMessages } from './chat/chat-messages'
export { default as ChatMessage, ContentPosition } from './chat/chat-message'
// Context Provider Hooks
export { useChatUI } from './chat/chat.context'
export { useChatCanvas } from './chat/canvas/context'
export { useChatMessage } from './chat/chat-message.context'
export { useChatInput } from './chat/chat-input'
export { useChatMessages } from './chat/chat-messages'
+1 -1
View File
@@ -1,4 +1,4 @@
@import 'highlight.js/styles/atom-one-dark-reasonable.css';
@import 'highlight.js/styles/atom-one-light.css';
@import 'katex/dist/katex.min.css';
.custom-markdown ul {
+56
View File
@@ -0,0 +1,56 @@
'use client'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import * as React from 'react'
import { cn } from '../lib/utils'
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn('border-b', className)}
{...props}
/>
))
AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-neutral-500 transition-transform duration-200 dark:text-neutral-400" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
+38
View File
@@ -0,0 +1,38 @@
'use client'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { cn } from '../lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
+48
View File
@@ -0,0 +1,48 @@
'use client'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import * as React from 'react'
import { cn } from '../lib/utils'
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground 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-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
+56 -22
View File
@@ -3,11 +3,15 @@ import { Check, Copy, Download } from 'lucide-react'
import { FC, memo, useEffect, useRef } from 'react'
import { Button } from '../ui/button'
import { useCopyToClipboard } from '../hook/use-copy-to-clipboard'
import { cn } from '../lib/utils'
interface Props {
language: string
value: string
className?: string
showHeader?: boolean
headerClassName?: string
codeClassName?: string
}
interface languageMap {
@@ -50,7 +54,15 @@ export const generateRandomString = (length: number, lowercase = false) => {
return lowercase ? result.toLowerCase() : result
}
const CodeBlock: FC<Props> = memo(({ language, value, className }) => {
const CodeBlock: FC<Props> = memo(props => {
const {
language,
value,
className,
headerClassName,
codeClassName,
showHeader = true,
} = props
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
const codeRef = useRef<HTMLElement>(null)
@@ -94,28 +106,50 @@ const CodeBlock: FC<Props> = memo(({ language, value, className }) => {
}
return (
<div
className={`codeblock relative w-full bg-zinc-950 font-sans ${className}`}
>
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
<Button variant="ghost" onClick={downloadAsFile} size="icon">
<Download />
<span className="sr-only">Download</span>
</Button>
<Button variant="ghost" size="icon" onClick={onCopy}>
{isCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
<span className="sr-only">Copy code</span>
</Button>
<div className={cn('codeblock relative w-full', className)}>
{showHeader && (
<div
className={cn(
'absolute left-0 top-0 flex w-full items-center justify-between px-4 py-2',
headerClassName
)}
>
<span className="text-xs lowercase">{language}</span>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
onClick={downloadAsFile}
size="icon"
className="size-8"
>
<Download className="size-4" />
<span className="sr-only">Download</span>
</Button>
<Button
variant="ghost"
size="icon"
onClick={onCopy}
className="size-8"
>
{isCopied ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
<span className="sr-only">Copy code</span>
</Button>
</div>
</div>
</div>
<pre className="border border-zinc-700 text-white">
<code ref={codeRef} className={`language-${language} font-mono`}>
)}
<pre>
<code
ref={codeRef}
className={cn(
`language-${language} rounded-lg border font-mono`,
codeClassName,
{ 'pt-12!': showHeader }
)}
>
{value}
</code>
</pre>
+1
View File
@@ -70,6 +70,7 @@ module.exports = {
'jsx-a11y/anchor-is-valid': 'off',
'no-await-in-loop': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
camelcase: 'off',
},
overrides: [
{
+449
View File
@@ -108,6 +108,9 @@ importers:
'@llamaindex/pdf-viewer':
specifier: 1.3.0
version: 1.3.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-accordion':
specifier: ^1.2.3
version: 1.2.8(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.1.3(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
@@ -117,6 +120,9 @@ importers:
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.2(react@18.3.1)
'@radix-ui/react-popover':
specifier: ^1.1.7
version: 1.1.11(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-progress':
specifier: ^1.1.0
version: 1.1.2(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
@@ -1455,6 +1461,37 @@ packages:
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
dev: false
/@radix-ui/primitive@1.1.2:
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
dev: false
/@radix-ui/react-accordion@1.2.8(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-c7OKBvO36PfQIUGIjj1Wko0hH937pYFU2tR5zbIJDUsmTzHoZVHHt4bmb7OOJbzTaWJtVELKWojBHa7OcnUHmQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-collapsible': 1.1.8(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-collection': 1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-arrow@1.1.2(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==}
peerDependencies:
@@ -1474,6 +1511,25 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-arrow@1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collapsible@1.1.3(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==}
peerDependencies:
@@ -1500,6 +1556,32 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collapsible@1.1.8(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-presence': 1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collection@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
peerDependencies:
@@ -1523,6 +1605,28 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-collection@1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.2.0(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
peerDependencies:
@@ -1536,6 +1640,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-context@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
@@ -1549,6 +1666,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-context@1.1.2(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-dialog@1.1.6(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
peerDependencies:
@@ -1594,6 +1724,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-direction@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-dismissable-layer@1.1.5(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
peerDependencies:
@@ -1617,6 +1760,29 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-dismissable-layer@1.1.7(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
@@ -1630,6 +1796,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-focus-scope@1.1.2(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==}
peerDependencies:
@@ -1651,6 +1830,27 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-focus-scope@1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-hover-card@1.1.6(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==}
peerDependencies:
@@ -1700,6 +1900,53 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-id@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-popover@1.1.11(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-yFMfZkVA5G3GJnBgb2PxrrcLKm1ZLWXrbYVgdyTl//0TYEIHS9LJbnyz7WWcZ0qCq7hIlJZpRtxeSeIG5T5oJw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.7(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-focus-scope': 1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-popper': 1.2.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-portal': 1.1.6(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot': 1.2.0(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
aria-hidden: 1.2.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1)
dev: false
/@radix-ui/react-popper@1.2.2(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
peerDependencies:
@@ -1728,6 +1975,34 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-popper@1.2.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-arrow': 1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/rect': 1.1.1
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-portal@1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
peerDependencies:
@@ -1748,6 +2023,26 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-portal@1.1.6(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-primitive': 2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
peerDependencies:
@@ -1769,6 +2064,26 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-presence@1.1.4(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-primitive@2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==}
peerDependencies:
@@ -1789,6 +2104,25 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-primitive@2.1.0(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-slot': 1.2.0(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-progress@1.1.2(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
peerDependencies:
@@ -1890,6 +2224,20 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-slot@1.2.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-tabs@1.1.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==}
peerDependencies:
@@ -1930,6 +2278,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
@@ -1944,6 +2305,35 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
peerDependencies:
@@ -1958,6 +2348,20 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies:
@@ -1971,6 +2375,19 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-use-previous@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
peerDependencies:
@@ -1998,6 +2415,20 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-use-rect@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/rect': 1.1.1
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-use-size@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
peerDependencies:
@@ -2012,6 +2443,20 @@ packages:
react: 18.3.1
dev: false
/@radix-ui/react-use-size@1.1.1(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@types/react': 18.3.18
react: 18.3.1
dev: false
/@radix-ui/react-visually-hidden@1.1.2(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
peerDependencies:
@@ -2035,6 +2480,10 @@ packages:
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
dev: false
/@radix-ui/rect@1.1.1:
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
dev: false
/@rollup/rollup-android-arm-eabi@4.34.8:
resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==}
cpu: [arm]