mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73b4a78487 |
No files matched your search
@@ -0,0 +1,188 @@
|
||||
# Message Elements
|
||||
|
||||
PraisonAIUI supports rich inline message elements for displaying images, PDFs, videos, audio, files, and code blocks within chat messages. This provides Chainlit-style element rendering parity for seamless migration.
|
||||
|
||||
## Element Types
|
||||
|
||||
### 1. Image Elements
|
||||
|
||||
Display images with thumbnail and click-to-zoom functionality.
|
||||
|
||||
```python
|
||||
from praisonaiui import Message
|
||||
|
||||
msg = Message("Check out this chart:")
|
||||
msg.add_image(
|
||||
url="https://example.com/chart.png",
|
||||
name="Sales Chart",
|
||||
alt="Monthly sales chart showing growth",
|
||||
display="inline" # or "side", "page"
|
||||
)
|
||||
await msg.send()
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `url` (required): Image URL
|
||||
- `name` (optional): Display name
|
||||
- `alt` (optional): Alt text for accessibility
|
||||
- `display` (optional): Display mode - "inline" (default), "side", "page"
|
||||
- `width` (optional): Fixed width in pixels
|
||||
- `height` (optional): Fixed height in pixels
|
||||
|
||||
### 2. PDF Elements
|
||||
|
||||
Display PDF documents with inline viewer or link to open in new tab.
|
||||
|
||||
```python
|
||||
msg.add_pdf(
|
||||
url="https://example.com/report.pdf",
|
||||
name="Quarterly Report",
|
||||
display="inline" # Shows PDF viewer inline
|
||||
)
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `url` (required): PDF URL
|
||||
- `name` (optional): Display name
|
||||
- `display` (optional):
|
||||
- `"inline"`: Embedded PDF viewer (600px height)
|
||||
- `"side"`: Smaller embedded viewer (400px height)
|
||||
- `"page"`: Link to open in new tab
|
||||
|
||||
### 3. Video Elements
|
||||
|
||||
Display videos with native HTML5 video player.
|
||||
|
||||
```python
|
||||
msg.add_video(
|
||||
url="https://example.com/demo.mp4",
|
||||
name="Product Demo",
|
||||
controls=True, # Show video controls
|
||||
autoplay=False # Don't autoplay
|
||||
)
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `url` (required): Video URL
|
||||
- `name` (optional): Display name
|
||||
- `controls` (optional): Show video controls (default: True)
|
||||
- `autoplay` (optional): Auto-play video (default: False)
|
||||
- `loop` (optional): Loop video (default: False)
|
||||
|
||||
### 4. Audio Elements
|
||||
|
||||
Display audio with native HTML5 audio player.
|
||||
|
||||
```python
|
||||
msg.add_audio(
|
||||
url="https://example.com/clip.mp3",
|
||||
name="Audio Clip",
|
||||
controls=True
|
||||
)
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `url` (required): Audio URL
|
||||
- `name` (optional): Display name
|
||||
- `controls` (optional): Show audio controls (default: True)
|
||||
- `autoplay` (optional): Auto-play audio (default: False)
|
||||
- `loop` (optional): Loop audio (default: False)
|
||||
|
||||
### 5. File Elements
|
||||
|
||||
Display downloadable files with file type icons and size information.
|
||||
|
||||
```python
|
||||
msg.add_file(
|
||||
url="https://example.com/data.csv",
|
||||
name="Data Export",
|
||||
size=1024000, # Size in bytes
|
||||
mime_type="text/csv"
|
||||
)
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `url` (required): File download URL
|
||||
- `name` (optional): Display name
|
||||
- `size` (optional): File size in bytes
|
||||
- `mime_type` (optional): MIME type for file type detection
|
||||
|
||||
### 6. Code Elements
|
||||
|
||||
Display syntax-highlighted code blocks with copy functionality.
|
||||
|
||||
```python
|
||||
msg.add_code(
|
||||
content='print("Hello, World!")',
|
||||
language="python",
|
||||
name="Hello World Example"
|
||||
)
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
- `content` (required): Code content
|
||||
- `language` (optional): Programming language for syntax highlighting
|
||||
- `name` (optional): Display name
|
||||
|
||||
## Display Modes
|
||||
|
||||
All elements support three display modes:
|
||||
|
||||
- **`inline`** (default): Full width, normal display
|
||||
- **`side`**: Smaller size, suitable for sidebars
|
||||
- **`page`**: Full page width, maximum size
|
||||
|
||||
## Size Limits
|
||||
|
||||
- **File elements**: Maximum 100MB per file
|
||||
- **Code elements**: Maximum 1MB per code block
|
||||
- **Images**: No size limit (browser-dependent)
|
||||
|
||||
## Accessibility Features
|
||||
|
||||
- **Images**: Alt text support, keyboard navigation for zoom
|
||||
- **PDFs**: Proper ARIA labels and roles
|
||||
- **Videos/Audio**: Native browser accessibility
|
||||
- **Files**: Icon-based file type recognition
|
||||
- **Code**: Copy button with keyboard support
|
||||
|
||||
## Migration from Chainlit
|
||||
|
||||
PraisonAIUI elements provide direct parity with Chainlit elements:
|
||||
|
||||
| Chainlit | PraisonAIUI |
|
||||
|----------|-------------|
|
||||
| `cl.Image()` | `msg.add_image()` |
|
||||
| `cl.Pdf()` | `msg.add_pdf()` |
|
||||
| `cl.Video()` | `msg.add_video()` |
|
||||
| `cl.Audio()` | `msg.add_audio()` |
|
||||
| `cl.File()` | `msg.add_file()` |
|
||||
| Code blocks | `msg.add_code()` |
|
||||
|
||||
## Low-Level API
|
||||
|
||||
For advanced use cases, you can use the low-level element API:
|
||||
|
||||
```python
|
||||
from praisonaiui.schema.models import ImageElement
|
||||
|
||||
# Create typed element
|
||||
element = ImageElement(
|
||||
url="https://example.com/image.png",
|
||||
alt="Description",
|
||||
display="inline"
|
||||
)
|
||||
|
||||
# Add to message
|
||||
msg.elements.append(element)
|
||||
```
|
||||
|
||||
## Legacy Support
|
||||
|
||||
The new element system maintains backward compatibility with existing message properties:
|
||||
|
||||
- `message.images` (still supported)
|
||||
- `message.files` (still supported)
|
||||
- New `message.elements` takes precedence when both are present
|
||||
|
||||
This ensures existing code continues to work while providing a migration path to the new standardized element system.
|
||||
@@ -5,6 +5,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import type { ChatMessage, ToolCall } from '../types'
|
||||
import { ToolCallDisplay } from './ToolCallDisplay'
|
||||
import { ElementRenderer } from './MultimediaElements'
|
||||
|
||||
interface ChatMessagesProps {
|
||||
messages: ChatMessage[]
|
||||
@@ -316,6 +317,21 @@ function MessageBubble({ message }: MessageBubbleProps) {
|
||||
</div>
|
||||
)}
|
||||
<MarkdownContent content={message.content} />
|
||||
|
||||
{/* New standardized elements rendering */}
|
||||
{message.elements && message.elements.length > 0 && (
|
||||
<div className="mt-3 space-y-3">
|
||||
{message.elements.map((element, i) => (
|
||||
<ElementRenderer
|
||||
key={i}
|
||||
element={element}
|
||||
className="w-full"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Legacy format support for backward compatibility */}
|
||||
{message.images && message.images.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{message.images.map((url, i) => (
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { useState } from 'react'
|
||||
import type { MessageElementUnion, ImageElement, PdfElement, VideoElement, AudioElement, FileElement, CodeElement } from '../types'
|
||||
|
||||
interface ImageElementProps {
|
||||
url: string
|
||||
alt?: string
|
||||
className?: string
|
||||
display?: 'inline' | 'side' | 'page'
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export function ImageElement({ url, alt = '', className = '' }: ImageElementProps) {
|
||||
export function ImageElement({ url, alt = '', className = '', display = 'inline', width, height }: ImageElementProps) {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [zoomed, setZoomed] = useState(false)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -18,19 +23,137 @@ export function ImageElement({ url, alt = '', className = '' }: ImageElementProp
|
||||
)
|
||||
}
|
||||
|
||||
const maxHeight = display === 'side' ? 'max-h-64' : display === 'page' ? 'max-h-screen' : 'max-h-96'
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{!loaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-muted rounded-md animate-pulse">
|
||||
<span className="text-muted-foreground text-sm">Loading...</span>
|
||||
<>
|
||||
<div className={`relative cursor-pointer ${className}`} onClick={() => setZoomed(true)}>
|
||||
{!loaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-muted rounded-md animate-pulse">
|
||||
<span className="text-muted-foreground text-sm" aria-live="polite">Loading image...</span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={url}
|
||||
alt={alt || 'Image'}
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => setError(true)}
|
||||
className={`max-w-full rounded-md ${maxHeight} object-contain transition-transform hover:scale-105 ${loaded ? '' : 'invisible'}`}
|
||||
style={width || height ? { width, height } : undefined}
|
||||
role="img"
|
||||
aria-label={alt || 'Click to enlarge image'}
|
||||
/>
|
||||
{loaded && (
|
||||
<div className="absolute bottom-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded opacity-0 hover:opacity-100 transition-opacity">
|
||||
Click to zoom
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Zoom overlay */}
|
||||
{zoomed && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
|
||||
onClick={() => setZoomed(false)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Image zoom view"
|
||||
>
|
||||
<div className="relative max-w-full max-h-full">
|
||||
<img
|
||||
src={url}
|
||||
alt={alt || 'Zoomed image'}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
role="img"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-4 right-4 bg-white/20 hover:bg-white/30 text-white p-2 rounded-full transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setZoomed(false)
|
||||
}}
|
||||
aria-label="Close zoom view"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={url}
|
||||
alt={alt}
|
||||
onLoad={() => setLoaded(true)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface PdfElementProps {
|
||||
url: string
|
||||
name?: string
|
||||
className?: string
|
||||
display?: 'inline' | 'side' | 'page'
|
||||
}
|
||||
|
||||
export function PdfElement({ url, name, className = '', display = 'inline' }: PdfElementProps) {
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center bg-muted rounded-md p-4 ${className}`}>
|
||||
<span className="text-muted-foreground text-sm">Failed to load PDF</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// For page display mode, open in a new tab
|
||||
if (display === 'page') {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex items-center gap-3 p-3 rounded-md border bg-card hover:bg-accent transition-colors ${className}`}
|
||||
>
|
||||
<span className="text-2xl">📄</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">{name || 'PDF Document'}</p>
|
||||
<p className="text-xs text-muted-foreground">Click to open in new tab</p>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<path d="M7 7h10v10" />
|
||||
<path d="m7 17 10-10" />
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
// For inline/side display, embed PDF viewer
|
||||
const height = display === 'side' ? '400px' : '600px'
|
||||
|
||||
return (
|
||||
<div className={`rounded-md overflow-hidden ${className}`}>
|
||||
{name && (
|
||||
<div className="px-3 py-2 bg-muted border-b text-sm font-medium">
|
||||
📄 {name}
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
src={`${url}#toolbar=1&navpanes=1&scrollbar=1`}
|
||||
width="100%"
|
||||
height={height}
|
||||
style={{ border: 'none' }}
|
||||
onError={() => setError(true)}
|
||||
className={`max-w-full rounded-md max-h-96 object-contain ${loaded ? '' : 'invisible'}`}
|
||||
title={name || 'PDF Document'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -161,3 +284,118 @@ export function CodeBlock({ code, language, className = '' }: CodeBlockProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Unified element renderer supporting all element types and display modes
|
||||
interface ElementRendererProps {
|
||||
element: MessageElementUnion | Record<string, unknown>
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ElementRenderer({ element, className = '' }: ElementRendererProps) {
|
||||
// Type guard to check if element has required properties
|
||||
const hasType = element && typeof element === 'object' && 'type' in element
|
||||
if (!hasType) return null
|
||||
|
||||
const elementType = element.type as string
|
||||
|
||||
try {
|
||||
switch (elementType) {
|
||||
case 'image': {
|
||||
const img = element as ImageElement
|
||||
return (
|
||||
<ImageElement
|
||||
url={img.url}
|
||||
alt={img.alt}
|
||||
className={`${className} ${getDisplayModeClass(img.display)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'pdf': {
|
||||
const pdf = element as PdfElement
|
||||
return (
|
||||
<PdfElement
|
||||
url={pdf.url}
|
||||
name={pdf.name}
|
||||
display={pdf.display}
|
||||
className={`${className} ${getDisplayModeClass(pdf.display)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'video': {
|
||||
const video = element as VideoElement
|
||||
return (
|
||||
<VideoElement
|
||||
url={video.url}
|
||||
className={`${className} ${getDisplayModeClass(video.display)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'audio': {
|
||||
const audio = element as AudioElement
|
||||
return (
|
||||
<AudioElement
|
||||
url={audio.url}
|
||||
className={`${className} ${getDisplayModeClass(audio.display)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'file': {
|
||||
const file = element as FileElement
|
||||
return (
|
||||
<FileElement
|
||||
url={file.url}
|
||||
name={file.name || 'File'}
|
||||
size={file.size}
|
||||
type={file.mimeType}
|
||||
className={`${className} ${getDisplayModeClass(file.display)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'code': {
|
||||
const code = element as CodeElement
|
||||
return (
|
||||
<CodeBlock
|
||||
code={code.content}
|
||||
language={code.language}
|
||||
className={`${className} ${getDisplayModeClass(code.display)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default:
|
||||
// Fallback for legacy/unknown element types
|
||||
if ('url' in element && typeof element.url === 'string') {
|
||||
if (elementType === 'image') {
|
||||
return <ImageElement url={element.url} className={className} />
|
||||
} else if (elementType === 'video') {
|
||||
return <VideoElement url={element.url} className={className} />
|
||||
} else if (elementType === 'audio') {
|
||||
return <AudioElement url={element.url} className={className} />
|
||||
} else {
|
||||
const name = ('name' in element && typeof element.name === 'string') ? element.name : 'File'
|
||||
return <FileElement url={element.url} name={name} className={className} />
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error rendering element:', error)
|
||||
return (
|
||||
<div className={`flex items-center justify-center bg-muted rounded-md p-4 ${className}`}>
|
||||
<span className="text-muted-foreground text-sm">Failed to render element</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get CSS classes based on display mode
|
||||
function getDisplayModeClass(display?: string): string {
|
||||
switch (display) {
|
||||
case 'side':
|
||||
return 'max-w-sm'
|
||||
case 'page':
|
||||
return 'w-full'
|
||||
case 'inline':
|
||||
default:
|
||||
return 'max-w-full'
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,57 @@ export interface RouteManifest {
|
||||
routes?: { pattern: string; template: string }[]
|
||||
}
|
||||
|
||||
// Message element types (matching Python schema)
|
||||
export interface MessageElement {
|
||||
type: 'image' | 'pdf' | 'video' | 'audio' | 'file' | 'code'
|
||||
name?: string
|
||||
display?: 'inline' | 'side' | 'page'
|
||||
}
|
||||
|
||||
export interface ImageElement extends MessageElement {
|
||||
type: 'image'
|
||||
url: string
|
||||
alt?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface PdfElement extends MessageElement {
|
||||
type: 'pdf'
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface VideoElement extends MessageElement {
|
||||
type: 'video'
|
||||
url: string
|
||||
autoplay?: boolean
|
||||
controls?: boolean
|
||||
loop?: boolean
|
||||
}
|
||||
|
||||
export interface AudioElement extends MessageElement {
|
||||
type: 'audio'
|
||||
url: string
|
||||
autoplay?: boolean
|
||||
controls?: boolean
|
||||
loop?: boolean
|
||||
}
|
||||
|
||||
export interface FileElement extends MessageElement {
|
||||
type: 'file'
|
||||
url: string
|
||||
size?: number
|
||||
mimeType?: string
|
||||
}
|
||||
|
||||
export interface CodeElement extends MessageElement {
|
||||
type: 'code'
|
||||
content: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export type MessageElementUnion = ImageElement | PdfElement | VideoElement | AudioElement | FileElement | CodeElement
|
||||
|
||||
// Chat types
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
@@ -76,6 +127,9 @@ export interface ChatMessage {
|
||||
timestamp: string
|
||||
thinking?: string[]
|
||||
toolCalls?: ToolCall[]
|
||||
// Rich inline elements (new standardized format)
|
||||
elements?: (MessageElementUnion | Record<string, unknown>)[]
|
||||
// Legacy format (for backward compatibility)
|
||||
images?: string[]
|
||||
audio?: string[]
|
||||
video?: string[]
|
||||
|
||||
+102
-13
@@ -16,9 +16,22 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from praisonaiui.server import MessageContext
|
||||
from praisonaiui.schema.models import (
|
||||
MessageElementUnion,
|
||||
ImageElement,
|
||||
PdfElement,
|
||||
VideoElement,
|
||||
AudioElement,
|
||||
FileElement,
|
||||
CodeElement
|
||||
)
|
||||
|
||||
# Size limits in bytes
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
MAX_CODE_SIZE = 1024 * 1024 # 1MB for code blocks
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -55,7 +68,7 @@ class Message:
|
||||
content: str = ""
|
||||
author: str = "assistant"
|
||||
streaming: bool = False
|
||||
elements: list[dict[str, Any]] = field(default_factory=list)
|
||||
elements: list[Union[MessageElementUnion, dict[str, Any]]] = field(default_factory=list)
|
||||
actions: list[dict[str, Any]] = field(default_factory=list)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@@ -184,25 +197,101 @@ class Message:
|
||||
content: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> "Message":
|
||||
"""Add an element (image, file, etc.) to the message.
|
||||
"""Add a typed element (image, file, etc.) to the message.
|
||||
|
||||
Args:
|
||||
element_type: Type of element ("image", "file", "audio", "video")
|
||||
url: URL of the element
|
||||
content: Inline content (for text elements)
|
||||
**kwargs: Additional element properties
|
||||
element_type: Type of element ("image", "pdf", "video", "audio", "file", "code")
|
||||
url: URL of the element (required for non-code elements)
|
||||
content: Inline content (required for code elements)
|
||||
**kwargs: Additional element properties (name, display, alt, etc.)
|
||||
|
||||
Returns:
|
||||
self for chaining
|
||||
"""
|
||||
element = {"type": element_type, **kwargs}
|
||||
if url:
|
||||
element["url"] = url
|
||||
if content:
|
||||
element["content"] = content
|
||||
self.elements.append(element)
|
||||
try:
|
||||
# Validate size limits
|
||||
if kwargs.get("size") and kwargs["size"] > MAX_FILE_SIZE:
|
||||
raise ValueError(f"File size {kwargs['size']} exceeds maximum allowed size {MAX_FILE_SIZE}")
|
||||
|
||||
if content and len(content.encode('utf-8')) > MAX_CODE_SIZE:
|
||||
raise ValueError(f"Code content size exceeds maximum allowed size {MAX_CODE_SIZE}")
|
||||
|
||||
# Create typed element based on type
|
||||
if element_type == "image":
|
||||
if not url:
|
||||
raise ValueError("url is required for image elements")
|
||||
element = ImageElement(url=url, **kwargs)
|
||||
elif element_type == "pdf":
|
||||
if not url:
|
||||
raise ValueError("url is required for pdf elements")
|
||||
element = PdfElement(url=url, **kwargs)
|
||||
elif element_type == "video":
|
||||
if not url:
|
||||
raise ValueError("url is required for video elements")
|
||||
element = VideoElement(url=url, **kwargs)
|
||||
elif element_type == "audio":
|
||||
if not url:
|
||||
raise ValueError("url is required for audio elements")
|
||||
element = AudioElement(url=url, **kwargs)
|
||||
elif element_type == "file":
|
||||
if not url:
|
||||
raise ValueError("url is required for file elements")
|
||||
element = FileElement(url=url, **kwargs)
|
||||
elif element_type == "code":
|
||||
if not content:
|
||||
raise ValueError("content is required for code elements")
|
||||
element = CodeElement(content=content, **kwargs)
|
||||
else:
|
||||
# Fallback to legacy dict format for unknown types
|
||||
element = {"type": element_type, **kwargs}
|
||||
if url:
|
||||
element["url"] = url
|
||||
if content:
|
||||
element["content"] = content
|
||||
|
||||
self.elements.append(element)
|
||||
except Exception:
|
||||
# Fallback to legacy dict format on validation errors
|
||||
element = {"type": element_type, **kwargs}
|
||||
if url:
|
||||
element["url"] = url
|
||||
if content:
|
||||
element["content"] = content
|
||||
self.elements.append(element)
|
||||
|
||||
return self
|
||||
|
||||
def add_image(self, url: str, name: Optional[str] = None, alt: Optional[str] = None,
|
||||
display: str = "inline", **kwargs: Any) -> "Message":
|
||||
"""Add an image element to the message."""
|
||||
return self.add_element("image", url=url, name=name, alt=alt, display=display, **kwargs)
|
||||
|
||||
def add_pdf(self, url: str, name: Optional[str] = None, display: str = "inline", **kwargs: Any) -> "Message":
|
||||
"""Add a PDF element to the message."""
|
||||
return self.add_element("pdf", url=url, name=name, display=display, **kwargs)
|
||||
|
||||
def add_video(self, url: str, name: Optional[str] = None, display: str = "inline",
|
||||
controls: bool = True, **kwargs: Any) -> "Message":
|
||||
"""Add a video element to the message."""
|
||||
return self.add_element("video", url=url, name=name, display=display, controls=controls, **kwargs)
|
||||
|
||||
def add_audio(self, url: str, name: Optional[str] = None, display: str = "inline",
|
||||
controls: bool = True, **kwargs: Any) -> "Message":
|
||||
"""Add an audio element to the message."""
|
||||
return self.add_element("audio", url=url, name=name, display=display, controls=controls, **kwargs)
|
||||
|
||||
def add_file(self, url: str, name: Optional[str] = None, display: str = "inline",
|
||||
size: Optional[int] = None, mime_type: Optional[str] = None, **kwargs: Any) -> "Message":
|
||||
"""Add a file download element to the message."""
|
||||
return self.add_element("file", url=url, name=name, display=display, size=size,
|
||||
mimeType=mime_type, **kwargs)
|
||||
|
||||
def add_code(self, content: str, language: Optional[str] = None, name: Optional[str] = None,
|
||||
display: str = "inline", **kwargs: Any) -> "Message":
|
||||
"""Add a code block element to the message."""
|
||||
return self.add_element("code", content=content, language=language, name=name,
|
||||
display=display, **kwargs)
|
||||
|
||||
def add_action(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@@ -386,6 +386,76 @@ class InputWidgetConfig(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MessageElement(BaseModel):
|
||||
"""Base class for message elements (images, PDFs, videos, etc.)."""
|
||||
|
||||
type: Literal["image", "pdf", "video", "audio", "file", "code"]
|
||||
name: Optional[str] = None
|
||||
display: Literal["inline", "side", "page"] = "inline"
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class ImageElement(MessageElement):
|
||||
"""Image element for messages."""
|
||||
|
||||
type: Literal["image"] = "image"
|
||||
url: str
|
||||
alt: Optional[str] = None
|
||||
width: Optional[int] = None
|
||||
height: Optional[int] = None
|
||||
|
||||
|
||||
class PdfElement(MessageElement):
|
||||
"""PDF element for messages."""
|
||||
|
||||
type: Literal["pdf"] = "pdf"
|
||||
url: str
|
||||
|
||||
|
||||
class VideoElement(MessageElement):
|
||||
"""Video element for messages."""
|
||||
|
||||
type: Literal["video"] = "video"
|
||||
url: str
|
||||
autoplay: bool = False
|
||||
controls: bool = True
|
||||
loop: bool = False
|
||||
|
||||
|
||||
class AudioElement(MessageElement):
|
||||
"""Audio element for messages."""
|
||||
|
||||
type: Literal["audio"] = "audio"
|
||||
url: str
|
||||
autoplay: bool = False
|
||||
controls: bool = True
|
||||
loop: bool = False
|
||||
|
||||
|
||||
class FileElement(MessageElement):
|
||||
"""File download element for messages."""
|
||||
|
||||
type: Literal["file"] = "file"
|
||||
url: str
|
||||
size: Optional[int] = None
|
||||
mime_type: Optional[str] = Field(default=None, alias="mimeType")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class CodeElement(MessageElement):
|
||||
"""Code block element for messages."""
|
||||
|
||||
type: Literal["code"] = "code"
|
||||
content: str
|
||||
language: Optional[str] = None
|
||||
|
||||
|
||||
# Union type for all element types
|
||||
MessageElementUnion = ImageElement | PdfElement | VideoElement | AudioElement | FileElement | CodeElement
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
"""Root configuration model for aiui.template.yaml."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user