refactor: cleaned annotations (#106)

* refactor: cleaned annotations
---------

Co-authored-by: thucpn <thucsh2@gmail.com>
This commit is contained in:
Marcus Schiesser
2025-06-02 12:27:32 +07:00
committed by GitHub
parent a43af901f4
commit 8e60c05b50
31 changed files with 403 additions and 466 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@llamaindex/chat-ui': patch
'@llamaindex/chat-ui-docs': patch
---
Cleaned up internal annotation retrieval
+11 -63
View File
@@ -449,7 +449,7 @@ const weatherAnnotation: WeatherAnnotation = {
### Custom Widget Implementation
```tsx
import { useChatMessage, getCustomAnnotations } from '@llamaindex/chat-ui'
import { useChatMessage, getAnnotationData } from '@llamaindex/chat-ui'
interface WeatherData {
location: string
@@ -459,79 +459,27 @@ interface WeatherData {
windSpeed: number
}
export function CustomWeatherWidget() {
function WeatherWidget() {
const { message } = useChatMessage()
const weatherData = getCustomAnnotations<WeatherData>(
message.annotations,
'weather'
)
const weatherData = getAnnotationData<WeatherData>(message, 'weather')
if (!weatherData[0]) return null
if (!weatherData?.[0]) return null
const data = weatherData[0]
return (
<div className="my-4 rounded-lg border border-blue-200 bg-blue-50 p-4">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
<WeatherIcon condition={data.condition} />
</div>
<div className="flex-1">
<h3 className="font-semibold text-blue-900">{data.location}</h3>
<div className="flex items-center gap-4 text-sm text-blue-700">
<span className="text-2xl font-bold">{data.temperature}°C</span>
<span>{data.condition}</span>
</div>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-4 text-sm text-blue-600">
<div className="flex items-center gap-2">
<span>💧 Humidity:</span>
<span className="font-medium">{data.humidity}%</span>
</div>
<div className="flex items-center gap-2">
<span>🌬️ Wind:</span>
<span className="font-medium">{data.windSpeed} km/h</span>
</div>
</div>
</div>
)
}
function WeatherIcon({ condition }: { condition: string }) {
const iconMap: Record<string, string> = {
sunny: '☀️',
cloudy: '☁️',
rainy: '🌧️',
snowy: '❄️',
stormy: '⛈️',
}
return (
<span className="text-2xl">{iconMap[condition.toLowerCase()] || '🌤️'}</span>
)
// Render weather data...
}
```
### Using Custom Widgets
### getAnnotationData
Extract annotation data by type from messages:
```tsx
import { ChatMessage } from '@llamaindex/chat-ui'
import { CustomWeatherWidget } from './custom-weather-widget'
import { getAnnotationData } from '@llamaindex/chat-ui'
function MessageWithCustomWidgets({ message }) {
return (
<ChatMessage message={message}>
<ChatMessage.Content>
<ChatMessage.Content.Markdown />
<ChatMessage.Content.Image />
<ChatMessage.Content.Source />
<CustomWeatherWidget /> {/* Add custom widget */}
</ChatMessage.Content>
</ChatMessage>
)
}
// Usage
return getAnnotationData<WeatherData>(message, 'weather')
```
## Annotation Utilities
+69 -72
View File
@@ -15,22 +15,20 @@ Renders rich text with LaTeX math support, syntax highlighting, and citations.
import { Markdown } from '@llamaindex/chat-ui/widgets'
function RichText({ content }) {
return (
<Markdown>
{content}
</Markdown>
)
return <Markdown>{content}</Markdown>
}
```
**Features:**
- **LaTeX Math** - Inline and block math rendering with KaTeX
- **Code Highlighting** - Syntax highlighting with highlight.js
- **Citations** - Clickable citation links
- **Custom Renderers** - Extensible rendering pipeline
**Example Content:**
```markdown
````markdown
# Mathematical Formula
The quadratic formula is: $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$
@@ -43,11 +41,13 @@ def fibonacci(n):
return n
return fibonacci(n-1) + fibonacci(n-2)
```
````
This algorithm has O(2^n) complexity [^1].
[^1]: Introduction to Algorithms, Chapter 15
```
````
### CodeBlock
@@ -66,9 +66,10 @@ function CodeDisplay({ code, language, filename }) {
/>
)
}
```
````
**Props:**
- `code` - Source code string
- `language` - Programming language for highlighting
- `filename` - Optional filename display
@@ -95,14 +96,16 @@ function InteractiveCode({ initialCode, language, onChange }) {
```
**Supported Languages:**
- JavaScript/TypeScript
- Python
- Python
- CSS/SCSS
- HTML
- JSON
- Markdown
**Features:**
- **Syntax Highlighting** - Real-time highlighting
- **Auto-completion** - Language-aware suggestions
- **Code Folding** - Collapse code blocks
@@ -129,6 +132,7 @@ function DocumentEdit({ content, onChange, title }) {
```
**Features:**
- **WYSIWYG Editing** - Visual editing with markdown output
- **Formatting Tools** - Bold, italic, lists, headers
- **Link Support** - Insert and edit links
@@ -149,16 +153,17 @@ const imageAnnotation = {
type: 'IMAGE',
data: {
url: 'https://example.com/image.jpg',
alt: 'Description of the image'
}
alt: 'Description of the image',
},
}
function ImageDisplay() {
return <ChatImage /> // Automatically renders from context
return <ChatImage /> // Automatically renders from context
}
```
**Features:**
- **Zoom & Pan** - Interactive image viewing
- **Lazy Loading** - Performance optimization
- **Alt Text** - Accessibility support
@@ -181,18 +186,19 @@ const fileAnnotation = {
name: 'report.pdf',
type: 'application/pdf',
url: '/files/report.pdf',
size: 1024000
}
]
}
size: 1024000,
},
],
},
}
function FileDisplay() {
return <ChatFiles /> // Automatically renders from context
return <ChatFiles /> // Automatically renders from context
}
```
**Supported File Types:**
- **PDF** - Inline viewer
- **Images** - Thumbnail preview
- **Text Files** - Content preview
@@ -216,19 +222,20 @@ const sourceAnnotation = {
url: '/documents/paper.pdf',
metadata: {
title: 'Research Paper',
page_number: 5
}
}
]
}
page_number: 5,
},
},
],
},
}
function SourceDisplay() {
return <ChatSources /> // Automatically renders from context
return <ChatSources /> // Automatically renders from context
}
```
**Features:**
- **Document Grouping** - Groups citations by document
- **Page Numbers** - Shows specific page references
- **Click to View** - Opens source documents
@@ -249,13 +256,13 @@ const eventAnnotation = {
type: 'function_call',
name: 'search_documents',
args: { query: 'machine learning' },
result: 'Found 15 relevant documents'
}
]
result: 'Found 15 relevant documents',
},
],
}
function EventDisplay() {
return <ChatEvents /> // Automatically renders from context
return <ChatEvents /> // Automatically renders from context
}
```
@@ -276,13 +283,13 @@ const agentAnnotation = {
events: [
{ step: 'Searching', status: 'completed' },
{ step: 'Analyzing', status: 'in_progress' },
{ step: 'Summarizing', status: 'pending' }
]
}
{ step: 'Summarizing', status: 'pending' },
],
},
}
function AgentEventDisplay() {
return <ChatAgentEvents /> // Automatically renders from context
return <ChatAgentEvents /> // Automatically renders from context
}
```
@@ -300,13 +307,13 @@ const suggestionsAnnotation = {
questions: [
'Can you explain this in more detail?',
'What are the practical applications?',
'How does this compare to other approaches?'
]
}
'How does this compare to other approaches?',
],
},
}
function QuestionSuggestions() {
return <SuggestedQuestions /> // Automatically renders from context
return <SuggestedQuestions /> // Automatically renders from context
}
```
@@ -321,7 +328,7 @@ const starterQuestions = [
'How can I improve my code?',
'Explain machine learning concepts',
'Help me debug this error',
'What are best practices for React?'
'What are best practices for React?',
]
function ChatStarters() {
@@ -349,7 +356,7 @@ function UploadArea({ onFilesSelected }) {
accept={{
'image/*': ['.png', '.jpg', '.jpeg'],
'application/pdf': ['.pdf'],
'text/*': ['.txt', '.md']
'text/*': ['.txt', '.md'],
}}
maxSize={10 * 1024 * 1024} // 10MB
onFiles={onFilesSelected}
@@ -364,6 +371,7 @@ function UploadArea({ onFilesSelected }) {
```
**Features:**
- **Drag & Drop** - Intuitive file selection
- **File Validation** - Type and size checking
- **Multiple Files** - Batch upload support
@@ -444,9 +452,9 @@ function MessageWithWidgets({ message }) {
<ChatMessage message={message}>
<ChatMessage.Content>
<ChatMessage.Content.Markdown />
<ChatMessage.Content.Image /> {/* Renders ChatImage */}
<ChatMessage.Content.Source /> {/* Renders ChatSources */}
<ChatMessage.Content.Event /> {/* Renders ChatEvents */}
<ChatMessage.Content.Image /> {/* Renders ChatImage */}
<ChatMessage.Content.Source /> {/* Renders ChatSources */}
<ChatMessage.Content.Event /> {/* Renders ChatEvents */}
</ChatMessage.Content>
</ChatMessage>
)
@@ -458,35 +466,25 @@ function MessageWithWidgets({ message }) {
Use widgets independently for custom layouts:
```tsx
import {
Markdown,
CodeBlock,
import {
Markdown,
CodeBlock,
ChatImage,
SuggestedQuestions
SuggestedQuestions,
} from '@llamaindex/chat-ui/widgets'
function CustomMessageLayout({ message }) {
return (
<div className="space-y-4">
<Markdown>{message.content}</Markdown>
{message.code && (
<CodeBlock
code={message.code}
language="python"
/>
)}
{message.code && <CodeBlock code={message.code} language="python" />}
{message.imageUrl && (
<ChatImage
src={message.imageUrl}
alt="Generated image"
/>
<ChatImage src={message.imageUrl} alt="Generated image" />
)}
<SuggestedQuestions
questions={message.suggestions}
/>
<SuggestedQuestions questions={message.suggestions} />
</div>
)
}
@@ -497,26 +495,25 @@ function CustomMessageLayout({ message }) {
Create custom widgets by following the pattern:
```tsx
import { useChatMessage, getCustomAnnotations } from '@llamaindex/chat-ui'
import { useChatMessage, getAnnotationData } from '@llamaindex/chat-ui'
interface WeatherData {
location: string
temperature: number
condition: string
humidity: number
windSpeed: number
}
function CustomWeatherWidget() {
export function CustomWeatherWidget() {
const { message } = useChatMessage()
const weatherData = getCustomAnnotations<WeatherData>(
message.annotations,
'weather'
)
if (!weatherData[0]) return null
const weatherData = getAnnotationData<WeatherData>(message, 'weather')
if (!weatherData?.[0]) return null
const data = weatherData[0]
return (
<div className="rounded-lg bg-blue-50 p-4">
<h3 className="font-semibold">{data.location}</h3>
@@ -532,4 +529,4 @@ function CustomWeatherWidget() {
- [Annotations](./annotations.mdx) - Learn how to create and send annotation data
- [Artifacts](./artifacts.mdx) - Implement interactive code and document artifacts
- [Hooks](./hooks.mdx) - Understand the widget hook system
- [Customization](./customization.mdx) - Style and customize widget appearance
- [Customization](./customization.mdx) - Style and customize widget appearance
@@ -1,6 +1,6 @@
'use client'
import { useChatMessage, getCustomAnnotations } from '@llamaindex/chat-ui'
import { useChatMessage, getAnnotationData } from '@llamaindex/chat-ui'
interface WeatherData {
location: string
@@ -13,12 +13,9 @@ interface WeatherData {
export function CustomWeatherAnnotation() {
const { message } = useChatMessage()
const weatherData = getCustomAnnotations<WeatherData>(
message.annotations,
'weather'
)
const weatherData = getAnnotationData<WeatherData>(message, 'weather')
if (!weatherData[0]) return null
if (!weatherData?.[0]) return null
const data = weatherData[0]
@@ -1,6 +1,6 @@
'use client'
import { useChatMessage, getCustomAnnotations } from '@llamaindex/chat-ui'
import { useChatMessage, getAnnotationData } from '@llamaindex/chat-ui'
interface WeatherData {
location: string
@@ -13,12 +13,9 @@ interface WeatherData {
export function CustomWeatherAnnotation() {
const { message } = useChatMessage()
const weatherData = getCustomAnnotations<WeatherData>(
message.annotations,
'weather'
)
const weatherData = getAnnotationData<WeatherData>(message, 'weather')
if (!weatherData[0]) return null
if (!weatherData?.[0]) return null
const data = weatherData[0]
-225
View File
@@ -1,225 +0,0 @@
import { JSONValue, Message } from './chat.interface'
export enum MessageAnnotationType {
IMAGE = 'image',
DOCUMENT_FILE = 'document_file',
SOURCES = 'sources',
EVENTS = 'events',
SUGGESTED_QUESTIONS = 'suggested_questions',
AGENT_EVENTS = 'agent',
ARTIFACT = 'artifact',
}
export type ImageData = {
url: string
}
export type DocumentFileType = 'csv' | 'pdf' | 'txt' | 'docx'
export const DOCUMENT_FILE_TYPES: DocumentFileType[] = [
'csv',
'pdf',
'txt',
'docx',
]
export type DocumentFile = {
id: string
name: string // The uploaded file name in the backend
size: number // The file size in bytes
type: DocumentFileType
url: string // The URL of the uploaded file in the backend
refs?: string[] // DocumentIDs of the uploaded file in the vector index
}
export type DocumentFileData = {
files: DocumentFile[]
}
export type SourceNode = {
id: string
metadata: Record<string, unknown>
score?: number
text: string
url?: string
}
export type SourceData = {
nodes: SourceNode[]
}
export type EventData = {
title: string
}
export type ProgressData = {
id: string
total: number
current: number
}
export type AgentEventData = {
agent: string
text: string
type: 'text' | 'progress'
data?: ProgressData
}
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
| SourceData
| EventData
| AgentEventData
| SuggestedQuestionsData
| Artifact
export type MessageAnnotation = {
type: MessageAnnotationType
data: AnnotationData
}
export type CustomAnnotation<T = unknown> = {
type: string
data: T
}
export type AnyAnnotation<T = unknown> = MessageAnnotation | CustomAnnotation<T>
/**
* Gets custom message annotations that don't match any standard MessageAnnotationType
* @param annotations - Array of message annotations to filter
* @param filter - Optional custom filter function to apply after filtering out standard annotations
* @returns Filtered array of custom message annotations
*
* First filters out any annotations that match MessageAnnotationType values,
* then applies the optional custom filter if provided.
* @deprecated Use getCustomAnnotations instead
*/
export function getCustomAnnotation<T = JSONValue>(
annotations: JSONValue[] | undefined,
filterFn?: (a: T) => boolean
): T[] {
if (!Array.isArray(annotations) || !annotations.length) return [] as T[]
const customAnnotations = annotations.filter(
a => !isSupportedAnnotation(a)
) as T[]
return filterFn ? customAnnotations.filter(filterFn) : customAnnotations
}
function isSupportedAnnotation(a: JSONValue): boolean {
return (
typeof a === 'object' &&
a !== null &&
a !== undefined &&
'type' in a &&
'data' in a &&
Object.values(MessageAnnotationType).includes(
a.type as MessageAnnotationType
)
)
}
export function getChatUIAnnotation<T extends AnnotationData>(
annotations: MessageAnnotation[],
type: string
): T[] {
if (!annotations?.length) return []
return annotations
.filter(a => a && 'type' in a && a.type.toString() === type)
.map(a => a.data as T)
}
/**
* Gets custom annotations by type from message annotations
* @param annotations - Array of message annotations (can be undefined)
* @param type - The annotation type to filter by
* @returns Array of data from custom annotations of the specified type
*/
export function getCustomAnnotations<T = unknown>(
annotations: JSONValue[] | undefined,
type: string
): T[] {
if (!annotations?.length) return []
return annotations
.filter(
a =>
a &&
typeof a === 'object' &&
a !== null &&
'type' in a &&
a.type === type &&
'data' in a
)
.map(a => (a as CustomAnnotation<T>).data)
}
export function getSourceAnnotationData(
annotations: MessageAnnotation[]
): SourceData[] {
const data = getChatUIAnnotation<SourceData>(
annotations,
MessageAnnotationType.SOURCES
)
if (!data.length) return []
return data.map(item => ({
...item,
nodes: item.nodes ? preprocessSourceNodes(item.nodes) : [],
}))
}
function preprocessSourceNodes(nodes: SourceNode[]): SourceNode[] {
// Filter source nodes has lower score
const processedNodes = nodes.map(node => {
// remove trailing slash for node url if exists
if (node.url) {
node.url = node.url.replace(/\/$/, '')
}
return node
})
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,37 @@
import { Message } from '../chat.interface'
// TODO: add different methods to retrieve annotations from a message - e.g. by parsing its content for inline annotations
export type MessageAnnotation<T = unknown> = {
type: string
data: T
}
/**
* Gets annotation data directly from a message by type
* @param message - The message to extract annotations from
* @param type - The annotation type to filter by (can be standard or custom)
* @returns Array of data from annotations of the specified type, or null if none found
*/
export function getAnnotationData<T = unknown>(
message: Message,
type: string
): T[] | null {
const annotations = message.annotations
if (!annotations) return []
const matchingAnnotations = annotations
.filter(
a =>
a &&
typeof a === 'object' &&
a !== null &&
'type' in a &&
a.type === type &&
'data' in a
)
.map(a => (a as { data: T }).data)
return matchingAnnotations
}
@@ -0,0 +1,43 @@
import { Message } from '../chat.interface'
import { MessageAnnotationType } from './data'
import { getAnnotationData } from './annotations'
// 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
}
// 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)
}
export function extractArtifactsFromMessage(message: Message): Artifact[] {
const artifacts = getAnnotationData<Artifact>(
message,
MessageAnnotationType.ARTIFACT
)
return artifacts ?? []
}
export type CodeArtifactError = {
artifact: CodeArtifact
errors: 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
}>
@@ -0,0 +1,9 @@
export enum MessageAnnotationType {
IMAGE = 'image',
DOCUMENT_FILE = 'document_file',
SOURCES = 'sources',
EVENTS = 'events',
SUGGESTED_QUESTIONS = 'suggested_questions',
AGENT_EVENTS = 'agent',
ARTIFACT = 'artifact',
}
@@ -0,0 +1,4 @@
export * from './annotations'
export * from './data'
export * from './artifacts'
export * from './sources'
@@ -0,0 +1,37 @@
import { Message } from '../chat.interface'
import { MessageAnnotationType } from './data'
import { getAnnotationData } from './annotations'
export function preprocessSourceNodes(nodes: SourceNode[]): SourceNode[] {
// Filter source nodes has lower score
const processedNodes = nodes.map(node => {
// remove trailing slash for node url if exists
if (node.url) {
node.url = node.url.replace(/\/$/, '')
}
return node
})
return processedNodes
}
export function getSourceAnnotationData(message: Message): SourceData[] {
const data = getAnnotationData<SourceData>(
message,
MessageAnnotationType.SOURCES
)
if (!data?.length) return []
return data.map(item => ({
...item,
nodes: item.nodes ? preprocessSourceNodes(item.nodes) : [],
}))
}
export type SourceNode = {
id: string
metadata: Record<string, unknown>
score?: number
text: string
url?: string
}
export type SourceData = {
nodes: SourceNode[]
}
+3 -3
View File
@@ -7,13 +7,13 @@ import { cn } from '../../lib/utils'
import { Badge } from '../../ui/badge'
import { Button } from '../../ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'
import { useChatCanvas } from './context'
import {
Artifact,
CodeArtifact,
DocumentArtifact,
CodeArtifact,
isEqualArtifact,
} from '../annotation'
import { useChatCanvas } from './context'
} from '../annotations/artifacts'
interface ChatCanvasActionsProps {
children?: React.ReactNode
@@ -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 {
DocumentArtifact,
CodeArtifact,
isEqualArtifact,
Artifact,
} from '../annotations/artifacts'
import { useChatCanvas } from './context'
const IconMap: Record<Artifact['type'], LucideIcon> = {
code: FileCode,
document: FileText,
}
export function ArtifactCard({ data }: { data: Artifact }) {
const {
openArtifactInCanvas,
getArtifactVersion,
restoreArtifact,
displayedArtifact,
} = useChatCanvas()
const { versionNumber, isLatest } = getArtifactVersion(data)
const Icon = IconMap[data.type]
const title = getCardTitle(data)
const isDisplayed =
displayedArtifact && isEqualArtifact(data, 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(data)}
>
<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(data)
}}
>
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 ''
}
@@ -5,7 +5,7 @@ import { cn } from '../../../lib/utils'
import { Button } from '../../../ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../ui/tabs'
import { CodeEditor, fileExtensionToEditorLang } from '../../../widgets'
import { CodeArtifact } from '../../annotation'
import { CodeArtifact } from '../../annotations/artifacts'
import { ChatCanvasActions } from '../actions'
import { useChatCanvas } from '../context'
@@ -3,7 +3,7 @@
import { FileText } from 'lucide-react'
import { cn } from '../../../lib/utils'
import { DocumentEditor } from '../../../widgets'
import { DocumentArtifact } from '../../annotation'
import { DocumentArtifact } from '../../annotations/artifacts'
import { ChatCanvasActions } from '../actions'
import { useChatCanvas } from '../context'
import { useState } from 'react'
+3 -3
View File
@@ -3,11 +3,11 @@ import { cn } from '../../lib/utils'
import { Badge } from '../../ui/badge'
import { Button } from '../../ui/button'
import {
isEqualArtifact,
DocumentArtifact,
Artifact,
CodeArtifact,
DocumentArtifact,
isEqualArtifact,
} from '../annotation'
} from '../annotations/artifacts'
import { useChatCanvas } from './context'
const IconMap: Record<Artifact['type'], LucideIcon> = {
+1 -1
View File
@@ -16,7 +16,7 @@ import {
extractArtifactsFromAllMessages,
isEqualArtifact,
DocumentArtifact,
} from '../annotation'
} from '../annotations/artifacts'
import { Message } from '../chat.interface'
import { useChatUI } from '../chat.context'
+27 -51
View File
@@ -5,20 +5,17 @@ import {
ChatFiles,
ChatImage,
ChatSources,
EventData,
ImageData,
DocumentFileData,
AgentEventData,
SuggestedQuestionsData,
SuggestedQuestions,
} from '../widgets/index.js' // this import needs the file extension as it's importing the widget bundle
import {
AgentEventData,
DocumentFileData,
EventData,
extractArtifactsFromMessage,
getChatUIAnnotation,
getSourceAnnotationData,
ImageData,
MessageAnnotation,
MessageAnnotationType,
SuggestedQuestionsData,
} from './annotation'
import { MessageAnnotationType } from './annotations/data.js'
import { getAnnotationData } from './annotations/annotations.js'
import { getSourceAnnotationData } from './annotations/sources.js'
import { extractArtifactsFromMessage } from './annotations/artifacts.js'
import { useChatMessage } from './chat-message.context.js'
import { useChatUI } from './chat.context.js'
import { ArtifactCard } from './canvas/card.js'
@@ -27,14 +24,10 @@ export function EventAnnotations() {
const { message, isLast, isLoading } = useChatMessage()
const showLoading = (isLast && isLoading) ?? false
const annotations = message.annotations as MessageAnnotation[] | undefined
const eventData =
annotations && annotations.length > 0
? getChatUIAnnotation<EventData>(
annotations,
MessageAnnotationType.EVENTS
)
: null
const eventData = getAnnotationData<EventData>(
message,
MessageAnnotationType.EVENTS
)
if (!eventData?.length) return null
return <ChatEvents data={eventData} showLoading={showLoading} />
}
@@ -42,14 +35,10 @@ export function EventAnnotations() {
export function AgentEventAnnotations() {
const { message, isLast } = useChatMessage()
const annotations = message.annotations as MessageAnnotation[] | undefined
const agentEventData =
annotations && annotations.length > 0
? getChatUIAnnotation<AgentEventData>(
annotations,
MessageAnnotationType.AGENT_EVENTS
)
: null
const agentEventData = getAnnotationData<AgentEventData>(
message,
MessageAnnotationType.AGENT_EVENTS
)
if (!agentEventData?.length) return null
return (
<ChatAgentEvents
@@ -63,11 +52,7 @@ export function AgentEventAnnotations() {
export function ImageAnnotations() {
const { message } = useChatMessage()
const annotations = message.annotations as MessageAnnotation[] | undefined
const imageData =
annotations && annotations.length > 0
? getChatUIAnnotation<ImageData>(annotations, 'image')
: null
const imageData = getAnnotationData<ImageData>(message, 'image')
if (!imageData) return null
return imageData[0] ? <ChatImage data={imageData[0]} /> : null
}
@@ -75,14 +60,10 @@ export function ImageAnnotations() {
export function DocumentFileAnnotations() {
const { message } = useChatMessage()
const annotations = message.annotations as MessageAnnotation[] | undefined
const contentFileData =
annotations && annotations.length > 0
? getChatUIAnnotation<DocumentFileData>(
annotations,
MessageAnnotationType.DOCUMENT_FILE
)
: null
const contentFileData = getAnnotationData<DocumentFileData>(
message,
MessageAnnotationType.DOCUMENT_FILE
)
if (!contentFileData) return null
return contentFileData[0] ? <ChatFiles data={contentFileData[0]} /> : null
}
@@ -90,8 +71,7 @@ export function DocumentFileAnnotations() {
export function SourceAnnotations() {
const { message } = useChatMessage()
const annotations = (message.annotations ?? []) as MessageAnnotation[]
const sourceData = getSourceAnnotationData(annotations)
const sourceData = getSourceAnnotationData(message)
if (!sourceData?.length) return null
const allNodes = sourceData.flatMap(item => item.nodes)
@@ -103,14 +83,10 @@ export function SuggestedQuestionsAnnotations() {
const { message, isLast } = useChatMessage()
if (!isLast || !append) return null
const annotations = message.annotations as MessageAnnotation[] | undefined
const suggestedQuestionsData =
annotations && annotations.length > 0
? getChatUIAnnotation<SuggestedQuestionsData>(
annotations,
MessageAnnotationType.SUGGESTED_QUESTIONS
)
: null
const suggestedQuestionsData = getAnnotationData<SuggestedQuestionsData>(
message,
MessageAnnotationType.SUGGESTED_QUESTIONS
)
if (!suggestedQuestionsData?.[0]) return null
return (
<SuggestedQuestions
+3 -4
View File
@@ -8,7 +8,7 @@ import {
Markdown,
LanguageRendererProps,
} from '../widgets/index.js'
import { getSourceAnnotationData, MessageAnnotation } from './annotation'
import { getSourceAnnotationData } from './annotations/sources.js'
import {
AgentEventAnnotations,
ArtifactAnnotations,
@@ -138,12 +138,11 @@ function ChatMessageContent(props: ChatMessageContentProps) {
function ChatMarkdown(props: ChatMarkdownProps) {
const { message } = useChatMessage()
const annotations = message.annotations as MessageAnnotation[] | undefined
const allNodes = useMemo(() => {
const sourceData = getSourceAnnotationData(annotations ?? [])
const sourceData = getSourceAnnotationData(message)
return sourceData.flatMap(item => item.nodes)
}, [annotations])
}, [message])
return (
<Markdown
+1 -1
View File
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import { DocumentFileType, DocumentFile } from '../chat/annotation'
import { DocumentFileType, DocumentFile } from '../widgets'
const docMineTypeMap: Record<string, DocumentFileType> = {
'text/csv': 'csv',
+1 -1
View File
@@ -1,6 +1,6 @@
// Chat components
export * from './chat/chat.interface'
export * from './chat/annotation'
export * from './chat/annotations'
export { default as ChatSection } from './chat/chat-section'
export { default as ChatCanvas } from './chat/canvas'
export { default as ChatInput } from './chat/chat-input'
@@ -10,7 +10,6 @@ import {
DrawerTrigger,
} from '../ui/drawer'
import { Markdown } from './markdown'
import { AgentEventData, ProgressData } from '../chat/annotation'
import { Progress } from '../ui/progress'
const AgentIcons: Record<string, LucideIcon> = {
@@ -36,6 +35,18 @@ type MergedEvent = {
steps: (StepText | StepProgress)[]
}
export type ProgressData = {
id: string
total: number
current: number
}
export type AgentEventData = {
agent: string
text: string
type: 'text' | 'progress'
data?: ProgressData
}
export function ChatAgentEvents({
data,
isFinished,
+4 -1
View File
@@ -6,7 +6,10 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '../ui/collapsible'
import { EventData } from '../chat/annotation'
export type EventData = {
title: string
}
export function ChatEvents({
data,
+5 -2
View File
@@ -1,5 +1,8 @@
import { DocumentFileData } from '../chat/annotation'
import { DocumentInfo } from './document-info'
import { DocumentInfo, DocumentFile } from './document-info'
export type DocumentFileData = {
files: DocumentFile[]
}
export function ChatFiles({ data }: { data: DocumentFileData }) {
if (!data.files.length) return null
+3 -1
View File
@@ -1,4 +1,6 @@
import { type ImageData } from '../index'
export type ImageData = {
url: string
}
export function ChatImage({ data }: { data: ImageData }) {
return (
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { SourceData, SourceNode } from '../chat/annotation'
import { SourceData, SourceNode } from '../chat/annotations/sources'
import { DocumentInfo } from './document-info'
type Document = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { SourceNode } from '../chat/annotation'
import { SourceNode } from '../chat/annotations/sources'
import { SourceNumberButton } from './source-number-button'
export interface CitationComponentProps {
+17 -1
View File
@@ -4,7 +4,7 @@ import {
HoverCardTrigger,
} from '@radix-ui/react-hover-card'
import { Check, Copy, FileIcon, XCircleIcon } from 'lucide-react'
import { DocumentFileType, SourceNode } from '../chat/annotation'
import { SourceNode } from '../chat/annotations/sources'
import { useCopyToClipboard } from '../hook/use-copy-to-clipboard'
import { Button } from '../ui/button'
import { PdfDialog } from './pdf-dialog'
@@ -16,6 +16,22 @@ import { PDFIcon } from '../ui/icons/pdf'
import { SheetIcon } from '../ui/icons/sheet'
import { TxtIcon } from '../ui/icons/txt'
export type DocumentFileType = 'csv' | 'pdf' | 'txt' | 'docx'
export const DOCUMENT_FILE_TYPES: DocumentFileType[] = [
'csv',
'pdf',
'txt',
'docx',
]
export type DocumentFile = {
id: string
name: string // The uploaded file name in the backend
size: number // The file size in bytes
type: DocumentFileType
url: string // The URL of the uploaded file in the backend
refs?: string[] // DocumentIDs of the uploaded file in the vector index
}
type Document = {
url: string
sources: SourceNode[]
+16 -16
View File
@@ -1,17 +1,17 @@
// Other useful components
export { ChatAgentEvents } from './chat-agent-events'
export { ChatEvents } from './chat-events'
export { ChatFiles } from './chat-files'
export { ChatImage } from './chat-image'
export { ChatSources } from './chat-sources'
export { Markdown, type LanguageRendererProps } from './markdown'
export { CodeBlock } from './codeblock'
export { PdfDialog } from './pdf-dialog'
export { SuggestedQuestions } from './suggested-questions'
export { StarterQuestions } from './starter-questions'
export { DocumentInfo } from './document-info'
export { ImagePreview } from './image-preview'
export { FileUploader } from './file-uploader'
export { Citation, type CitationComponentProps } from './citation'
export { CodeEditor, fileExtensionToEditorLang } from './code-editor'
export { DocumentEditor } from './document-editor'
export * from './chat-agent-events'
export * from './chat-events'
export * from './chat-files'
export * from './chat-image'
export * from './chat-sources'
export * from './markdown'
export * from './codeblock'
export * from './pdf-dialog'
export * from './suggested-questions'
export * from './starter-questions'
export * from './document-info'
export * from './image-preview'
export * from './file-uploader'
export * from './citation'
export * from './code-editor'
export * from './document-editor'
+3 -3
View File
@@ -7,9 +7,9 @@ import { CodeBlock } from './codeblock'
import {
DOCUMENT_FILE_TYPES,
DocumentFileType,
SourceData,
} from '../chat/annotation'
import { DocumentInfo } from './document-info'
DocumentInfo,
} from './document-info'
import { SourceData } from '../chat/annotations/sources'
import { Citation, CitationComponentProps } from './citation'
import { cn } from '../lib/utils'
@@ -1,6 +1,7 @@
import { SuggestedQuestionsData } from '../chat/annotation'
import { ChatHandler } from '../chat/chat.interface'
export type SuggestedQuestionsData = string[]
export function SuggestedQuestions({
questions,
append,