Initial commit

This commit is contained in:
Dom
2026-03-05 01:20:15 +01:00
commit c0c50e56f0
364 changed files with 62207 additions and 0 deletions

76
.gitignore vendored Normal file
View File

@@ -0,0 +1,76 @@
# === Python ===
__pycache__/
*.py[cod]
*.pyo
*.egg-info/
*.egg
dist/
build/
*.whl
# === Virtual environments ===
.venv/
venv/
venv_*/
env/
# === ML Models & Data ===
*.pt
*.pth
*.onnx
*.bin
*.safetensors
*.h5
*.hdf5
*.pkl
*.pickle
*.npy
*.npz
*.faiss
models/
*.tar.gz
*.zip
# === Documents & Media ===
*.pdf
*.docx
*.xlsx
*.csv
*.png
*.jpg
*.jpeg
*.gif
*.mp3
*.wav
*.mp4
# === IDE ===
.idea/
.vscode/
*.swp
*.swo
*~
# === OS ===
.DS_Store
Thumbs.db
.~lock.*
# === Secrets ===
.env
*.env
credentials.json
token.pickle
# === Logs & Cache ===
*.log
logs/
.pytest_cache/
.mypy_cache/
.ruff_cache/
htmlcov/
.coverage
# === Backups ===
*_backup_*
backups/

151
.snapshots/config.json Normal file
View File

@@ -0,0 +1,151 @@
{
"excluded_patterns": [
".git",
".gitignore",
"gradle",
"gradlew",
"gradlew.*",
"node_modules",
".snapshots",
".idea",
".vscode",
"*.log",
"*.tmp",
"target",
"dist",
"build",
".DS_Store",
"*.bak",
"*.swp",
"*.swo",
"*.lock",
"*.iml",
"coverage",
"*.min.js",
"*.min.css",
"__pycache__",
".marketing",
".env",
".env.*",
"*.jpg",
"*.jpeg",
"*.png",
"*.gif",
"*.bmp",
"*.tiff",
"*.ico",
"*.svg",
"*.webp",
"*.psd",
"*.ai",
"*.eps",
"*.indd",
"*.raw",
"*.cr2",
"*.nef",
"*.mp4",
"*.mov",
"*.avi",
"*.wmv",
"*.flv",
"*.mkv",
"*.webm",
"*.m4v",
"*.wfp",
"*.prproj",
"*.aep",
"*.psb",
"*.xcf",
"*.sketch",
"*.fig",
"*.xd",
"*.db",
"*.sqlite",
"*.sqlite3",
"*.mdb",
"*.accdb",
"*.frm",
"*.myd",
"*.myi",
"*.ibd",
"*.dbf",
"*.rdb",
"*.aof",
"*.pdb",
"*.sdb",
"*.s3db",
"*.ddb",
"*.db-shm",
"*.db-wal",
"*.sqlitedb",
"*.sql.gz",
"*.bak.sql",
"dump.sql",
"dump.rdb",
"*.vsix",
"*.jar",
"*.war",
"*.ear",
"*.zip",
"*.tar",
"*.tar.gz",
"*.tgz",
"*.rar",
"*.7z",
"*.exe",
"*.dll",
"*.so",
"*.dylib",
"*.app",
"*.dmg",
"*.iso",
"*.msi",
"*.deb",
"*.rpm",
"*.apk",
"*.aab",
"*.ipa",
"*.pkg",
"*.nupkg",
"*.snap",
"*.whl",
"*.gem",
"*.pyc",
"*.pyo",
"*.pyd",
"*.class",
"*.o",
"*.obj",
"*.lib",
"*.a",
"*.map",
".npmrc"
],
"default": {
"default_prompt": "Enter your prompt here",
"default_include_all_files": false,
"default_include_entire_project_structure": true
},
"included_patterns": [
"build.gradle",
"settings.gradle",
"gradle.properties",
"pom.xml",
"Makefile",
"CMakeLists.txt",
"package.json",
"requirements.txt",
"Pipfile",
"Gemfile",
"composer.json",
".editorconfig",
".eslintrc.json",
".eslintrc.js",
".prettierrc",
".babelrc",
".dockerignore",
".gitattributes",
".stylelintrc",
".npmrc"
]
}

11
.snapshots/readme.md Normal file
View File

@@ -0,0 +1,11 @@
# Snapshots Directory
This directory contains snapshots of your code for AI interactions. Each snapshot is a markdown file that includes relevant code context and project structure information.
## What's included in snapshots?
- Selected code files and their contents
- Project structure (if enabled)
- Your prompt/question for the AI
## Configuration
You can customize snapshot behavior in `config.json`.

44
.snapshots/sponsors.md Normal file
View File

@@ -0,0 +1,44 @@
# Thank you for using Snapshots for AI
Thanks for using Snapshots for AI. We hope this tool has helped you solve a problem or two.
If you would like to support our work, please help us by considering the following offers and requests:
## Ways to Support
### Join the GBTI Network!!! 🙏🙏🙏
The GBTI Network is a community of developers who are passionate about open source and community-driven development. Members enjoy access to exclussive tools, resources, a private MineCraft server, a listing in our members directory, co-op opportunities and more.
- Support our work by becoming a [GBTI Network member](https://gbti.network/membership/).
### Try out BugHerd 🐛
BugHerd is a visual feedback and bug-tracking tool designed to streamline website development by enabling users to pin feedback directly onto web pages. This approach facilitates clear communication among clients, designers, developers, and project managers.
- Start your free trial with [BugHerd](https://partners.bugherd.com/55z6c8az8rvr) today.
### Hire Developers from Codeable 👥
Codeable connects you with top-tier professionals skilled in frameworks and technologies such as Laravel, React, Django, Node, Vue.js, Angular, Ruby on Rails, and Node.js. Don't let the WordPress focus discourage you. Codeable experts do it all.
- Visit [Codeable](https://www.codeable.io/developers/?ref=z8h3e) to hire your next team member.
### Lead positive reviews on our marketplace listing ⭐⭐⭐⭐⭐
- Rate us on [VSCode marketplace](https://marketplace.visualstudio.com/items?itemName=GBTI.snapshots-for-ai)
- Review us on [Cursor marketplace](https://open-vsx.org/extension/GBTI/snapshots-for-ai)
### Star Our GitHub Repository ⭐
- Star and watch our [repository](https://github.com/gbti-network/vscode-snapshots-for-ai)
### 📡 Stay Connected
Follow us on your favorite platforms for updates, news, and community discussions:
- **[Twitter/X](https://twitter.com/gbti_network)**
- **[GitHub](https://github.com/gbti-network)**
- **[YouTube](https://www.youtube.com/channel/UCh4FjB6r4oWQW-QFiwqv-UA)**
- **[Dev.to](https://dev.to/gbti)**
- **[Daily.dev](https://dly.to/zfCriM6JfRF)**
- **[Hashnode](https://gbti.hashnode.dev/)**
- **[Discord Community](https://gbti.network)**
- **[Reddit Community](https://www.reddit.com/r/GBTI_network)**
---
Thank you for supporting open source software! 🙏

1
aivanov_project/vanna/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.ipynb linguist-detectable=false

28
aivanov_project/vanna/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
build
**.egg-info
venn
.DS_Store
tests/__pycache__
__pycache__/
.idea
.coverage
docs/*.html
.ipynb_checkpoints/
.tox/
notebooks/chroma.sqlite3
dist
.env
*.sqlite
htmlcov
chroma.sqlite3
*.bin
.coverage.*
milvus.db
.milvus.db.lock
# Frontend builds and dependencies
frontends/**/node_modules/
frontends/**/static/
frontends/**/.storybook-static/
frontends/**/package-lock.json
frontends/**/.mypy_cache/

View File

@@ -0,0 +1,19 @@
exclude: 'docs|node_modules|migrations|.git|.tox|assets.py'
default_stages: [ commit ]
fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: debug-statements
- id: mixed-line-ending
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: [ "--profile", "black", "--filter-files" ]

View File

@@ -0,0 +1,485 @@
# Contributing to Vanna
Thank you for your interest in contributing to Vanna! This guide will help you get started with contributing to the Vanna 2.0+ codebase.
## Table of Contents
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Code Standards](#code-standards)
- [Testing](#testing)
- [Pull Request Process](#pull-request-process)
- [Architecture Overview](#architecture-overview)
- [Adding New Features](#adding-new-features)
---
## Getting Started
### Prerequisites
- Python 3.11 or higher
- Git
- A GitHub account
### Fork and Clone
1. Fork the repository on GitHub
2. Clone your fork locally:
```bash
git clone https://github.com/YOUR_USERNAME/vanna.git
cd vanna
```
3. Add the upstream repository:
```bash
git remote add upstream https://github.com/vanna-ai/vanna.git
```
---
## Development Setup
### 1. Create a Virtual Environment
```bash
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
### 2. Install Dependencies
```bash
# Install the package in editable mode with all extras
pip install -e ".[all]"
# Install development tools
pip install tox ruff mypy pytest pytest-asyncio
```
### 3. Verify Installation
```bash
# Run unit tests
tox -e py311-unit
# Run type checking
tox -e mypy
# Run format checking
tox -e ruff
```
---
## Code Standards
### Formatting
We use [ruff](https://github.com/astral-sh/ruff) for code formatting and linting.
```bash
# Check formatting
ruff format --check src/vanna/ tests/
# Apply formatting
ruff format src/vanna/ tests/
# Run linting
ruff check src/vanna/ tests/
```
### Type Checking
We use mypy with strict mode for type checking:
```bash
tox -e mypy
```
All new code should include type hints.
### Code Style Guidelines
- Follow PEP 8 style guidelines
- Use descriptive variable and function names
- Add docstrings to all public functions and classes
- Keep functions focused and single-purpose
- Avoid circular imports by using `TYPE_CHECKING`
**Example:**
```python
"""Module docstring explaining the purpose."""
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from vanna.core.user import User
class MyClass:
"""Class docstring explaining what this class does."""
async def my_method(self, user: "User", count: int = 10) -> Optional[str]:
"""Method docstring explaining parameters and return value.
Args:
user: The user making the request
count: Maximum number of items to return
Returns:
Result string if found, None otherwise
"""
pass
```
---
## Testing
### Test Organization
Tests are organized in the `tests/` directory:
- `test_tool_permissions.py` - Tool access control tests
- `test_llm_context_enhancer.py` - LLM enhancer tests
- `test_legacy_adapter.py` - Legacy compatibility tests
- `test_agent_memory.py` - Agent memory tests
- `test_database_sanity.py` - Database integration tests
- `test_agents.py` - End-to-end agent tests
### Running Tests
```bash
# Run all unit tests (no external dependencies)
tox -e py311-unit
# Run specific test file
pytest tests/test_tool_permissions.py -v
# Run tests with a specific marker
pytest tests/ -v -m anthropic
# Run legacy adapter tests
tox -e py311-legacy
```
### Writing Tests
1. **Unit tests** should not require external dependencies (databases, APIs, etc.)
2. Use **pytest markers** for tests that require external services:
```python
@pytest.mark.anthropic
@pytest.mark.asyncio
async def test_with_anthropic():
# Test code here
pass
```
3. **Mock external dependencies** in unit tests:
```python
class MockLlmService(LlmService):
async def send_request(self, request):
# Mock implementation
pass
```
4. **Test both success and failure cases**
5. **Use descriptive test names** that explain what is being tested
### Test Coverage
When adding new features, ensure:
- Core functionality is covered by unit tests
- Integration points are tested
- Error handling is validated
- Edge cases are considered
---
## Pull Request Process
### 1. Create a Feature Branch
```bash
git checkout -b feature/my-new-feature
# or
git checkout -b fix/bug-description
```
### 2. Make Your Changes
- Write your code following the code standards
- Add tests for your changes
- Update documentation as needed
### 3. Run All Checks
```bash
# Format code
ruff format src/vanna/ tests/
# Run linting
ruff check src/vanna/ tests/
# Run type checking
tox -e mypy
# Run tests
tox -e py311-unit
```
### 4. Commit Your Changes
Use clear, descriptive commit messages:
```bash
git add .
git commit -m "feat: add new LLM context enhancer for RAG
- Implements TextMemoryEnhancer class
- Adds tests for memory retrieval
- Updates documentation"
```
**Commit message format:**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `test:` - Adding or updating tests
- `refactor:` - Code refactoring
- `chore:` - Maintenance tasks
### 5. Push and Create PR
```bash
git push origin feature/my-new-feature
```
Then create a pull request on GitHub with:
- Clear title describing the change
- Description of what was changed and why
- Link to any related issues
- Screenshots or examples if applicable
### 6. Code Review
- Address review feedback promptly
- Keep discussions focused and professional
- Be open to suggestions and alternative approaches
---
## Architecture Overview
### Core Components
Vanna 2.0+ is built around several key abstractions:
#### 1. **Agent** (`vanna.core.agent`)
The main orchestrator that coordinates tools, memory, and LLM interactions.
#### 2. **Tools** (`vanna.tools`, `vanna.core.tool`)
Modular capabilities that the agent can use. Each tool:
- Has a schema defining its inputs
- Implements an `execute()` method
- Declares access control via `access_groups`
#### 3. **Tool Registry** (`vanna.core.registry`)
Manages tool registration and access control.
#### 4. **Agent Memory** (`vanna.capabilities.agent_memory`)
Stores and retrieves tool usage patterns and documentation.
#### 5. **LLM Services** (`vanna.core.llm`)
Abstract interface for different LLM providers (Anthropic, OpenAI, etc.).
#### 6. **SQL Runners** (`vanna.capabilities.sql_runner`)
Abstract interface for executing SQL against different databases.
#### 7. **Components** (`vanna.components`)
Rich UI components for rendering results (tables, charts, status cards, etc.).
### Data Flow
```
User Request → Agent → LLM Service → Tool Selection → Tool Execution → Response Components
↓ ↓
Agent Memory SQL Runner / Other Capabilities
```
---
## Adding New Features
### Adding a New Tool
1. **Create the tool class** in `src/vanna/tools/`:
```python
from vanna.core.tool import Tool, ToolContext, ToolResult
from pydantic import BaseModel, Field
class MyToolArgs(BaseModel):
"""Arguments for my tool."""
query: str = Field(description="The query to process")
class MyTool(Tool[MyToolArgs]):
"""Tool that does something useful."""
@property
def name(self) -> str:
return "my_tool"
@property
def description(self) -> str:
return "Does something useful with a query"
def get_args_schema(self) -> type[MyToolArgs]:
return MyToolArgs
async def execute(
self,
context: ToolContext,
args: MyToolArgs
) -> ToolResult:
# Implement your tool logic
result = f"Processed: {args.query}"
return ToolResult(
success=True,
result_for_llm=result,
ui_component=None
)
```
2. **Add tests** in `tests/test_my_tool.py`
3. **Register the tool** in examples or documentation
### Adding a New Database Integration
1. **Implement SqlRunner** in `src/vanna/integrations/mydb/`:
```python
from vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs
from vanna.core.tool import ToolContext
import pandas as pd
class MyDbRunner(SqlRunner):
"""SQL runner for MyDB database."""
def __init__(self, connection_string: str):
self.connection_string = connection_string
# Initialize your DB connection
async def run_sql(
self,
args: RunSqlToolArgs,
context: ToolContext
) -> pd.DataFrame:
# Execute SQL and return DataFrame
pass
```
2. **Add sanity tests** in `tests/test_database_sanity.py`
3. **Add tox target** in `tox.ini`
4. **Update documentation**
### Adding a New LLM Integration
1. **Implement LlmService** in `src/vanna/integrations/myllm/`:
```python
from vanna.core.llm.base import LlmService
from vanna.core.llm.models import LlmRequest, LlmResponse, LlmStreamChunk
from typing import AsyncGenerator
class MyLlmService(LlmService):
"""LLM service for MyLLM provider."""
def __init__(self, api_key: str, model: str = "default"):
self.api_key = api_key
self.model = model
async def send_request(self, request: LlmRequest) -> LlmResponse:
# Implement API call
pass
async def stream_request(
self,
request: LlmRequest
) -> AsyncGenerator[LlmStreamChunk, None]:
# Implement streaming API call
yield LlmStreamChunk(...)
async def validate_tools(self, tools) -> list[str]:
# Validate tool schemas
return []
```
2. **Add tests** with the `@pytest.mark.myllm` marker
3. **Add tox target** for integration tests
### Adding a New Agent Memory Backend
1. **Implement AgentMemory** in `src/vanna/integrations/mystore/`:
```python
from vanna.capabilities.agent_memory import (
AgentMemory,
ToolMemory,
ToolMemorySearchResult,
TextMemory,
TextMemorySearchResult
)
from vanna.core.tool import ToolContext
class MyStoreMemory(AgentMemory):
"""Agent memory using MyStore vector database."""
async def save_tool_usage(self, question, tool_name, args, context, success=True, metadata=None):
# Implement storage
pass
async def search_similar_usage(self, question, context, *, limit=10, similarity_threshold=0.7, tool_name_filter=None):
# Implement search
pass
# Implement other AgentMemory methods...
```
2. **Add tests** in `tests/test_agent_memory.py`
3. **Add to extras** in `pyproject.toml`
---
## Legacy Compatibility
If you're working on legacy VannaBase compatibility:
- The `LegacyVannaAdapter` bridges legacy code with Vanna 2.0+
- Add tests to `tests/test_legacy_adapter.py`
- See `src/vanna/legacy/adapter.py` for examples
---
## Getting Help
- **Documentation**: https://vanna.ai/docs/
- **GitHub Issues**: https://github.com/vanna-ai/vanna/issues
- **Discussions**: https://github.com/vanna-ai/vanna/discussions
---
## License
By contributing to Vanna, you agree that your contributions will be licensed under the MIT License.
---
Thank you for contributing to Vanna! 🎉

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Vanna.AI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,296 @@
# Migration Guide: Vanna 0.x to Vanna 2.0+
This guide will help you migrate from Vanna 0.x (legacy) to Vanna 2.0+, the new user-aware agent framework.
## Table of Contents
- [Overview of Changes](#overview-of-changes)
- [Quick Migration Path](#quick-migration-path)
- [Migration Strategies](#migration-strategies)
- [Strategy 1: Using the Legacy Adapter (Recommended for Quick Migration)](#strategy-1-using-the-legacy-adapter-recommended-for-quick-migration)
- [Strategy 2: Full Migration to New Architecture](#strategy-2-full-migration-to-new-architecture)
- [Key Architectural Differences](#key-architectural-differences)
- [API Mapping](#api-mapping)
- [Common Migration Scenarios](#common-migration-scenarios)
- [Breaking Changes](#breaking-changes)
- [FAQ](#faq)
---
## Overview of Changes
Vanna 2.0+ represents a fundamental architectural shift from a simple LLM wrapper to a full-fledged **user-aware agent framework**. Here are the major changes:
### What's New in 2.0+
-**User awareness** - Identity and permissions flow through every layer
-**Web component** - Pre-built UI with streaming responses
-**Tool registry** - Modular, extensible tool system
-**Rich UI components** - Tables, charts, status cards (not just text)
-**Streaming by default** - Progressive responses via SSE
-**Enterprise features** - Audit logs, rate limiting, observability
-**FastAPI/Flask servers** - Production-ready backends included
### What Changed from 0.x
- ❌ Direct method calls (`vn.ask()`) → Agent-based workflow
- ❌ Monolithic `VannaBase` class → Modular tool system
- ❌ No user context → User-aware at every layer
- ❌ Simple text responses → Rich streaming UI components
---
## Quick Migration Path
**Can't migrate immediately?** Use the Legacy Adapter to get started quickly:
```python
# Assume you already have a working vn object from your Vanna 0.x code:
# vn = MyVanna(config={"model": "gpt-4"})
# vn.connect_to_postgres(...)
# vn.train(ddl="...")
# NEW: Just add these imports and wrap your existing vn object
from vanna import Agent, AgentConfig
from vanna.servers.fastapi import VannaFastAPIServer
from vanna.core.user import UserResolver, User, RequestContext
from vanna.legacy.adapter import LegacyVannaAdapter
from vanna.integrations.anthropic import AnthropicLlmService
# Define simple user resolver
class SimpleUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
user_email = request_context.get_cookie('vanna_email')
return User(id=user_email, email=user_email, group_memberships=['user'])
# Wrap your existing vn with the adapter
tools = LegacyVannaAdapter(vn)
# Create agent with new LLM service
llm = AnthropicLlmService(model="claude-haiku-4-5")
agent = Agent(llm_service=llm, tool_registry=tools, user_resolver=SimpleUserResolver())
# Run server
server = VannaFastAPIServer(agent)
server.run(host='0.0.0.0', port=8000)
# Now it works with the new Agent framework!
# (See Strategy 1 below for complete example)
```
---
## Migration Strategies
### Strategy 1: Using the Legacy Adapter (Recommended for Quick Migration)
**Best for:** Teams that want to adopt Vanna 2.0+ gradually while maintaining existing code.
#### Step 1: Install Vanna 2.0+
```bash
pip install 'vanna[flask,anthropic]'
```
#### Step 2: Wrap Your Existing VannaBase Instance
```python
from vanna import Agent, AgentConfig
from vanna.servers.fastapi import VannaFastAPIServer
from vanna.core.user import UserResolver, User, RequestContext
from vanna.legacy.adapter import LegacyVannaAdapter
from vanna.integrations.anthropic import AnthropicLlmService
# Assume you already have a working vn object from your existing code:
# vn = MyVanna(config={'model': 'gpt-4', 'api_key': 'your-key'})
# vn.connect_to_postgres(...)
# vn.train(ddl="...")
# etc.
# NEW: Define user resolution (required in 2.0+)
class SimpleUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
user_email = request_context.get_cookie('vanna_email')
if not user_email:
raise ValueError("Missing 'vanna_email' cookie")
# Admin users get 'admin' group membership
if user_email == "admin@example.com":
return User(id="admin_user", email=user_email, group_memberships=['admin'])
# Regular users get 'user' group membership
return User(id=user_email, email=user_email, group_memberships=['user'])
# NEW: Wrap with legacy adapter
# This automatically registers run_sql and memory tools from your VannaBase instance
tools = LegacyVannaAdapter(vn)
# NEW: Set up LLM for the new Agent framework
llm = AnthropicLlmService(
model="claude-haiku-4-5",
api_key="YOUR_ANTHROPIC_API_KEY"
)
# NEW: Create agent with legacy adapter as tool registry
agent = Agent(
llm_service=llm,
tool_registry=tools, # LegacyVannaAdapter is a ToolRegistry
user_resolver=SimpleUserResolver(),
config=AgentConfig()
)
# NEW: Create and run server
server = VannaFastAPIServer(agent)
if __name__ == "__main__":
# Run with: python your_script.py
# Or: uvicorn your_module:server --host 0.0.0.0 --port 8000
server.run(host='0.0.0.0', port=8000)
```
**What the LegacyVannaAdapter does:**
- Automatically wraps `vn.run_sql()` as the `run_sql` tool (available to 'user' and 'admin' groups)
- Exposes training data from `vn.get_training_data()` as searchable memory (via `search_saved_correct_tool_uses` tool)
- Optionally allows saving new training data (via `save_question_tool_args` tool - admin only)
- Maintains your existing database connection and training data
**Pros:**
- ✅ Minimal code changes
- ✅ Preserve existing training data
- ✅ Gradual migration path
- ✅ Get new features (web UI, streaming) immediately
**Cons:**
- ⚠️ Limited user awareness (all requests use same VannaBase instance)
- ⚠️ Can't leverage row-level security
- ⚠️ Missing some advanced features
---
### Strategy 2: Full Migration to New Architecture
**Best for:** New projects or teams ready for a complete rewrite.
#### Before (Vanna 0.x)
```python
from vanna import VannaBase
from vanna.openai_chat import OpenAI_Chat
from vanna.chromadb import ChromaDB_VectorStore
class MyVanna(ChromaDB_VectorStore, OpenAI_Chat):
def __init__(self, config=None):
ChromaDB_VectorStore.__init__(self, config=config)
OpenAI_Chat.__init__(self, config=config)
vn = MyVanna(config={'model': 'gpt-4', 'api_key': 'your-key'})
vn.connect_to_postgres(...)
# Train
vn.train(ddl="CREATE TABLE customers ...")
vn.train(question="Top customers?", sql="SELECT ...")
# Ask
sql = vn.generate_sql("Who are the top customers?")
df = vn.run_sql(sql)
print(df)
```
#### After (Vanna 2.0+)
```python
from vanna import Agent, AgentConfig
from vanna.servers.fastapi import VannaFastAPIServer
from vanna.core.registry import ToolRegistry
from vanna.core.user import UserResolver, User, RequestContext
from vanna.integrations.anthropic import AnthropicLlmService
from vanna.tools import RunSqlTool
from vanna.integrations.postgres import PostgresRunner
# 1. Define user resolution
class MyUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
# Extract from your auth system (JWT, cookies, etc.)
token = request_context.get_header('Authorization')
user_data = await self.validate_token(token)
return User(
id=user_data['id'],
email=user_data['email'],
permissions=user_data['permissions'],
metadata={'role': user_data['role']}
)
# 2. Set up tools
tools = ToolRegistry()
postgres_runner = PostgresRunner(
host="localhost",
dbname="mydb",
user="user",
password="password",
port=5432
)
tools.register_local_tool(
RunSqlTool(sql_runner=postgres_runner),
access_groups=['user', 'admin']
)
# 3. Create agent
llm = AnthropicLlmService(model="claude-sonnet-4-5")
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=MyUserResolver(),
config=AgentConfig(stream_responses=True)
)
# 4. Create server
server = VannaFastAPIServer(agent)
app = server.create_app()
# Run with: uvicorn main:app --host 0.0.0.0 --port 8000
# Visit http://localhost:8000 for web UI
```
**Pros:**
- ✅ Full access to new features
- ✅ True user awareness
- ✅ Better security and permissions
- ✅ Production-ready architecture
**Cons:**
- ⚠️ Requires rewriting code
- ⚠️ Need to migrate training data approach
- ⚠️ Steeper learning curve
---
## Key Architectural Differences
| Feature | Vanna 0.x | Vanna 2.0+ |
|---------|-----------|------------|
| **User Context** | None | `User` object with permissions flows through entire system |
| **Interaction Model** | Direct method calls (`vn.ask()`) | Agent-based with streaming components |
| **Tools** | Monolithic methods | Modular `Tool` classes with schemas |
| **Responses** | Plain text/DataFrames | Rich UI components (tables, charts, code) |
| **Training** | `vn.train()` with vector DB | System prompts, context enrichers, RAG tools |
| **Database Connection** | `vn.connect_to_postgres()` | `SqlRunner` implementations as dependencies |
| **Web UI** | None (custom implementation) | Built-in web component + backend |
| **Streaming** | None | Server-Sent Events by default |
| **Permissions** | None | Group-based access control on tools |
| **Audit Logs** | None | Built-in audit logging system |
---
## Summary
| If you want to... | Use this strategy |
|-------------------|-------------------|
| Migrate quickly with minimal changes | **Strategy 1: Legacy Adapter** |
| Get full access to new features | **Strategy 2: Full Migration** |
| Support both legacy and new code | **Strategy 1** initially, then gradual migration |
| Start a new project | **Strategy 2: Full Migration** |
**Recommended Path:**
1. Start with Legacy Adapter for quick migration
2. Gradually rewrite critical paths to native 2.0+ architecture
3. Eventually remove Legacy Adapter once fully migrated
Good luck with your migration! 🚀

View File

@@ -0,0 +1,311 @@
# Vanna 2.0: Turn Questions into Data Insights
**Natural language → SQL → Answers.** Now with enterprise security and user-aware permissions.
[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://python.org)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
https://github.com/user-attachments/assets/476cd421-d0b0-46af-8b29-0f40c73d6d83
![Vanna2 Demo](img/architecture.png)
---
## What's New in 2.0
🔐 **User-Aware at Every Layer** — Queries automatically filtered per user permissions
🎨 **Modern Web Interface** — Beautiful pre-built `<vanna-chat>` component
**Streaming Responses** — Real-time tables, charts, and progress updates
🔒 **Enterprise Security** — Row-level security, audit logs, rate limiting
🔄 **Production-Ready** — FastAPI integration, observability, lifecycle hooks
> **Upgrading from 0.x?** See the [Migration Guide](MIGRATION_GUIDE.md) | [What changed?](#migration-notes)
---
## Get Started
### Try it with Sample Data
[Quickstart](https://vanna.ai/docs/quick-start)
### Configure
[Configure](https://vanna.ai/docs/configure)
### Web Component
```html
<!-- Drop into any existing webpage -->
<script src="https://img.vanna.ai/vanna-components.js"></script>
<vanna-chat
sse-endpoint="https://your-api.com/chat"
theme="dark">
</vanna-chat>
```
Uses your existing cookies/JWTs. Works with React, Vue, or plain HTML.
---
## What You Get
Ask a question in natural language and get back:
**1. Streaming Progress Updates**
**2. SQL Code Block (By default only shown to "admin" users)**
**3. Interactive Data Table**
**4. Charts** (Plotly visualizations)
**5. Natural Language Summary**
All streamed in real-time to your web component.
---
## Why Vanna 2.0?
### ✅ Get Started Instantly
* Production chat interface
* Custom agent with your database
* Embed in any webpage
### ✅ Enterprise-Ready Security
**User-aware at every layer** — Identity flows through system prompts, tool execution, and SQL filtering
**Row-level security** — Queries automatically filtered per user permissions
**Audit logs** — Every query tracked per user for compliance
**Rate limiting** — Per-user quotas via lifecycle hooks
### ✅ Beautiful Web UI Included
**Pre-built `<vanna-chat>` component** — No need to build your own chat interface
**Streaming tables & charts** — Rich components, not just text
**Responsive & customizable** — Works on mobile, desktop, light/dark themes
**Framework-agnostic** — React, Vue, plain HTML
### ✅ Works With Your Stack
**Any LLM:** OpenAI, Anthropic, Ollama, Azure, Google Gemini, AWS Bedrock, Mistral, Others
**Any Database:** PostgreSQL, MySQL, Snowflake, BigQuery, Redshift, SQLite, Oracle, SQL Server, DuckDB, ClickHouse, Others
**Your Auth System:** Bring your own — cookies, JWTs, OAuth tokens
**Your Framework:** FastAPI, Flask
### ✅ Extensible But Opinionated
**Custom tools** — Extend the `Tool` base class
**Lifecycle hooks** — Quota checking, logging, content filtering
**LLM middlewares** — Caching, prompt engineering
**Observability** — Built-in tracing and metrics
---
## Architecture
![Vanna2 Diagram](img/vanna2.svg)
---
## How It Works
```mermaid
sequenceDiagram
participant U as 👤 User
participant W as 🌐 <vanna-chat>
participant S as 🐍 Your Server
participant A as 🤖 Agent
participant T as 🧰 Tools
U->>W: "Show Q4 sales"
W->>S: POST /api/vanna/v2/chat_sse (with auth)
S->>A: User(id=alice, groups=[read_sales])
A->>T: Execute SQL tool (user-aware)
T->>T: Apply row-level security
T->>A: Filtered results
A->>W: Stream: Table → Chart → Summary
W->>U: Display beautiful UI
```
**Key Concepts:**
1. **User Resolver** — You define how to extract user identity from requests (cookies, JWTs, etc.)
2. **User-Aware Tools** — Tools automatically check permissions based on user's group memberships
3. **Streaming Components** — Backend streams structured UI components (tables, charts) to frontend
4. **Built-in Web UI** — Pre-built `<vanna-chat>` component renders everything beautifully
---
## Production Setup with Your Auth
Here's a complete example integrating Vanna with your existing FastAPI app and authentication:
```python
from fastapi import FastAPI
from vanna import Agent
from vanna.servers.fastapi.routes import register_chat_routes
from vanna.servers.base import ChatHandler
from vanna.core.user import UserResolver, User, RequestContext
from vanna.integrations.anthropic import AnthropicLlmService
from vanna.tools import RunSqlTool
from vanna.integrations.sqlite import SqliteRunner
from vanna.core.registry import ToolRegistry
# Your existing FastAPI app
app = FastAPI()
# 1. Define your user resolver (using YOUR auth system)
class MyUserResolver(UserResolver):
async def resolve_user(self, request_context: RequestContext) -> User:
# Extract from cookies, JWTs, or session
token = request_context.get_header('Authorization')
user_data = self.decode_jwt(token) # Your existing logic
return User(
id=user_data['id'],
email=user_data['email'],
group_memberships=user_data['groups'] # Used for permissions
)
# 2. Set up agent with tools
llm = AnthropicLlmService(model="claude-sonnet-4-5")
tools = ToolRegistry()
tools.register(RunSqlTool(sql_runner=SqliteRunner("./data.db")))
agent = Agent(
llm_service=llm,
tool_registry=tools,
user_resolver=MyUserResolver()
)
# 3. Add Vanna routes to your app
chat_handler = ChatHandler(agent)
register_chat_routes(app, chat_handler)
# Now you have:
# - POST /api/vanna/v2/chat_sse (streaming endpoint)
# - GET / (optional web UI)
```
**Then in your frontend:**
```html
<vanna-chat sse-endpoint="/api/vanna/v2/chat_sse"></vanna-chat>
```
See [Full Documentation](https://vanna.ai/docs) for custom tools, lifecycle hooks, and advanced configuration
---
## Custom Tools
Extend Vanna with custom tools for your specific use case:
```python
from vanna.core.tool import Tool, ToolContext, ToolResult
from pydantic import BaseModel, Field
from typing import Type
class EmailArgs(BaseModel):
recipient: str = Field(description="Email recipient")
subject: str = Field(description="Email subject")
class EmailTool(Tool[EmailArgs]):
@property
def name(self) -> str:
return "send_email"
@property
def access_groups(self) -> list[str]:
return ["send_email"] # Permission check
def get_args_schema(self) -> Type[EmailArgs]:
return EmailArgs
async def execute(self, context: ToolContext, args: EmailArgs) -> ToolResult:
user = context.user # Automatically injected
# Your business logic
await self.email_service.send(
from_email=user.email,
to=args.recipient,
subject=args.subject
)
return ToolResult(success=True, result_for_llm=f"Email sent to {args.recipient}")
# Register your tool
tools.register(EmailTool())
```
---
## Advanced Features
Vanna 2.0 includes powerful enterprise features for production use:
**Lifecycle Hooks** — Add quota checking, custom logging, content filtering at key points in the request lifecycle
**LLM Middlewares** — Implement caching, prompt engineering, or cost tracking around LLM calls
**Conversation Storage** — Persist and retrieve conversation history per user
**Observability** — Built-in tracing and metrics integration
**Context Enrichers** — Add RAG, memory, or documentation to enhance agent responses
**Agent Configuration** — Control streaming, temperature, max iterations, and more
---
## Use Cases
**Vanna is ideal for:**
- 📊 Data analytics applications with natural language interfaces
- 🔐 Multi-tenant SaaS needing user-aware permissions
- 🎨 Teams wanting a pre-built web component + backend
- 🏢 Enterprise environments with security/audit requirements
- 📈 Applications needing rich streaming responses (tables, charts, SQL)
- 🔄 Integrating with existing authentication systems
---
## Community & Support
- 📖 **[Full Documentation](https://vanna.ai/docs)** — Complete guides and API reference
- 💡 **[GitHub Discussions](https://github.com/vanna-ai/vanna/discussions)** — Feature requests and Q&A
- 🐛 **[GitHub Issues](https://github.com/vanna-ai/vanna/issues)** — Bug reports
- 📧 **Enterprise Support** — support@vanna.ai
---
## Migration Notes
**Upgrading from Vanna 0.x?**
Vanna 2.0 is a complete rewrite focused on user-aware agents and production deployments. Key changes:
- **New API**: Agent-based instead of `VannaBase` class methods
- **User-aware**: Every component now knows the user identity
- **Streaming**: Rich UI components instead of text/dataframes
- **Web-first**: Built-in `<vanna-chat>` component and server
**Migration path:**
1. **Quick wrap** — Use `LegacyVannaAdapter` to wrap your existing Vanna 0.x instance and get the new web UI immediately
2. **Gradual migration** — Incrementally move to the new Agent API and tools
See the complete [Migration Guide](MIGRATION_GUIDE.md) for step-by-step instructions.
---
## License
MIT License — See [LICENSE](LICENSE) for details.
---
**Built with ❤️ by the Vanna team** | [Website](https://vanna.ai) | [Docs](https://vanna.ai/docs) | [Discussions](https://github.com/vanna-ai/vanna/discussions)

View File

@@ -0,0 +1,183 @@
# AIVANOV — Assistant IA d'Analyse de Données
Interface conversationnelle pour interroger des bases de données en langage naturel, avec génération automatique de graphiques interactifs.
---
## 📁 Structure du Projet
```
vanna/
├── run_server.py # Point d'entrée — Lance le serveur AIVANOV
├── README_AIVANOV.md # Ce fichier
├── src/vanna/ # Code backend Python
│ ├── core/
│ │ ├── agent/agent.py # Agent conversationnel principal
│ │ ├── workflow/default.py # Gestionnaire de commandes (/help, /status)
│ │ ├── user/ # Gestion des utilisateurs
│ │ └── tool/models.py # Modèles Pydantic pour les outils
│ │
│ ├── tools/
│ │ ├── run_sql.py # Outil d'exécution SQL
│ │ ├── visualize_data.py # Outil de génération de graphiques
│ │ ├── export_pdf.py # Outil d'export PDF
│ │ └── file_system.py # Abstraction système de fichiers
│ │
│ ├── integrations/
│ │ ├── ollama/llm.py # Connecteur Ollama (LLM)
│ │ ├── postgres/runner.py # Connecteur PostgreSQL
│ │ └── local/ # Stockage local (conversations, mémoire)
│ │
│ └── servers/
│ ├── fastapi/
│ │ ├── app.py # Application FastAPI
│ │ └── routes.py # Routes API (chat, historique, suggestions)
│ └── base/templates.py # Template HTML de la page d'accueil
├── frontends/webcomponent/ # Code frontend TypeScript (Lit)
│ ├── src/
│ │ ├── components/
│ │ │ ├── vanna-chat.ts # Composant chat principal
│ │ │ ├── vanna-status-bar.ts # Barre de statut
│ │ │ ├── vanna-progress-tracker.ts # Suivi des tâches
│ │ │ ├── rich-component-system.ts # Rendu des composants riches
│ │ │ └── plotly-chart.ts # Graphiques Plotly
│ │ │
│ │ ├── styles/
│ │ │ ├── vanna-design-tokens.ts # Variables CSS (couleurs, espacements)
│ │ │ └── rich-component-styles.ts # Styles des composants
│ │ │
│ │ └── services/
│ │ └── api-client.ts # Client API (SSE, WebSocket)
│ │
│ ├── dist/ # Build de production (généré)
│ │ └── vanna-components.js
│ │
│ └── package.json
└── data/ # Données persistées
└── conversations/ # Historique des conversations
```
---
## 🚀 Installation
### Prérequis
- **Python 3.10+**
- **Node.js 18+** (pour le build frontend)
- **PostgreSQL** avec une base de données (ex: Chinook)
- **Ollama** avec un modèle LLM (ex: gpt-oss:120b-cloud, llama3, mistral)
### 1. Dépendances Python
```bash
pip install fastapi uvicorn pydantic pandas plotly psycopg2-binary ollama reportlab
```
### 2. Dépendances Frontend
```bash
cd frontends/webcomponent
npm install
npm run build
```
### 3. Configuration
Éditez `run_server.py` pour configurer :
```python
# Modèle LLM Ollama
llm_service = OllamaLlmService(
model="gpt-oss:120b-cloud", # Nom du modèle Ollama
host="http://localhost:11434", # URL du serveur Ollama
)
# Base de données PostgreSQL
postgres_runner = PostgresRunner(
host="localhost",
port=5432,
database="chinook", # Nom de la BDD
user="votre_user",
password="votre_mot_de_passe",
)
```
### 4. Schéma de la base
Modifiez le `SYSTEM_PROMPT` dans `run_server.py` pour décrire votre schéma de base de données.
---
## ▶️ Lancement
```bash
python3 run_server.py
```
Le serveur démarre sur **http://localhost:8084**
---
## 🎯 Fonctionnalités
### Types de visualisations
- 🥧 **Camemberts** — Répartitions, parts de marché
- 📊 **Barres** — Comparaisons, classements
- 📈 **Courbes** — Évolutions temporelles
- 📉 **Histogrammes** — Distributions
- 🔥 **Cartes de chaleur** — Corrélations
- 🔀 **Combinés** — Multi-dimensions
### Commandes spéciales
- `/help` — Affiche l'aide
- `/status` — État du système (connexion SQL, mémoire)
### Exemples de questions
- "Fais-moi un camembert de la répartition des genres musicaux"
- "Quels sont les 10 artistes les plus vendus ? Montre un graphique en barres"
- "Évolution du chiffre d'affaires par année avec une courbe"
- "Donne-moi les ventes par pays avec des commentaires"
---
## 🔌 API Endpoints
| Endpoint | Description |
|----------|-------------|
| `POST /api/vanna/v2/chat_sse` | Chat en streaming (SSE) |
| `GET /api/aivanov/v1/history` | Historique des requêtes |
| `GET /api/aivanov/v1/suggestions` | Suggestions de questions |
| `GET /api/aivanov/v1/download/{file}` | Téléchargement de fichiers |
---
## 🛠️ Développement
### Rebuild du frontend
```bash
cd frontends/webcomponent
npm run build
```
### Mode développement frontend
```bash
npm run dev
```
### Logs du serveur
Les logs s'affichent dans le terminal. En cas d'erreur, vérifiez :
1. La connexion à Ollama (`curl http://localhost:11434/api/tags`)
2. La connexion PostgreSQL (`psql -h localhost -d chinook`)
---
## 📝 Licence
Projet interne AIVANOV.

View File

@@ -0,0 +1,270 @@
| GitHub | PyPI | Documentation | Gurubase |
| ------ | ---- | ------------- | -------- |
| [![GitHub](https://img.shields.io/badge/GitHub-vanna-blue?logo=github)](https://github.com/vanna-ai/vanna) | [![PyPI](https://img.shields.io/pypi/v/vanna?logo=pypi)](https://pypi.org/project/vanna/) | [![Documentation](https://img.shields.io/badge/Documentation-vanna-blue?logo=read-the-docs)](https://vanna.ai/docs/) | [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Vanna%20Guru-006BFF)](https://gurubase.io/g/vanna) |
# Vanna
Vanna is an MIT-licensed open-source Python RAG (Retrieval-Augmented Generation) framework for SQL generation and related functionality.
https://github.com/vanna-ai/vanna/assets/7146154/1901f47a-515d-4982-af50-f12761a3b2ce
![vanna-quadrants](https://github.com/vanna-ai/vanna/assets/7146154/1c7c88ba-c144-4ecf-a028-cf5ba7344ca2)
## How Vanna works
![Screen Recording 2024-01-24 at 11 21 37AM](https://github.com/vanna-ai/vanna/assets/7146154/1d2718ad-12a8-4a76-afa2-c61754462f93)
Vanna works in two easy steps - train a RAG "model" on your data, and then ask questions which will return SQL queries that can be set up to automatically run on your database.
1. **Train a RAG "model" on your data**.
2. **Ask questions**.
![](img/vanna-readme-diagram.png)
If you don't know what RAG is, don't worry -- you don't need to know how this works under the hood to use it. You just need to know that you "train" a model, which stores some metadata and then use it to "ask" questions.
See the [base class](https://github.com/vanna-ai/vanna/blob/main/src/vanna/base/base.py) for more details on how this works under the hood.
## User Interfaces
These are some of the user interfaces that we've built using Vanna. You can use these as-is or as a starting point for your own custom interface.
- [Jupyter Notebook](https://vanna.ai/docs/postgres-openai-vanna-vannadb/)
- [vanna-ai/vanna-streamlit](https://github.com/vanna-ai/vanna-streamlit)
- [vanna-ai/vanna-flask](https://github.com/vanna-ai/vanna-flask)
- [vanna-ai/vanna-slack](https://github.com/vanna-ai/vanna-slack)
## Supported LLMs
- [OpenAI](https://github.com/vanna-ai/vanna/tree/main/src/vanna/openai)
- [Anthropic](https://github.com/vanna-ai/vanna/tree/main/src/vanna/anthropic)
- [Gemini](https://github.com/vanna-ai/vanna/blob/main/src/vanna/google/gemini_chat.py)
- [HuggingFace](https://github.com/vanna-ai/vanna/blob/main/src/vanna/hf/hf.py)
- [AWS Bedrock](https://github.com/vanna-ai/vanna/tree/main/src/vanna/bedrock)
- [Ollama](https://github.com/vanna-ai/vanna/tree/main/src/vanna/ollama)
- [Qianwen](https://github.com/vanna-ai/vanna/tree/main/src/vanna/qianwen)
- [Qianfan](https://github.com/vanna-ai/vanna/tree/main/src/vanna/qianfan)
- [Zhipu](https://github.com/vanna-ai/vanna/tree/main/src/vanna/ZhipuAI)
## Supported VectorStores
- [AzureSearch](https://github.com/vanna-ai/vanna/tree/main/src/vanna/azuresearch)
- [Opensearch](https://github.com/vanna-ai/vanna/tree/main/src/vanna/opensearch)
- [PgVector](https://github.com/vanna-ai/vanna/tree/main/src/vanna/pgvector)
- [PineCone](https://github.com/vanna-ai/vanna/tree/main/src/vanna/pinecone)
- [ChromaDB](https://github.com/vanna-ai/vanna/tree/main/src/vanna/chromadb)
- [FAISS](https://github.com/vanna-ai/vanna/tree/main/src/vanna/faiss)
- [Marqo](https://github.com/vanna-ai/vanna/tree/main/src/vanna/marqo)
- [Milvus](https://github.com/vanna-ai/vanna/tree/main/src/vanna/milvus)
- [Qdrant](https://github.com/vanna-ai/vanna/tree/main/src/vanna/qdrant)
- [Weaviate](https://github.com/vanna-ai/vanna/tree/main/src/vanna/weaviate)
- [Oracle](https://github.com/vanna-ai/vanna/tree/main/src/vanna/oracle)
## Supported Databases
- [PostgreSQL](https://www.postgresql.org/)
- [MySQL](https://www.mysql.com/)
- [PrestoDB](https://prestodb.io/)
- [Apache Hive](https://hive.apache.org/)
- [ClickHouse](https://clickhouse.com/)
- [Snowflake](https://www.snowflake.com/en/)
- [Oracle](https://www.oracle.com/)
- [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads)
- [BigQuery](https://cloud.google.com/bigquery)
- [SQLite](https://www.sqlite.org/)
- [DuckDB](https://duckdb.org/)
## Getting started
See the [documentation](https://vanna.ai/docs/) for specifics on your desired database, LLM, etc.
If you want to get a feel for how it works after training, you can try this [Colab notebook](https://vanna.ai/docs/app/).
### Install
```bash
pip install vanna
```
There are a number of optional packages that can be installed so see the [documentation](https://vanna.ai/docs/) for more details.
### Import
See the [documentation](https://vanna.ai/docs/) if you're customizing the LLM or vector database.
```python
# The import statement will vary depending on your LLM and vector database. This is an example for OpenAI + ChromaDB
from vanna.openai.openai_chat import OpenAI_Chat
from vanna.chromadb.chromadb_vector import ChromaDB_VectorStore
class MyVanna(ChromaDB_VectorStore, OpenAI_Chat):
def __init__(self, config=None):
ChromaDB_VectorStore.__init__(self, config=config)
OpenAI_Chat.__init__(self, config=config)
vn = MyVanna(config={'api_key': 'sk-...', 'model': 'gpt-4-...'})
# See the documentation for other options
```
## Training
You may or may not need to run these `vn.train` commands depending on your use case. See the [documentation](https://vanna.ai/docs/) for more details.
These statements are shown to give you a feel for how it works.
### Train with DDL Statements
DDL statements contain information about the table names, columns, data types, and relationships in your database.
```python
vn.train(ddl="""
CREATE TABLE IF NOT EXISTS my-table (
id INT PRIMARY KEY,
name VARCHAR(100),
age INT
)
""")
```
### Train with Documentation
Sometimes you may want to add documentation about your business terminology or definitions.
```python
vn.train(documentation="Our business defines XYZ as ...")
```
### Train with SQL
You can also add SQL queries to your training data. This is useful if you have some queries already laying around. You can just copy and paste those from your editor to begin generating new SQL.
```python
vn.train(sql="SELECT name, age FROM my-table WHERE name = 'John Doe'")
```
## Asking questions
```python
vn.ask("What are the top 10 customers by sales?")
```
You'll get SQL
```sql
SELECT c.c_name as customer_name,
sum(l.l_extendedprice * (1 - l.l_discount)) as total_sales
FROM snowflake_sample_data.tpch_sf1.lineitem l join snowflake_sample_data.tpch_sf1.orders o
ON l.l_orderkey = o.o_orderkey join snowflake_sample_data.tpch_sf1.customer c
ON o.o_custkey = c.c_custkey
GROUP BY customer_name
ORDER BY total_sales desc limit 10;
```
If you've connected to a database, you'll get the table:
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>CUSTOMER_NAME</th>
<th>TOTAL_SALES</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>Customer#000143500</td>
<td>6757566.0218</td>
</tr>
<tr>
<th>1</th>
<td>Customer#000095257</td>
<td>6294115.3340</td>
</tr>
<tr>
<th>2</th>
<td>Customer#000087115</td>
<td>6184649.5176</td>
</tr>
<tr>
<th>3</th>
<td>Customer#000131113</td>
<td>6080943.8305</td>
</tr>
<tr>
<th>4</th>
<td>Customer#000134380</td>
<td>6075141.9635</td>
</tr>
<tr>
<th>5</th>
<td>Customer#000103834</td>
<td>6059770.3232</td>
</tr>
<tr>
<th>6</th>
<td>Customer#000069682</td>
<td>6057779.0348</td>
</tr>
<tr>
<th>7</th>
<td>Customer#000102022</td>
<td>6039653.6335</td>
</tr>
<tr>
<th>8</th>
<td>Customer#000098587</td>
<td>6027021.5855</td>
</tr>
<tr>
<th>9</th>
<td>Customer#000064660</td>
<td>5905659.6159</td>
</tr>
</tbody>
</table>
</div>
You'll also get an automated Plotly chart:
![](img/top-10-customers.png)
## RAG vs. Fine-Tuning
RAG
- Portable across LLMs
- Easy to remove training data if any of it becomes obsolete
- Much cheaper to run than fine-tuning
- More future-proof -- if a better LLM comes out, you can just swap it out
Fine-Tuning
- Good if you need to minimize tokens in the prompt
- Slow to get started
- Expensive to train and run (generally)
## Why Vanna?
1. **High accuracy on complex datasets.**
- Vannas capabilities are tied to the training data you give it
- More training data means better accuracy for large and complex datasets
2. **Secure and private.**
- Your database contents are never sent to the LLM or the vector database
- SQL execution happens in your local environment
3. **Self learning.**
- If using via Jupyter, you can choose to "auto-train" it on the queries that were successfully executed
- If using via other interfaces, you can have the interface prompt the user to provide feedback on the results
- Correct question to SQL pairs are stored for future reference and make the future results more accurate
4. **Supports any SQL database.**
- The package allows you to connect to any SQL database that you can otherwise connect to with Python
5. **Choose your front end.**
- Most people start in a Jupyter Notebook.
- Expose to your end users via Slackbot, web app, Streamlit app, or a custom front end.
## Extending Vanna
Vanna is designed to connect to any database, LLM, and vector database. There's a [VannaBase](https://github.com/vanna-ai/vanna/blob/main/src/vanna/base/base.py) abstract base class that defines some basic functionality. The package provides implementations for use with OpenAI and ChromaDB. You can easily extend Vanna to use your own LLM or vector database. See the [documentation](https://vanna.ai/docs/) for more details.
## Vanna in 100 Seconds
https://github.com/vanna-ai/vanna/assets/7146154/eb90ee1e-aa05-4740-891a-4fc10e611cab
## More resources
- [Full Documentation](https://vanna.ai/docs/)
- [Website](https://vanna.ai)
- [Discord group for support](https://discord.gg/qUZYKHremx)

View File

@@ -0,0 +1,137 @@
"""
Example: Using ChromaDB AgentMemory with GPU acceleration
This example demonstrates how to use ChromaAgentMemory with intelligent
device selection for GPU acceleration when available.
"""
from vanna.integrations.chromadb import (
ChromaAgentMemory,
get_device,
create_sentence_transformer_embedding_function
)
def example_default_usage():
"""Example 1: Use default embedding function (no GPU, no sentence-transformers required)"""
print("Example 1: Default ChromaDB embedding (CPU-only, no extra dependencies)")
memory = ChromaAgentMemory(
persist_directory="./chroma_memory_default"
)
print("✓ ChromaAgentMemory created with default embedding function")
print()
def example_auto_gpu():
"""Example 2: Automatic GPU detection with SentenceTransformers"""
print("Example 2: Automatic GPU detection")
# Detect the best available device
device = get_device()
print(f"Detected device: {device}")
# Create embedding function with automatic device selection
embedding_fn = create_sentence_transformer_embedding_function()
memory = ChromaAgentMemory(
persist_directory="./chroma_memory_gpu",
embedding_function=embedding_fn
)
print(f"✓ ChromaAgentMemory created with SentenceTransformer on {device}")
print()
def example_explicit_cuda():
"""Example 3: Explicitly use CUDA"""
print("Example 3: Explicitly request CUDA")
# Explicitly request CUDA
embedding_fn = create_sentence_transformer_embedding_function(device="cuda")
memory = ChromaAgentMemory(
persist_directory="./chroma_memory_cuda",
embedding_function=embedding_fn
)
print("✓ ChromaAgentMemory created with SentenceTransformer on CUDA")
print()
def example_custom_model_gpu():
"""Example 4: Use a larger model with GPU"""
print("Example 4: Custom model with GPU acceleration")
# Use a larger, more accurate model with GPU
embedding_fn = create_sentence_transformer_embedding_function(
model_name="sentence-transformers/all-mpnet-base-v2"
)
memory = ChromaAgentMemory(
persist_directory="./chroma_memory_large",
embedding_function=embedding_fn
)
print("✓ ChromaAgentMemory created with all-mpnet-base-v2 model")
print()
def example_manual_chromadb():
"""Example 5: Manually configure ChromaDB embedding function"""
print("Example 5: Manual ChromaDB embedding function configuration")
from chromadb.utils import embedding_functions
# Manually create and configure the embedding function
device = get_device()
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="sentence-transformers/all-MiniLM-L6-v2",
device=device
)
memory = ChromaAgentMemory(
persist_directory="./chroma_memory_manual",
embedding_function=embedding_fn
)
print(f"✓ ChromaAgentMemory created with manual configuration on {device}")
print()
if __name__ == "__main__":
print("=" * 70)
print("ChromaDB AgentMemory GPU Acceleration Examples")
print("=" * 70)
print()
# Example 1: Default (no GPU, no sentence-transformers needed)
example_default_usage()
# Examples 2-5 require sentence-transformers to be installed
try:
import sentence_transformers
example_auto_gpu()
# Only run CUDA example if CUDA is available
device = get_device()
if device == "cuda":
example_explicit_cuda()
example_custom_model_gpu()
example_manual_chromadb()
except ImportError:
print("⚠️ sentence-transformers not installed")
print(" Install with: pip install sentence-transformers")
print(" Examples 2-5 require this package for GPU acceleration")
print()
print("=" * 70)
print("Summary:")
print("- Example 1 works without sentence-transformers (CPU only)")
print("- Examples 2-5 require sentence-transformers for GPU support")
print("- GPU acceleration automatically detected when available")
print("=" * 70)

View File

@@ -0,0 +1,156 @@
"""
Example demonstrating how to use ToolRegistry.transform_args for user-specific
argument transformation, such as applying row-level security (RLS) to SQL queries.
This example shows:
1. Creating a custom ToolRegistry subclass that overrides transform_args
2. Applying RLS transformation to SQL queries based on user context
3. Rejecting tool execution when validation fails
"""
from typing import Union
from pydantic import BaseModel
from vanna.core import ToolRegistry
from vanna.core.tool import Tool, ToolContext, ToolRejection, ToolResult
from vanna.core.user import User
# Example: SQL execution tool arguments
class SQLExecutionArgs(BaseModel):
query: str
database: str = "default"
class SQLExecutionTool(Tool[SQLExecutionArgs]):
@property
def name(self) -> str:
return "execute_sql"
@property
def description(self) -> str:
return "Execute a SQL query against the database"
def get_args_schema(self):
return SQLExecutionArgs
async def execute(self, context: ToolContext, args: SQLExecutionArgs) -> ToolResult:
# Execute the SQL query (implementation not shown)
return ToolResult(
success=True,
result_for_llm=f"Executed query: {args.query[:50]}...",
)
class RLSToolRegistry(ToolRegistry):
"""Custom ToolRegistry that applies row-level security to SQL queries."""
async def transform_args(
self,
tool: Tool,
args,
user: User,
context: ToolContext,
) -> Union[SQLExecutionArgs, ToolRejection]:
"""Apply row-level security transformation to SQL queries."""
# Only transform SQL execution tools
if tool.name == "execute_sql" and isinstance(args, SQLExecutionArgs):
original_query = args.query.strip()
# Example 1: Reject queries that try to access restricted tables
if "restricted_table" in original_query.lower():
return ToolRejection(
reason="Access to 'restricted_table' is not permitted for your user group"
)
# Example 2: Apply RLS by modifying the WHERE clause
# This is a simplified example - real RLS would be more sophisticated
if "SELECT" in original_query.upper() and "users" in original_query.lower():
# Add a WHERE clause to filter by user's organization
user_org_id = user.metadata.get("organization_id")
if user_org_id:
# Simple RLS: append WHERE clause for organization filtering
if "WHERE" in original_query.upper():
transformed_query = original_query.replace(
"WHERE",
f"WHERE organization_id = {user_org_id} AND",
1
)
else:
# Add WHERE clause before ORDER BY, LIMIT, etc.
transformed_query = original_query.rstrip(";")
transformed_query += f" WHERE organization_id = {user_org_id}"
# Return transformed arguments
return args.model_copy(update={"query": transformed_query})
# Example 3: Validate required parameters
if not args.database:
return ToolRejection(
reason="Database parameter is required for SQL execution"
)
# For all other tools or if no transformation needed, pass through
return args
# Usage example
async def example_usage():
"""Demonstrate using the RLS-enabled ToolRegistry."""
from vanna.capabilities.agent_memory import AgentMemory
# Create registry and register tool
registry = RLSToolRegistry()
sql_tool = SQLExecutionTool()
registry.register_local_tool(sql_tool, access_groups=[])
# Create a user with organization context
user = User(
user_id="user123",
metadata={"organization_id": 42}
)
# Create tool context
context = ToolContext(
user=user,
conversation_id="conv123",
request_id="req123",
agent_memory=AgentMemory(),
)
# Example 1: Query that will be transformed with RLS
from vanna.core.tool import ToolCall
tool_call = ToolCall(
id="call1",
name="execute_sql",
arguments={
"query": "SELECT * FROM users",
"database": "production"
}
)
result = await registry.execute(tool_call, context)
print(f"Result: {result.result_for_llm}")
# The query will be transformed to: SELECT * FROM users WHERE organization_id = 42
# Example 2: Query that will be rejected
tool_call_rejected = ToolCall(
id="call2",
name="execute_sql",
arguments={
"query": "SELECT * FROM restricted_table",
"database": "production"
}
)
result = await registry.execute(tool_call_rejected, context)
print(f"Rejected: {result.error}")
# Will return: "Access to 'restricted_table' is not permitted for your user group"
if __name__ == "__main__":
import asyncio
asyncio.run(example_usage())

View File

@@ -0,0 +1,25 @@
import type { StorybookConfig } from '@storybook/web-components-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-actions',
'@storybook/addon-controls',
'@storybook/addon-docs',
],
framework: {
name: '@storybook/web-components-vite',
options: {},
},
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
};
export default config;

View File

@@ -0,0 +1,6 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;500;600;700&family=Signika:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap"
rel="stylesheet"
/>

View File

@@ -0,0 +1,17 @@
import type { Preview } from '@storybook/web-components';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
docs: {
autodocs: 'tag',
},
},
};
export default preview;

View File

@@ -0,0 +1,422 @@
# Vanna Webcomponent Comprehensive Test Suite
This test suite validates all component types and update patterns in the vanna-webcomponent before pruning unused code.
## Overview
The test suite consists of:
- **`test_backend.py`**: Real Python backend that streams all component types
- **`test-comprehensive.html`**: Browser-based test interface with visual validation
- **Two test modes**: Rapid (stress test) and Realistic (with delays)
## Quick Start
### 1. Install Dependencies
```bash
cd submodule/vanna-webcomponent
pip install -r requirements-test.txt
```
### 2. Build the Webcomponent
```bash
npm run build
```
### 3. Start the Test Backend
```bash
# Realistic mode (with delays between components)
python test_backend.py --mode realistic
# Rapid mode (fast stress test)
python test_backend.py --mode rapid
```
The backend will start on `http://localhost:5555` and automatically serve the test page.
### 4. Open Test Interface
Simply open your browser to:
```
http://localhost:5555
```
The test page will load automatically!
### 5. Run the Test
1. Click **"Run Comprehensive Test"** button in the sidebar
2. Watch components render in real-time
3. Monitor the checklist - items check off as components render
4. Watch the console log for any errors
## Test Coverage
### Component Types Tested
The test exercises **all** rich component types with **19 different components**:
#### Primitive Components
- ✓ Text (with markdown)
- ✓ Badge
- ✓ Icon Text
#### Feedback Components
- ✓ Status Card (with all states: pending, running, completed, failed)
- ✓ Progress Display (0% → 50% → 100%)
- ✓ Progress Bar
- ✓ Status Indicator (with pulse animation)
- ✓ Notification (info, success, warning, error levels)
- ✓ Log Viewer (with info, warning, error logs)
#### Data Components
- ✓ Card (with buttons and actions)
- ✓ Task List (with status updates)
-**DataFrame** (tabular data with search/sort/filter/export)
-**Table** (structured data with explicit column definitions)
-**Chart** (Plotly charts: bar, line, scatter)
-**Code Block** (syntax highlighted code: Python, SQL, etc.)
#### Specialized Components
-**Artifact** (HTML/SVG interactive content)
#### Container Components
-**Container** (groups components in rows/columns)
#### Interactive Components
- ✓ Button (single)
- ✓ Button Group (horizontal/vertical)
- ✓ Button actions (click → backend response)
#### UI State Updates
- ✓ Status Bar Update (updates status bar above input)
- ✓ Task Tracker Update (adds/updates tasks in sidebar)
- ✓ Chat Input Update (changes placeholder/state)
### Update Operations Tested
For each component type, the test validates:
1. **Create** (`lifecycle: create`) - Initial component rendering
2. **Update** (`lifecycle: update`) - Incremental property updates
3. **Replace** - Full component replacement
4. **Remove** - Component removal from DOM
### Interactive Features Tested
- **Button Actions**: Clicking buttons sends actions to backend
- **Action Handling**: Backend receives actions and responds with new components
- **Round-trip Communication**: Full interaction loop validation
## Test Modes
### Realistic Mode (Default)
```bash
python test_backend.py --mode realistic
```
- Includes delays between component updates (0.2-0.5s)
- Simulates real conversation flow
- Easier to observe rendering behavior
- **Recommended for initial validation**
### Rapid Mode
```bash
python test_backend.py --mode rapid
```
- Minimal delays (0.05-0.1s)
- Stress tests rendering performance
- Validates no race conditions
- **Use for performance testing**
## Validation Checklist
The test interface provides real-time validation:
### ✅ Visual Checklist
- Automatically checks off components as they render
- Shows 19 component types
- Green checkmark = successfully rendered
### 📊 Metrics
- **Components Rendered**: Total unique component types
- **Updates Processed**: Total number of updates (create + update + replace)
- **Errors**: Console errors detected
### 🔴 Console Monitor
- Real-time console log display
- Errors highlighted in red
- Warnings in yellow
- Info messages in blue
### 🟢 Status Indicators
- **Backend Status**: Green = connected, Red = disconnected
- **Console Status**: Green = no errors, Red = errors detected
## Using for Webcomponent Pruning
The test suite is designed to validate that pruning doesn't break functionality:
### Pruning Workflow
1. **Run baseline test**:
```bash
python test_backend.py --mode realistic
# Browser: Open http://localhost:5555 and run test
# Verify: All 19 components render, 0 errors
```
2. **Identify cruft to remove**:
- Unused imports
- Dead code paths
- Deprecated components
- Development-only utilities
3. **Remove one piece of cruft**:
```bash
# Example: Remove unused import from vanna-chat.ts
# or delete unused utility file
```
4. **Rebuild**:
```bash
npm run build
```
5. **Refresh browser test**:
- Press F5 to reload test page
- Click "Run Comprehensive Test" again
- Check console for errors
- Verify all 12 components still render
6. **If green → continue; if red → investigate**:
- Green (no errors): Commit the change, continue pruning
- Red (errors): Revert change, that code was actually needed
7. **Repeat until clean**: Continue removing cruft until webcomponent is minimal
### What to Prune
Look for these common types of cruft:
- ❌ **Unused imports**: Components imported but never used
- ❌ **Development utilities**: Debug helpers, test mocks in production code
- ❌ **Deprecated components**: Old component versions no longer referenced
- ❌ **Unused CSS**: Styles for removed components
- ❌ **Dead code paths**: Conditional logic that's never executed
- ❌ **Commented code**: Old implementations that are commented out
- ❌ **Storybook-only code**: Utilities only used in stories, not production
### What NOT to Prune
Be careful with these:
- ✅ **Base component renderers**: Even if rarely used, may be needed
- ✅ **ComponentRegistry entries**: Needed for dynamic component lookup
- ✅ **Shadow DOM utilities**: Required for web components
- ✅ **Event handlers**: May be used by runtime events
- ✅ **Type definitions**: Used at compile time even if not runtime
## Customizing the Test
### Add More Component Tests
Edit `test_backend.py` and add new test functions:
```python
async def test_my_component(conversation_id: str, request_id: str, mode: str):
"""Test my custom component."""
my_component = MyComponent(
id=str(uuid.uuid4()),
# ... component properties
)
yield await yield_chunk(my_component, conversation_id, request_id)
await delay(mode)
# Then add to run_comprehensive_test():
async for chunk in test_my_component(conversation_id, request_id, mode):
yield chunk
```
### Modify Test Delays
In `test_backend.py`, adjust the `delay()` function:
```python
async def delay(mode: str, short: float = 0.1, long: float = 0.5):
if mode == "realistic":
await asyncio.sleep(long) # Adjust long delay here
elif mode == "rapid":
await asyncio.sleep(short) # Adjust short delay here
```
### Add Custom Validation
Edit `test-comprehensive.html` and add custom validation logic:
```javascript
// Add to MutationObserver callback
const componentType = node.getAttribute('data-component-type');
if (componentType === 'my_component') {
// Custom validation for my_component
console.log('My component rendered!');
}
```
## Troubleshooting
### Backend won't start
**Error**: `ModuleNotFoundError: No module named 'vanna'`
**Solution**: Make sure vanna is in the Python path:
```bash
cd submodule/vanna-webcomponent
python test_backend.py # Already adds ../vanna/src to sys.path
```
### Frontend shows "Backend not responding"
**Solutions**:
1. Check backend is running: `curl http://localhost:5555/health`
2. Check CORS is enabled (should be by default)
3. Verify port 5555 is not in use: `lsof -i :5555`
### Components not rendering
**Check**:
1. Browser console for errors (F12)
2. Webcomponent is built: `ls dist/`
3. Test HTML is loading: `<script type="module" src="./dist/index.js"></script>`
### Test page is blank
**Solutions**:
1. Check you're serving from the right directory:
```bash
cd submodule/vanna-webcomponent
python -m http.server 8080
```
2. Open correct URL: `http://localhost:8080/test-comprehensive.html`
3. Check browser console for 404 errors
### Checklist not updating
The checklist tracks components by their `data-component-type` attribute. If components don't have this attribute, they won't be tracked.
**Verify**: Open browser DevTools and inspect rendered components for `data-component-type`.
## Advanced Usage
### Run Backend on Different Port
```bash
python test_backend.py --port 8000
```
Then update `test-comprehensive.html`:
```html
<vanna-chat
api-url="http://localhost:8000"
...
></vanna-chat>
```
### Enable Debug Logging
Add to `test_backend.py`:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
```
### Run Type Checking
Validate the backend code with mypy:
```bash
python -m mypy test_backend.py
```
This catches type errors before runtime (e.g., wrong field names in Pydantic models).
### Test Specific Component Only
Modify `run_comprehensive_test()` to only run specific tests:
```python
async def run_comprehensive_test(conversation_id, request_id, mode):
# Comment out tests you don't want to run
async for chunk in test_status_card(conversation_id, request_id, mode):
yield chunk
# async for chunk in test_progress_display(...): # Disabled
# yield chunk
```
## Architecture
### Backend Flow
1. FastAPI receives POST to `/api/vanna/v2/chat_sse`
2. `chat_sse()` creates async generator
3. Generator yields components wrapped in `ChatStreamChunk`
4. Each chunk serialized to SSE format: `data: {json}\n\n`
5. Stream ends with `data: [DONE]\n\n`
### Frontend Flow
1. `<vanna-chat>` web component connects to backend
2. Opens SSE connection to `/api/vanna/v2/chat_sse`
3. Receives chunks, parses JSON
4. `ComponentManager` processes updates
5. `ComponentRegistry` renders HTML elements
6. Elements appended to shadow DOM container
7. MutationObserver detects new components
8. Checklist updates automatically
### Button Action Flow
1. User clicks button in frontend
2. Button's `action` property sent as new message
3. Backend receives message via `/api/vanna/v2/chat_sse` POST
4. `handle_action_message()` processes action
5. Response components streamed back
6. Frontend renders response
## Files
- **`test_backend.py`** - Python FastAPI backend (400 lines)
- **`test-comprehensive.html`** - Browser test interface (500 lines)
- **`requirements-test.txt`** - Python dependencies
- **`TEST_README.md`** - This documentation
## Next Steps
After validating the webcomponent with this test suite:
1. **Run baseline test** - Verify all components work before pruning
2. **Identify cruft** - Find unused code in the webcomponent
3. **Prune iteratively** - Remove one piece at a time, test after each change
4. **Commit clean code** - Once pruned, commit the cleaned webcomponent
5. **Copy to vanna package** - Integrate cleaned webcomponent into vanna Python package
## Support
If you encounter issues with the test suite:
1. Check this README's Troubleshooting section
2. Verify all dependencies are installed
3. Ensure you're in the correct directory
4. Check browser and terminal console output
---
**Happy Testing!** 🧪

View File

@@ -0,0 +1,57 @@
{
"name": "@vanna/webcomponent",
"version": "2.0.2",
"description": "Lit-based web components for Vanna User Agents",
"main": "dist/vanna-components.js",
"scripts": {
"sync-version": "node scripts/sync-version.js",
"dev": "vite",
"build": "npm run sync-version && tsc && vite build",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"vanna",
"ai",
"sql",
"web-components",
"lit",
"chat",
"llm",
"natural-language"
],
"author": "Zain Hoda <zain@vanna.ai>",
"license": "MIT",
"type": "commonjs",
"repository": {
"type": "git",
"url": "https://github.com/vanna-ai/vanna.git",
"directory": "frontends/webcomponent"
},
"homepage": "https://github.com/vanna-ai/vanna",
"bugs": {
"url": "https://github.com/vanna-ai/vanna/issues"
},
"files": [
"dist",
"src"
],
"dependencies": {
"lit": "^3.3.1",
"plotly.js-dist-min": "^3.1.0"
},
"devDependencies": {
"@storybook/addon-actions": "^8.6.14",
"@storybook/addon-controls": "^8.6.14",
"@storybook/addon-docs": "^8.6.14",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/web-components": "^8.6.14",
"@storybook/web-components-vite": "^8.6.14",
"@types/plotly.js-dist-min": "^2.3.4",
"storybook": "^8.6.14",
"typescript": "^5.9.2",
"vite": "^7.1.5"
}
}

View File

@@ -0,0 +1,8 @@
# Test backend requirements for vanna-webcomponent comprehensive testing
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
pydantic>=2.0.0
# Note: The vanna package itself will be imported from ../vanna/src
# No need to install it separately for local testing

View File

@@ -0,0 +1,63 @@
/**
* Sync version from pyproject.toml to package.json
*
* This ensures the webcomponent version always matches the Python package version.
* Single source of truth: pyproject.toml
*
* Usage: node scripts/sync-version.js
*/
const fs = require('fs');
const path = require('path');
// Paths relative to this script
const PYPROJECT_PATH = path.join(__dirname, '../../../pyproject.toml');
const PACKAGE_JSON_PATH = path.join(__dirname, '../package.json');
function extractVersionFromPyproject(content) {
// Match: version = "2.0.0"
const match = content.match(/^version\s*=\s*"([^"]+)"/m);
if (!match) {
throw new Error('Could not find version in pyproject.toml');
}
return match[1];
}
function updatePackageJsonVersion(packageJsonPath, newVersion) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const oldVersion = packageJson.version;
packageJson.version = newVersion;
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n',
'utf8'
);
return { oldVersion, newVersion };
}
function main() {
try {
// Read pyproject.toml
const pyprojectContent = fs.readFileSync(PYPROJECT_PATH, 'utf8');
const version = extractVersionFromPyproject(pyprojectContent);
// Update package.json
const { oldVersion, newVersion } = updatePackageJsonVersion(PACKAGE_JSON_PATH, version);
if (oldVersion !== newVersion) {
console.log(`✓ Version synced: ${oldVersion}${newVersion}`);
} else {
console.log(`✓ Version already in sync: ${newVersion}`);
}
process.exit(0);
} catch (error) {
console.error(`✗ Version sync failed: ${error.message}`);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,532 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { ComponentManager, ComponentUpdate } from './rich-component-system';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
const meta: Meta = {
title: 'Rich Components/Buttons',
parameters: {
layout: 'padded',
backgrounds: {
default: 'dark',
values: [
{ name: 'light', value: '#f5f7fa' },
{ name: 'dark', value: 'rgb(11, 15, 25)' },
],
},
},
};
export default meta;
type Story = StoryObj;
const ensureTokenStyles = () => {
if (document.getElementById('vanna-token-style')) {
return;
}
const style = document.createElement('style');
style.id = 'vanna-token-style';
style.textContent = vannaDesignTokens.cssText.replace(/:host/g, '.vanna-tokens');
document.head.appendChild(style);
};
const createContainer = () => {
ensureTokenStyles();
const container = document.createElement('div');
container.className = 'vanna-tokens';
container.style.cssText = `
padding: var(--vanna-space-5, 20px);
max-width: 800px;
margin: 0 auto;
background: var(--vanna-background-default);
border-radius: var(--vanna-border-radius-lg);
box-shadow: var(--vanna-shadow-md);
`;
return container;
};
const createManager = (container: HTMLElement) => new ComponentManager(container);
const renderComponent = (manager: ComponentManager, component: any) => {
const update: ComponentUpdate = {
operation: 'create',
target_id: component.id,
component,
timestamp: new Date().toISOString(),
} as ComponentUpdate;
manager.processUpdate(update);
};
const withDefaults = (component: any) => ({
layout: { position: 'append', size: {}, z_index: 0, classes: [] },
theme: {},
lifecycle: 'create',
...component,
});
const addMockVannaChat = (container: HTMLElement) => {
// Create a mock vanna-chat element with sendMessage method
const mockVannaChat = document.createElement('div');
mockVannaChat.setAttribute('id', 'mock-vanna-chat');
// Store the original querySelector
const originalQuerySelector = document.querySelector.bind(document);
// Override querySelector to return our mock when looking for vanna-chat
document.querySelector = function(selector: string) {
if (selector === 'vanna-chat') {
return mockVannaChat as any;
}
return originalQuerySelector(selector);
} as any;
// Add sendMessage method that logs to console and shows in UI
(mockVannaChat as any).sendMessage = (message: string) => {
console.log('📤 Button clicked - Message:', message);
// Show a visual feedback in the storybook
const feedback = document.createElement('div');
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
font-family: monospace;
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
feedback.textContent = `Message sent: ${message}`;
// Add animation
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(feedback);
setTimeout(() => {
feedback.style.opacity = '0';
feedback.style.transition = 'opacity 0.3s ease-out';
setTimeout(() => feedback.remove(), 300);
}, 2000);
};
container.appendChild(mockVannaChat);
return mockVannaChat;
};
export const SingleButtons: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
addMockVannaChat(container);
// Add title
const title = document.createElement('h2');
title.textContent = 'Single Button Components';
title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';
container.appendChild(title);
const buttons = [
withDefaults({
id: 'primary-button',
type: 'button',
data: {
label: 'Primary Action',
action: 'primary_action',
variant: 'primary',
size: 'medium',
},
}),
withDefaults({
id: 'secondary-button',
type: 'button',
data: {
label: 'Save Draft',
action: 'save_draft',
variant: 'secondary',
size: 'medium',
icon: '💾',
icon_position: 'left',
},
}),
withDefaults({
id: 'success-button',
type: 'button',
data: {
label: 'Approve',
action: 'approve',
variant: 'success',
size: 'medium',
icon: '✓',
},
}),
withDefaults({
id: 'warning-button',
type: 'button',
data: {
label: 'Caution',
action: 'warning',
variant: 'warning',
size: 'medium',
icon: '⚠️',
},
}),
withDefaults({
id: 'error-button',
type: 'button',
data: {
label: 'Delete',
action: 'delete',
variant: 'error',
size: 'medium',
icon: '🗑️',
},
}),
withDefaults({
id: 'ghost-button',
type: 'button',
data: {
label: 'Ghost Style',
action: 'ghost',
variant: 'ghost',
icon: '👻',
},
}),
withDefaults({
id: 'link-button',
type: 'button',
data: {
label: 'Learn More',
action: 'learn_more',
variant: 'link',
},
}),
withDefaults({
id: 'loading-button',
type: 'button',
data: {
label: 'Processing...',
action: 'loading',
variant: 'primary',
loading: true,
},
}),
withDefaults({
id: 'disabled-button',
type: 'button',
data: {
label: 'Disabled',
action: 'disabled',
variant: 'secondary',
disabled: true,
},
}),
];
buttons.forEach((component) => {
renderComponent(manager, component);
// Add some spacing
const spacer = document.createElement('div');
spacer.style.height = '12px';
container.appendChild(spacer);
});
// Add instruction
const instruction = document.createElement('p');
instruction.textContent = 'Click any button to see the message it sends (wrapped in square brackets)';
instruction.style.cssText = 'margin-top: 20px; color: var(--vanna-text-secondary); font-style: italic;';
container.appendChild(instruction);
return container;
},
};
export const ButtonSizes: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
addMockVannaChat(container);
const title = document.createElement('h2');
title.textContent = 'Button Sizes';
title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';
container.appendChild(title);
const sizes = ['small', 'medium', 'large'];
sizes.forEach((size) => {
const button = withDefaults({
id: `button-${size}`,
type: 'button',
data: {
label: `${size.charAt(0).toUpperCase() + size.slice(1)} Button`,
action: `${size}_action`,
variant: 'primary',
size,
icon: '⭐',
},
});
renderComponent(manager, button);
const spacer = document.createElement('div');
spacer.style.height = '12px';
container.appendChild(spacer);
});
return container;
},
};
export const ButtonGroups: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
addMockVannaChat(container);
const title = document.createElement('h2');
title.textContent = 'Button Group Components';
title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';
container.appendChild(title);
// Horizontal action group
const actionGroup = withDefaults({
id: 'action-group',
type: 'button_group',
data: {
buttons: [
{
label: 'Accept',
action: 'accept',
variant: 'success',
icon: '✓',
},
{
label: 'Reject',
action: 'reject',
variant: 'error',
icon: '✗',
},
{
label: 'Cancel',
action: 'cancel',
variant: 'secondary',
},
],
orientation: 'horizontal',
spacing: 'medium',
align: 'left',
},
});
const sectionTitle1 = document.createElement('h3');
sectionTitle1.textContent = 'Horizontal Action Group';
sectionTitle1.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';
container.appendChild(sectionTitle1);
renderComponent(manager, actionGroup);
// Centered navigation
const navigationGroup = withDefaults({
id: 'navigation-group',
type: 'button_group',
data: {
buttons: [
{
label: 'Back',
action: 'back',
variant: 'ghost',
icon: '←',
},
{
label: 'Continue',
action: 'continue',
variant: 'primary',
icon: '→',
icon_position: 'right',
},
],
orientation: 'horizontal',
spacing: 'large',
align: 'center',
},
});
const sectionTitle2 = document.createElement('h3');
sectionTitle2.textContent = 'Centered Navigation';
sectionTitle2.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';
container.appendChild(sectionTitle2);
renderComponent(manager, navigationGroup);
// Vertical options
const verticalGroup = withDefaults({
id: 'vertical-group',
type: 'button_group',
data: {
buttons: [
{ label: 'Option 1', action: 'option1', variant: 'secondary' },
{ label: 'Option 2', action: 'option2', variant: 'secondary' },
{ label: 'Option 3', action: 'option3', variant: 'secondary' },
],
orientation: 'vertical',
spacing: 'small',
align: 'left',
},
});
const sectionTitle3 = document.createElement('h3');
sectionTitle3.textContent = 'Vertical Options';
sectionTitle3.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';
container.appendChild(sectionTitle3);
renderComponent(manager, verticalGroup);
// Toolbar
const toolbarGroup = withDefaults({
id: 'toolbar-group',
type: 'button_group',
data: {
buttons: [
{
label: 'New',
action: 'new',
variant: 'primary',
icon: '',
size: 'small',
},
{
label: 'Edit',
action: 'edit',
variant: 'secondary',
icon: '✏️',
size: 'small',
},
{
label: 'Delete',
action: 'delete',
variant: 'error',
icon: '🗑️',
size: 'small',
},
{
label: 'Share',
action: 'share',
variant: 'ghost',
icon: '🔗',
size: 'small',
},
],
orientation: 'horizontal',
spacing: 'small',
align: 'left',
},
});
const sectionTitle4 = document.createElement('h3');
sectionTitle4.textContent = 'Toolbar (Small Buttons)';
sectionTitle4.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';
container.appendChild(sectionTitle4);
renderComponent(manager, toolbarGroup);
// Full width confirmation
const confirmationGroup = withDefaults({
id: 'confirmation-group',
type: 'button_group',
data: {
buttons: [
{ label: 'Yes', action: 'yes', variant: 'success' },
{ label: 'No', action: 'no', variant: 'error' },
],
orientation: 'horizontal',
spacing: 'medium',
align: 'space-between',
full_width: true,
},
});
const sectionTitle5 = document.createElement('h3');
sectionTitle5.textContent = 'Full Width Confirmation';
sectionTitle5.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';
container.appendChild(sectionTitle5);
renderComponent(manager, confirmationGroup);
// Add instruction
const instruction = document.createElement('p');
instruction.textContent = 'Click any button in the groups to see the message it sends';
instruction.style.cssText = 'margin-top: 20px; color: var(--vanna-text-secondary); font-style: italic;';
container.appendChild(instruction);
return container;
},
};
export const InteractiveDemo: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
addMockVannaChat(container);
const title = document.createElement('h2');
title.textContent = 'Interactive Button Demo';
title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';
container.appendChild(title);
const description = document.createElement('p');
description.textContent = 'This demo shows how buttons send messages with their labels wrapped in square brackets.';
description.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-secondary);';
container.appendChild(description);
// Simple choice buttons
const choiceGroup = withDefaults({
id: 'choice-group',
type: 'button_group',
data: {
buttons: [
{ label: 'Okay', action: 'okay', variant: 'primary' },
{ label: 'Not now', action: 'not_now', variant: 'secondary' },
{ label: 'Never', action: 'never', variant: 'ghost' },
],
orientation: 'horizontal',
spacing: 'medium',
align: 'center',
},
});
renderComponent(manager, choiceGroup);
const codeExample = document.createElement('pre');
codeExample.textContent = `// When you click "Okay", the message sent is: [Okay]
// When you click "Not now", the message sent is: [Not now]
// When you click "Never", the message sent is: [Never]`;
codeExample.style.cssText = `
margin-top: 20px;
padding: 12px;
background: rgba(0, 0, 0, 0.3);
border-radius: 6px;
color: #a0aec0;
font-size: 12px;
font-family: 'Courier New', monospace;
overflow-x: auto;
`;
container.appendChild(codeExample);
return container;
},
};

View File

@@ -0,0 +1,564 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { ComponentManager, ComponentUpdate } from './rich-component-system';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
import { richComponentStyleText } from '../styles/rich-component-styles.js';
const meta: Meta = {
title: 'Rich Components/DataFrame',
parameters: {
layout: 'padded',
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#f5f7fa' },
{ name: 'dark', value: 'rgb(11, 15, 25)' },
],
},
},
argTypes: {
theme: {
control: { type: 'select' },
options: ['light', 'dark'],
},
striped: {
control: { type: 'boolean' },
},
bordered: {
control: { type: 'boolean' },
},
compact: {
control: { type: 'boolean' },
},
searchable: {
control: { type: 'boolean' },
},
sortable: {
control: { type: 'boolean' },
},
exportable: {
control: { type: 'boolean' },
},
},
};
export default meta;
type Story = StoryObj;
const ensureTokenStyles = () => {
if (document.getElementById('vanna-token-style')) {
return;
}
const style = document.createElement('style');
style.id = 'vanna-token-style';
style.textContent = vannaDesignTokens.cssText.replace(/:host/g, '.vanna-tokens');
document.head.appendChild(style);
};
const ensureRichComponentStyles = () => {
if (document.getElementById('vanna-rich-component-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'vanna-rich-component-styles';
style.textContent = richComponentStyleText;
document.head.appendChild(style);
};
const createContainer = () => {
ensureTokenStyles();
ensureRichComponentStyles();
const container = document.createElement('div');
container.className = 'vanna-tokens';
container.style.cssText = `
padding: var(--vanna-space-5, 20px);
max-width: 1200px;
margin: 0 auto;
background: var(--vanna-background-default, #0b0f19);
border-radius: var(--vanna-border-radius-lg, 8px);
box-shadow: var(--vanna-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
color: var(--vanna-foreground-default, #ffffff);
`;
// Add some additional DataFrame-specific debugging styles
const additionalStyles = document.createElement('style');
additionalStyles.textContent = `
/* Ensure DataFrame styles are applied with higher specificity */
.vanna-tokens {
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .rich-dataframe {
background: var(--vanna-background-default, #0b0f19) !important;
border: 1px solid var(--vanna-outline-default, #333) !important;
border-radius: var(--vanna-border-radius-lg, 8px) !important;
overflow: hidden !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .dataframe-table {
width: 100% !important;
border-collapse: collapse !important;
font-size: 0.875rem !important;
color: var(--vanna-foreground-default, #ffffff) !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .dataframe-table th {
background: var(--vanna-background-higher, #1a1f2e) !important;
color: var(--vanna-foreground-default, #ffffff) !important;
font-weight: 600 !important;
text-align: left !important;
padding: 12px 16px !important;
border-bottom: 2px solid var(--vanna-outline-default, #333) !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .dataframe-table td {
padding: 12px 16px !important;
border-bottom: 1px solid var(--vanna-outline-dimmer, #222) !important;
color: var(--vanna-foreground-default, #ffffff) !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .dataframe-table.striped tbody tr:nth-child(even) {
background: rgba(255, 255, 255, 0.02) !important;
}
.vanna-tokens .dataframe-header {
padding: 16px 20px !important;
background: var(--vanna-background-higher, #1a1f2e) !important;
border-bottom: 1px solid var(--vanna-outline-default, #333) !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .dataframe-title {
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
color: var(--vanna-foreground-default, #ffffff) !important;
font-weight: 600 !important;
}
.vanna-tokens .dataframe-description {
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
color: var(--vanna-foreground-dimmer, #b1bac4) !important;
}
.vanna-tokens .dataframe-actions {
padding: 12px 20px !important;
background: var(--vanna-background-default, #0b0f19) !important;
border-bottom: 1px solid var(--vanna-outline-dimmer, #222) !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
gap: 12px !important;
}
.vanna-tokens .search-input {
width: 100% !important;
padding: 8px 12px !important;
border: 1px solid var(--vanna-outline-default, #333) !important;
border-radius: 6px !important;
background: var(--vanna-background-default, #0b0f19) !important;
color: var(--vanna-foreground-default, #ffffff) !important;
font-size: 0.875rem !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
.vanna-tokens .export-btn {
padding: 8px 12px !important;
border: 1px solid var(--vanna-outline-default, #333) !important;
border-radius: 6px !important;
background: var(--vanna-background-default, #0b0f19) !important;
color: var(--vanna-foreground-default, #ffffff) !important;
cursor: pointer !important;
font-size: 0.875rem !important;
font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif) !important;
}
`;
document.head.appendChild(additionalStyles);
return container;
};
const createManager = (container: HTMLElement) => new ComponentManager(container);
const renderComponent = (manager: ComponentManager, component: any) => {
const update: ComponentUpdate = {
operation: 'create',
target_id: component.id,
component,
timestamp: new Date().toISOString(),
} as ComponentUpdate;
manager.processUpdate(update);
};
const withDefaults = (component: any) => ({
layout: { position: 'append', size: {}, z_index: 0, classes: [] },
theme: {},
lifecycle: 'create',
timestamp: new Date().toISOString(),
visible: true,
interactive: false,
children: [],
...component,
});
// Sample data sets
const employeeData = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 28, city: 'New York', salary: 75000, active: true, department: 'Engineering' },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', age: 34, city: 'San Francisco', salary: 85000, active: true, department: 'Product' },
{ id: 3, name: 'Carol Davis', email: 'carol@example.com', age: 29, city: 'Chicago', salary: 70000, active: false, department: 'Design' },
{ id: 4, name: 'David Wilson', email: 'david@example.com', age: 42, city: 'Austin', salary: 90000, active: true, department: 'Engineering' },
{ id: 5, name: 'Eve Brown', email: 'eve@example.com', age: 31, city: 'Seattle', salary: 80000, active: true, department: 'Marketing' },
{ id: 6, name: 'Frank Miller', email: 'frank@example.com', age: 38, city: 'Boston', salary: 95000, active: false, department: 'Sales' },
{ id: 7, name: 'Grace Lee', email: 'grace@example.com', age: 26, city: 'Denver', salary: 65000, active: true, department: 'HR' },
{ id: 8, name: 'Henry Taylor', email: 'henry@example.com', age: 33, city: 'Portland', salary: 72000, active: true, department: 'Engineering' },
{ id: 9, name: 'Ivy Chen', email: 'ivy@example.com', age: 27, city: 'Los Angeles', salary: 78000, active: true, department: 'Product' },
{ id: 10, name: 'Jack Anderson', email: 'jack@example.com', age: 35, city: 'Miami', salary: 82000, active: false, department: 'Finance' },
];
const sqlQueryData = [
{ TrackId: 1, Name: 'For Those About To Rock (We Salute You)', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 343719, Bytes: 11170334, UnitPrice: 0.99 },
{ TrackId: 2, Name: 'Balls to the Wall', AlbumId: 2, MediaTypeId: 2, GenreId: 1, Composer: null, Milliseconds: 342562, Bytes: 5510424, UnitPrice: 0.99 },
{ TrackId: 3, Name: 'Fast As a Shark', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 230619, Bytes: 3990994, UnitPrice: 0.99 },
{ TrackId: 4, Name: 'Restless and Wild', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 252051, Bytes: 4331779, UnitPrice: 0.99 },
{ TrackId: 5, Name: 'Princess of the Dawn', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'Deaffy & R.A. Smith-Diesel', Milliseconds: 375418, Bytes: 6290521, UnitPrice: 0.99 },
{ TrackId: 6, Name: 'Put The Finger On You', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 205662, Bytes: 6713451, UnitPrice: 0.99 },
{ TrackId: 7, Name: "Let's Get It Up", AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 233926, Bytes: 7636561, UnitPrice: 0.99 },
{ TrackId: 8, Name: 'Inject The Venom', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 210834, Bytes: 6852860, UnitPrice: 0.99 },
];
export const BasicDataFrame: Story = {
render: (args) => {
const container = createContainer();
const manager = createManager(container);
const component = withDefaults({
id: 'basic-dataframe',
type: 'dataframe',
data: {
data: employeeData.slice(0, 5),
columns: ['id', 'name', 'email', 'age', 'city', 'department'],
title: 'Employee Records',
description: 'Basic employee data with essential information',
row_count: 5,
column_count: 6,
striped: args.striped ?? true,
bordered: args.bordered ?? true,
compact: args.compact ?? false,
searchable: args.searchable ?? false,
sortable: args.sortable ?? false,
exportable: args.exportable ?? false,
column_types: {
id: 'number',
name: 'string',
email: 'string',
age: 'number',
city: 'string',
department: 'string'
}
},
});
renderComponent(manager, component);
return container;
},
args: {
striped: true,
bordered: true,
compact: false,
searchable: false,
sortable: false,
exportable: false,
},
};
export const InteractiveDataFrame: Story = {
render: (args) => {
const container = createContainer();
const manager = createManager(container);
const component = withDefaults({
id: 'interactive-dataframe',
type: 'dataframe',
data: {
data: employeeData,
columns: ['id', 'name', 'email', 'age', 'city', 'salary', 'active', 'department'],
title: 'Interactive Employee Database',
description: 'Full dataset with search, sort, and export functionality',
row_count: employeeData.length,
column_count: 8,
striped: args.striped ?? true,
bordered: args.bordered ?? true,
compact: args.compact ?? false,
searchable: args.searchable ?? true,
sortable: args.sortable ?? true,
exportable: args.exportable ?? true,
max_rows_displayed: 8,
column_types: {
id: 'number',
name: 'string',
email: 'string',
age: 'number',
city: 'string',
salary: 'number',
active: 'boolean',
department: 'string'
}
},
});
renderComponent(manager, component);
return container;
},
args: {
striped: true,
bordered: true,
compact: false,
searchable: true,
sortable: true,
exportable: true,
},
};
export const SQLQueryResults: Story = {
render: (args) => {
const container = createContainer();
const manager = createManager(container);
const component = withDefaults({
id: 'sql-dataframe',
type: 'dataframe',
data: {
data: sqlQueryData,
columns: ['TrackId', 'Name', 'AlbumId', 'MediaTypeId', 'GenreId', 'Composer', 'Milliseconds', 'Bytes', 'UnitPrice'],
title: 'SQL Query Results',
description: 'SELECT * FROM Track LIMIT 8',
row_count: sqlQueryData.length,
column_count: 9,
striped: args.striped ?? true,
bordered: args.bordered ?? true,
compact: args.compact ?? false,
searchable: args.searchable ?? true,
sortable: args.sortable ?? true,
exportable: args.exportable ?? true,
column_types: {
TrackId: 'number',
Name: 'string',
AlbumId: 'number',
MediaTypeId: 'number',
GenreId: 'number',
Composer: 'string',
Milliseconds: 'number',
Bytes: 'number',
UnitPrice: 'number'
}
},
});
renderComponent(manager, component);
return container;
},
args: {
striped: true,
bordered: true,
compact: false,
searchable: true,
sortable: true,
exportable: true,
},
};
export const CompactView: Story = {
render: (args) => {
const container = createContainer();
const manager = createManager(container);
const component = withDefaults({
id: 'compact-dataframe',
type: 'dataframe',
data: {
data: employeeData.slice(0, 6),
columns: ['id', 'name', 'city', 'active'],
title: 'Compact Employee View',
description: 'Space-efficient display with essential columns only',
row_count: 6,
column_count: 4,
striped: args.striped ?? true,
bordered: args.bordered ?? false,
compact: args.compact ?? true,
searchable: args.searchable ?? false,
sortable: args.sortable ?? true,
exportable: args.exportable ?? false,
column_types: {
id: 'number',
name: 'string',
city: 'string',
active: 'boolean'
}
},
});
renderComponent(manager, component);
return container;
},
args: {
striped: true,
bordered: false,
compact: true,
searchable: false,
sortable: true,
exportable: false,
},
};
export const EmptyDataFrame: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
const component = withDefaults({
id: 'empty-dataframe',
type: 'dataframe',
data: {
data: [],
columns: [],
title: 'No Data Available',
description: 'This dataset contains no records',
row_count: 0,
column_count: 0,
},
});
renderComponent(manager, component);
return container;
},
};
export const LargeDataset: Story = {
render: (args) => {
const container = createContainer();
const manager = createManager(container);
// Generate a larger dataset
const largeData = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
score: Math.floor(Math.random() * 100),
category: ['A', 'B', 'C'][i % 3],
active: Math.random() > 0.3,
created_date: new Date(2024, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toISOString().split('T')[0]
}));
const component = withDefaults({
id: 'large-dataframe',
type: 'dataframe',
data: {
data: largeData,
columns: ['id', 'name', 'email', 'score', 'category', 'active', 'created_date'],
title: 'Large Dataset',
description: '50 records with pagination and search',
row_count: largeData.length,
column_count: 7,
striped: args.striped ?? true,
bordered: args.bordered ?? true,
compact: args.compact ?? false,
searchable: args.searchable ?? true,
sortable: args.sortable ?? true,
exportable: args.exportable ?? true,
max_rows_displayed: 15,
column_types: {
id: 'number',
name: 'string',
email: 'string',
score: 'number',
category: 'string',
active: 'boolean',
created_date: 'date'
}
},
});
renderComponent(manager, component);
return container;
},
args: {
striped: true,
bordered: true,
compact: false,
searchable: true,
sortable: true,
exportable: true,
},
};
export const DataTypesShowcase: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
const typesData = [
{
id: 1,
name: 'Alice',
score: 95.5,
active: true,
created: '2024-01-15',
notes: 'Excellent performance',
tags: null
},
{
id: 2,
name: 'Bob',
score: 87.2,
active: false,
created: '2024-02-20',
notes: 'Good but needs improvement',
tags: 'priority,review'
},
{
id: 3,
name: 'Carol',
score: 92.8,
active: true,
created: '2024-03-10',
notes: null,
tags: 'star-performer'
},
];
const component = withDefaults({
id: 'types-dataframe',
type: 'dataframe',
data: {
data: typesData,
columns: ['id', 'name', 'score', 'active', 'created', 'notes', 'tags'],
title: 'Data Types Showcase',
description: 'Demonstrates different column data types and null handling',
row_count: typesData.length,
column_count: 7,
striped: true,
bordered: true,
searchable: true,
sortable: true,
exportable: true,
column_types: {
id: 'number',
name: 'string',
score: 'number',
active: 'boolean',
created: 'date',
notes: 'string',
tags: 'string'
}
},
});
renderComponent(manager, component);
return container;
},
};

View File

@@ -0,0 +1,272 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './plotly-chart';
const meta: Meta = {
title: 'Rich Components/Plotly Chart',
component: 'plotly-chart',
parameters: {
layout: 'padded',
backgrounds: {
default: 'light',
values: [
{ name: 'dark', value: 'rgb(11, 15, 25)' },
{ name: 'light', value: '#f5f7fa' },
],
},
},
argTypes: {
theme: {
control: 'select',
options: ['light', 'dark']
},
loading: { control: 'boolean' },
error: { control: 'text' },
},
};
export default meta;
type Story = StoryObj;
export const LineChart: Story = {
args: {
theme: 'light',
loading: false,
error: '',
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
?loading=${args.loading}
error=${args.error}
.data=${[
{
x: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
y: [20, 14, 23, 25, 22, 16],
type: 'scatter',
mode: 'lines+markers',
name: 'Sales',
line: { color: 'rgb(0, 123, 255)' }
}
]}
.layout=${{
xaxis: { title: 'Month' },
yaxis: { title: 'Sales (in thousands)' }
}}>
</plotly-chart>
</div>
`,
};
export const BarChart: Story = {
args: {
theme: 'light',
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
.data=${[
{
x: ['Product A', 'Product B', 'Product C', 'Product D'],
y: [45, 32, 28, 35],
type: 'bar',
name: 'Revenue',
marker: {
color: ['rgb(16, 185, 129)', 'rgb(0, 123, 255)', 'rgb(245, 158, 11)', 'rgb(239, 68, 68)']
}
}
]}
.layout=${{
xaxis: { title: 'Products' },
yaxis: { title: 'Revenue ($M)' }
}}>
</plotly-chart>
</div>
`,
};
export const ScatterPlot: Story = {
args: {
theme: 'light',
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
.data=${[
{
x: [85, 78, 92, 88, 76, 95, 82, 89, 93, 79],
y: [450, 320, 580, 490, 280, 650, 380, 520, 610, 310],
type: 'scatter',
mode: 'markers',
name: 'Business Units',
marker: {
size: 12,
color: 'rgb(0, 123, 255)',
opacity: 0.7
}
}
]}
.layout=${{
xaxis: { title: 'Customer Satisfaction Score' },
yaxis: { title: 'Revenue ($K)' }
}}>
</plotly-chart>
</div>
`,
};
export const MultipleLines: Story = {
args: {
theme: 'light',
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
.data=${[
{
x: ['Q1', 'Q2', 'Q3', 'Q4'],
y: [85, 88, 92, 89],
type: 'scatter',
mode: 'lines+markers',
name: 'User Engagement',
line: { color: 'rgb(16, 185, 129)' }
},
{
x: ['Q1', 'Q2', 'Q3', 'Q4'],
y: [65, 72, 78, 81],
type: 'scatter',
mode: 'lines+markers',
name: 'Conversion Rate',
line: { color: 'rgb(0, 123, 255)' }
},
{
x: ['Q1', 'Q2', 'Q3', 'Q4'],
y: [42, 48, 55, 58],
type: 'scatter',
mode: 'lines+markers',
name: 'Customer Retention',
line: { color: 'rgb(245, 158, 11)' }
}
]}
.layout=${{
xaxis: { title: 'Quarter' },
yaxis: { title: 'Percentage (%)' }
}}>
</plotly-chart>
</div>
`,
};
export const LoadingState: Story = {
args: {
theme: 'light',
loading: true,
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
?loading=${args.loading}
.data=${[]}>
</plotly-chart>
</div>
`,
};
export const ErrorState: Story = {
args: {
theme: 'light',
error: 'Failed to load chart data from API',
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
error=${args.error}
.data=${[]}>
</plotly-chart>
</div>
`,
};
export const LightTheme: Story = {
args: {
theme: 'light',
},
parameters: {
backgrounds: { default: 'light' }
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
.data=${[
{
x: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
y: [20, 14, 23, 25, 22, 16],
type: 'scatter',
mode: 'lines+markers',
name: 'Sales',
line: { color: 'rgb(0, 123, 255)' }
}
]}
.layout=${{
xaxis: { title: 'Month' },
yaxis: { title: 'Sales (in thousands)' }
}}>
</plotly-chart>
</div>
`,
};
export const CustomLayout: Story = {
args: {
theme: 'light',
},
render: (args) => html`
<div style="max-width: 800px; margin: 0 auto;">
<plotly-chart
theme=${args.theme}
.data=${[
{
x: [1, 2, 3, 4, 5],
y: [10, 11, 12, 13, 14],
type: 'scatter',
mode: 'lines',
name: 'Trend A',
line: { color: 'rgb(16, 185, 129)', width: 3 }
},
{
x: [1, 2, 3, 4, 5],
y: [8, 9, 10, 11, 12],
type: 'scatter',
mode: 'lines',
name: 'Trend B',
line: { color: 'rgb(239, 68, 68)', width: 3, dash: 'dash' }
}
]}
.layout=${{
title: {
text: 'Custom Styled Chart',
font: { size: 18 }
},
xaxis: {
title: 'Time Period',
gridcolor: 'rgba(255, 255, 255, 0.1)'
},
yaxis: {
title: 'Value',
gridcolor: 'rgba(255, 255, 255, 0.1)'
},
height: 500,
showlegend: true
}}>
</plotly-chart>
</div>
`,
};

View File

@@ -0,0 +1,205 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
import Plotly from 'plotly.js-dist-min';
export interface PlotlyData {
x?: any[];
y?: any[];
type?: any;
mode?: any;
name?: string;
marker?: any;
line?: any;
[key: string]: any;
}
export interface PlotlyLayout {
title?: any;
xaxis?: any;
yaxis?: any;
font?: any;
paper_bgcolor?: string;
plot_bgcolor?: string;
margin?: any;
showlegend?: boolean;
height?: number;
width?: number;
modebar?: any;
[key: string]: any;
}
@customElement('plotly-chart')
export class PlotlyChart extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
font-family: var(--vanna-font-family-default);
width: 100%;
height: 100%;
}
.plotly-div {
width: 100%;
min-height: 400px;
}
/* Plotly layering fix for Shadow DOM */
.plotly-div,
.plotly-div .js-plotly-plot,
.plotly-div .plot-container,
.plotly-div .svg-container {
position: relative;
width: 100%;
height: 100%;
}
.plotly-div svg.main-svg {
position: absolute;
top: 0;
left: 0;
}
.plotly-div .hoverlayer {
pointer-events: none;
}
.error-message {
padding: var(--vanna-space-4);
color: var(--vanna-accent-negative-default);
text-align: center;
font-style: italic;
}
.loading-message {
padding: var(--vanna-space-4);
color: var(--vanna-foreground-dimmer);
text-align: center;
font-style: italic;
}
`
];
@property({ type: Array }) data: PlotlyData[] = [];
@property({ type: Object }) layout: PlotlyLayout = {};
@property({ type: Object }) config = {};
@property({ type: Boolean }) loading = false;
@property() error = '';
@property() theme: 'light' | 'dark' = 'dark';
private plotlyDiv?: HTMLElement;
private resizeObserver?: ResizeObserver;
firstUpdated() {
this.plotlyDiv = this.shadowRoot?.querySelector('.plotly-div') as HTMLElement;
this._renderChart();
this._setupResizeObserver();
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver?.disconnect();
}
private _setupResizeObserver() {
if (!this.plotlyDiv) return;
this.resizeObserver = new ResizeObserver(() => {
if (this.plotlyDiv && this.data.length > 0) {
const width = this.plotlyDiv.offsetWidth;
Plotly.relayout(this.plotlyDiv, { width });
}
});
this.resizeObserver.observe(this.plotlyDiv);
}
updated(changedProperties: Map<string | number | symbol, unknown>) {
if (changedProperties.has('data') || changedProperties.has('layout') || changedProperties.has('theme')) {
this._renderChart();
}
}
private _getDefaultLayout(): PlotlyLayout {
const isDark = this.theme === 'dark';
// Start with layout from backend (which may include white background)
const mergedLayout = {
...this.layout,
// Only add font/modebar if not already set by backend
font: this.layout.font || {
family: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
color: isDark ? 'rgb(242, 244, 247)' : 'rgb(17, 24, 39)',
size: 12
},
modebar: this.layout.modebar || {
bgcolor: isDark ? 'rgba(21, 26, 38, 0.8)' : 'rgba(255, 255, 255, 0.8)',
color: isDark ? 'rgb(177, 186, 196)' : 'rgb(75, 85, 99)',
activecolor: isDark ? 'rgb(242, 244, 247)' : 'rgb(17, 24, 39)',
orientation: 'h'
},
// Set explicit dimensions for Shadow DOM compatibility
autosize: false,
width: this.layout.width || undefined,
height: this.layout.height || 400,
};
// If backend didn't set background colors, use transparent
if (!this.layout.paper_bgcolor) {
mergedLayout.paper_bgcolor = 'transparent';
}
if (!this.layout.plot_bgcolor) {
mergedLayout.plot_bgcolor = 'transparent';
}
return mergedLayout;
}
private _getDefaultConfig() {
return {
responsive: true,
displayModeBar: false,
...this.config
};
}
private async _renderChart() {
// Re-query plotlyDiv in case it wasn't available at firstUpdated
if (!this.plotlyDiv) {
this.plotlyDiv = this.shadowRoot?.querySelector('.plotly-div') as HTMLElement;
}
if (!this.plotlyDiv || this.loading || this.error || this.data.length === 0) {
return;
}
try {
const layout = this._getDefaultLayout();
const config = this._getDefaultConfig();
await Plotly.newPlot(this.plotlyDiv, this.data, layout, config);
} catch (err) {
this.error = err instanceof Error ? err.message : 'Failed to render chart';
}
}
render() {
return html`
${this.loading ? html`
<div class="loading-message">Loading chart...</div>
` : this.error ? html`
<div class="error-message">Error: ${this.error}</div>
` : html`
<div class="plotly-div"></div>
`}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'plotly-chart': PlotlyChart;
}
}

View File

@@ -0,0 +1,187 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './rich-card';
const meta: Meta = {
title: 'Rich Components/Rich Card',
component: 'rich-card',
parameters: {
layout: 'padded',
backgrounds: {
default: 'light',
values: [
{ name: 'dark', value: 'rgb(11, 15, 25)' },
{ name: 'light', value: '#f5f7fa' },
],
},
},
argTypes: {
title: { control: 'text' },
subtitle: { control: 'text' },
content: { control: 'text' },
icon: { control: 'text' },
status: {
control: 'select',
options: ['info', 'success', 'warning', 'error']
},
collapsible: { control: 'boolean' },
collapsed: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {
title: 'Sample Card',
subtitle: 'This is a subtitle',
content: 'This is the content of the card. It can contain any text or HTML.',
status: 'info',
collapsible: false,
collapsed: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-card
title=${args.title}
subtitle=${args.subtitle}
content=${args.content}
icon=${args.icon}
status=${args.status}
?collapsible=${args.collapsible}
?collapsed=${args.collapsed}>
</rich-card>
</div>
`,
};
export const WithIcon: Story = {
args: {
title: 'Card with Icon',
subtitle: 'Featuring an emoji icon',
content: 'This card demonstrates how icons work with the rich card component.',
icon: '🚀',
status: 'success',
collapsible: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-card
title=${args.title}
subtitle=${args.subtitle}
content=${args.content}
icon=${args.icon}
status=${args.status}
?collapsible=${args.collapsible}>
</rich-card>
</div>
`,
};
export const WithActions: Story = {
args: {
title: 'Interactive Card',
subtitle: 'With action buttons',
content: 'This card includes action buttons that can trigger events.',
status: 'info',
collapsible: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-card
title=${args.title}
subtitle=${args.subtitle}
content=${args.content}
status=${args.status}
?collapsible=${args.collapsible}
.actions=${[
{ label: 'Primary Action', action: 'primary', variant: 'primary' },
{ label: 'Secondary Action', action: 'secondary', variant: 'secondary' }
]}
@card-action=${(e: CustomEvent) => {
console.log('Card action:', e.detail.action);
alert(`Action triggered: ${e.detail.action}`);
}}>
</rich-card>
</div>
`,
};
export const Collapsible: Story = {
args: {
title: 'Collapsible Card',
subtitle: 'Click to expand/collapse',
content: 'This content can be hidden by clicking the toggle button in the header.',
status: 'warning',
collapsible: true,
collapsed: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-card
title=${args.title}
subtitle=${args.subtitle}
content=${args.content}
status=${args.status}
?collapsible=${args.collapsible}
?collapsed=${args.collapsed}>
</rich-card>
</div>
`,
};
export const StatusVariants: Story = {
render: () => html`
<div style="max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px;">
<rich-card
title="Info Status"
content="This card shows the info status variant."
status="info">
</rich-card>
<rich-card
title="Success Status"
content="This card shows the success status variant."
status="success">
</rich-card>
<rich-card
title="Warning Status"
content="This card shows the warning status variant."
status="warning">
</rich-card>
<rich-card
title="Error Status"
content="This card shows the error status variant."
status="error">
</rich-card>
</div>
`,
};
export const LightTheme: Story = {
args: {
title: 'Light Theme Card',
subtitle: 'Styled for light backgrounds',
content: 'This card is displayed with the light theme variant.',
icon: '☀️',
status: 'success',
},
parameters: {
backgrounds: { default: 'light' }
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-card
theme="light"
title=${args.title}
subtitle=${args.subtitle}
content=${args.content}
icon=${args.icon}
status=${args.status}>
</rich-card>
</div>
`,
};

View File

@@ -0,0 +1,309 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
export interface CardAction {
label: string;
action: string;
variant?: 'primary' | 'secondary';
}
@customElement('rich-card')
export class RichCard extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
margin-bottom: var(--vanna-space-4);
font-family: var(--vanna-font-family-default);
}
.card {
border: 1px solid var(--vanna-outline-default);
border-radius: var(--vanna-border-radius-lg);
background: var(--vanna-background-default);
box-shadow: var(--vanna-shadow-sm);
overflow: hidden;
transition: box-shadow var(--vanna-duration-200) ease;
}
.card:hover {
box-shadow: var(--vanna-shadow-md);
}
.card-header {
display: flex;
align-items: center;
padding: var(--vanna-space-4) var(--vanna-space-5);
background: var(--vanna-background-higher);
border-bottom: 1px solid var(--vanna-outline-default);
gap: var(--vanna-space-3);
}
.card-header.collapsible {
cursor: pointer;
}
.card-icon {
font-size: 1.25rem;
display: flex;
align-items: center;
}
.card-title-section {
flex: 1;
}
.card-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--vanna-foreground-default);
}
.card-subtitle {
margin: var(--vanna-space-1) 0 0 0;
font-size: 0.875rem;
color: var(--vanna-foreground-dimmer);
}
.card-status {
padding: var(--vanna-space-1) var(--vanna-space-2);
border-radius: var(--vanna-border-radius-md);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.card-status.status-success {
background: #d4edda;
color: #155724;
}
.card-status.status-warning {
background: #fff3cd;
color: #856404;
}
.card-status.status-error {
background: #f8d7da;
color: #721c24;
}
.card-status.status-info {
background: #d1ecf1;
color: #0c5460;
}
.card-toggle {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: var(--vanna-foreground-dimmer);
padding: var(--vanna-space-1);
border-radius: var(--vanna-border-radius-sm);
transition: background-color var(--vanna-duration-200) ease;
}
.card-toggle:hover {
background: var(--vanna-background-root);
}
.card-content {
padding: var(--vanna-space-4) var(--vanna-space-5);
line-height: 1.5;
color: var(--vanna-foreground-default);
transition: all var(--vanna-duration-200) ease;
overflow: hidden;
}
.card-content.collapsed {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
}
.card-content h1,
.card-content h2,
.card-content h3 {
margin: var(--vanna-space-2) 0;
font-weight: 600;
}
.card-content h1 {
font-size: 1.5rem;
}
.card-content h2 {
font-size: 1.25rem;
}
.card-content h3 {
font-size: 1.125rem;
}
.card-content p {
margin: var(--vanna-space-2) 0;
}
.card-content ul {
margin: var(--vanna-space-2) 0;
padding-left: var(--vanna-space-5);
}
.card-content li {
margin: var(--vanna-space-1) 0;
}
.card-content code {
background: var(--vanna-background-higher);
padding: var(--vanna-space-1) var(--vanna-space-2);
border-radius: var(--vanna-border-radius-sm);
font-family: monospace;
font-size: 0.875em;
}
.card-content strong {
font-weight: 600;
}
.card-actions {
padding: var(--vanna-space-3) var(--vanna-space-5);
background: var(--vanna-background-root);
border-top: 1px solid var(--vanna-outline-default);
display: flex;
gap: var(--vanna-space-2);
}
.card-action {
padding: var(--vanna-space-2) var(--vanna-space-4);
border-radius: var(--vanna-border-radius-md);
border: 1px solid var(--vanna-outline-default);
background: var(--vanna-background-default);
color: var(--vanna-foreground-default);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all var(--vanna-duration-200) ease;
}
.card-action:hover {
background: var(--vanna-background-higher);
}
.card-action.primary {
background: var(--vanna-accent-primary-default);
color: white;
border-color: var(--vanna-accent-primary-default);
}
.card-action.primary:hover {
background: var(--vanna-accent-primary-stronger);
}
`
];
@property() title = '';
@property() subtitle = '';
@property() content = '';
@property() icon = '';
@property() status: 'info' | 'success' | 'warning' | 'error' = 'info';
@property({ type: Array }) actions: CardAction[] = [];
@property({ type: Boolean }) collapsible = false;
@property({ type: Boolean }) collapsed = false;
@property({ type: Boolean }) markdown = false;
@property() theme: 'light' | 'dark' = 'dark';
private _toggleCollapsed() {
if (this.collapsible) {
this.collapsed = !this.collapsed;
}
}
private _renderMarkdown(text: string): string {
// Simple markdown rendering - basic formatting
return text
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/^- (.*$)/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/\n\n/g, '</p><p>')
.replace(/^(?!<[h|u|l|p])(.+)$/gm, '<p>$1</p>');
}
render() {
const contentHtml = this.markdown
? html`<div class="card-content ${this.collapsed ? 'collapsed' : ''}" .innerHTML=${this._renderMarkdown(this.content)}></div>`
: html`<div class="card-content ${this.collapsed ? 'collapsed' : ''}">${this.content}</div>`;
return html`
<div class="card">
<div class="card-header ${this.collapsible ? 'collapsible' : ''}"
@click=${this._toggleCollapsed}>
${this.icon ? html`<span class="card-icon">${this.icon}</span>` : ''}
<div class="card-title-section">
<h3 class="card-title">${this.title}</h3>
${this.subtitle ? html`<p class="card-subtitle">${this.subtitle}</p>` : ''}
</div>
${this.status ? html`<span class="card-status status-${this.status}">${this.status}</span>` : ''}
${this.collapsible ? html`
<button class="card-toggle">${this.collapsed ? '▶' : '▼'}</button>
` : ''}
</div>
${contentHtml}
${this.actions.length > 0 ? html`
<div class="card-actions">
${this.actions.map(action => html`
<button class="card-action ${action.variant || 'secondary'}"
@click=${() => this._handleAction(action.action)}>
${action.label}
</button>
`)}
</div>
` : ''}
</div>
`;
}
private async _handleAction(action: string) {
console.log('🔘 Card action button clicked (rich-card)');
console.log(' Action:', action);
// Dispatch event for any listeners
this.dispatchEvent(new CustomEvent('card-action', {
detail: { action },
bubbles: true,
composed: true
}));
// Also directly send to vanna-chat
const vannaChat = document.querySelector('vanna-chat') as any;
if (vannaChat && typeof vannaChat.sendMessage === 'function') {
console.log(' Found vanna-chat, sending message...');
try {
const success = await vannaChat.sendMessage(action);
if (success) {
console.log(' ✅ Action sent successfully');
} else {
console.error(' ❌ Failed to send action');
}
} catch (error) {
console.error(' ❌ Error sending action:', error);
}
} else {
console.warn(' ⚠️ vanna-chat component not found or sendMessage not available');
}
}
}
declare global {
interface HTMLElementTagNameMap {
'rich-card': RichCard;
}
}

View File

@@ -0,0 +1,353 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { ComponentManager, ComponentUpdate } from './rich-component-system';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
const meta: Meta = {
title: 'Rich Components/Component System',
parameters: {
layout: 'padded',
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#f5f7fa' },
{ name: 'dark', value: 'rgb(11, 15, 25)' },
],
},
},
};
export default meta;
type Story = StoryObj;
const ensureTokenStyles = () => {
if (document.getElementById('vanna-token-style')) {
return;
}
const style = document.createElement('style');
style.id = 'vanna-token-style';
style.textContent = vannaDesignTokens.cssText.replace(/:host/g, '.vanna-tokens');
document.head.appendChild(style);
};
const createContainer = () => {
ensureTokenStyles();
const container = document.createElement('div');
container.className = 'vanna-tokens';
container.style.cssText = `
padding: var(--vanna-space-5, 20px);
max-width: 800px;
margin: 0 auto;
background: var(--vanna-background-default);
border-radius: var(--vanna-border-radius-lg);
box-shadow: var(--vanna-shadow-md);
`;
return container;
};
const createManager = (container: HTMLElement) => new ComponentManager(container);
const renderComponent = (manager: ComponentManager, component: any) => {
const update: ComponentUpdate = {
operation: 'create',
target_id: component.id,
component,
timestamp: new Date().toISOString(),
} as ComponentUpdate;
manager.processUpdate(update);
};
const withDefaults = (component: any) => ({
layout: { position: 'append', size: {}, z_index: 0, classes: [] },
theme: {},
lifecycle: 'create',
...component,
});
export const NotificationComponents: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
const components = [
withDefaults({
id: 'info-notification',
type: 'notification',
data: {
message: 'This is an informational message',
title: 'Information',
level: 'info',
dismissible: true,
actions: [],
},
}),
withDefaults({
id: 'success-notification',
type: 'notification',
data: {
message: 'Operation completed successfully!',
title: 'Success',
level: 'success',
dismissible: true,
actions: [
{ label: 'View Details', action: 'view', variant: 'primary' },
{ label: 'Dismiss', action: 'dismiss', variant: 'secondary' },
],
},
}),
withDefaults({
id: 'warning-notification',
type: 'notification',
data: {
message: 'Please review the configuration before proceeding',
title: 'Warning',
level: 'warning',
dismissible: true,
actions: [],
},
}),
withDefaults({
id: 'error-notification',
type: 'notification',
data: {
message: 'Failed to connect to the database. Please check your connection.',
title: 'Connection Error',
level: 'error',
dismissible: true,
actions: [
{ label: 'Retry', action: 'retry', variant: 'primary' },
{ label: 'Cancel', action: 'cancel', variant: 'secondary' },
],
},
}),
];
components.forEach((component) => renderComponent(manager, component));
return container;
},
};
export const StatusIndicatorComponents: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
const statuses = [
{ status: 'loading', message: 'Processing your request...', pulse: true },
{ status: 'success', message: 'Request completed successfully', pulse: false },
{ status: 'warning', message: 'Operation completed with warnings', pulse: false },
{ status: 'error', message: 'Request failed - please try again', pulse: false },
];
statuses.forEach((statusData, index) => {
const component = withDefaults({
id: `status-${index}`,
type: 'status_indicator',
data: statusData,
});
renderComponent(manager, component);
});
return container;
},
};
export const TextComponents: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
const plainText = withDefaults({
id: 'plain-text',
type: 'text',
data: {
content: 'This is a plain text component with some sample content to demonstrate text rendering.',
markdown: false,
},
});
const markdownText = withDefaults({
id: 'markdown-text',
type: 'text',
data: {
content: `# Rich Components Demo\n\nThis is a **markdown** text component with various formatting:\n\n- **Bold text** for emphasis\n- *Italic text* for style\n- Lists for organization\n\n## Features\n\nThe text component supports:\n- Plain text rendering\n- Basic markdown formatting\n- Code syntax highlighting`,
markdown: true,
},
});
[plainText, markdownText].forEach((component) => renderComponent(manager, component));
return container;
},
};
export const DataFrameComponents: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
// Sample data for different scenarios
const sampleData = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 28, city: 'New York', salary: 75000, active: true },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com', age: 34, city: 'San Francisco', salary: 85000, active: true },
{ id: 3, name: 'Carol Davis', email: 'carol@example.com', age: 29, city: 'Chicago', salary: 70000, active: false },
{ id: 4, name: 'David Wilson', email: 'david@example.com', age: 42, city: 'Austin', salary: 90000, active: true },
{ id: 5, name: 'Eve Brown', email: 'eve@example.com', age: 31, city: 'Seattle', salary: 80000, active: true },
{ id: 6, name: 'Frank Miller', email: 'frank@example.com', age: 38, city: 'Boston', salary: 95000, active: false },
{ id: 7, name: 'Grace Lee', email: 'grace@example.com', age: 26, city: 'Denver', salary: 65000, active: true },
{ id: 8, name: 'Henry Taylor', email: 'henry@example.com', age: 33, city: 'Portland', salary: 72000, active: true },
];
const columns = ['id', 'name', 'email', 'age', 'city', 'salary', 'active'];
// Basic DataFrame
const basicDataFrame = withDefaults({
id: 'basic-dataframe',
type: 'dataframe',
data: {
data: sampleData.slice(0, 5),
columns: columns,
title: 'Employee Records',
description: 'Sample employee data with various fields',
row_count: 5,
column_count: columns.length,
column_types: {
id: 'number',
name: 'string',
email: 'string',
age: 'number',
city: 'string',
salary: 'number',
active: 'boolean'
}
},
});
// Large DataFrame with all features
const fullDataFrame = withDefaults({
id: 'full-dataframe',
type: 'dataframe',
data: {
data: sampleData,
columns: columns,
title: 'Complete Employee Database',
description: 'Full dataset with search, sort, and export functionality',
row_count: sampleData.length,
column_count: columns.length,
searchable: true,
sortable: true,
filterable: true,
exportable: true,
striped: true,
bordered: true,
max_rows_displayed: 6,
column_types: {
id: 'number',
name: 'string',
email: 'string',
age: 'number',
city: 'string',
salary: 'number',
active: 'boolean'
}
},
});
// Empty DataFrame
const emptyDataFrame = withDefaults({
id: 'empty-dataframe',
type: 'dataframe',
data: {
data: [],
columns: [],
title: 'Empty Dataset',
description: 'No data available to display',
row_count: 0,
column_count: 0,
},
});
// Compact DataFrame
const compactDataFrame = withDefaults({
id: 'compact-dataframe',
type: 'dataframe',
data: {
data: sampleData.slice(0, 4),
columns: ['id', 'name', 'city', 'active'],
title: 'Compact View',
description: 'Space-efficient display with essential columns only',
row_count: 4,
column_count: 4,
compact: true,
searchable: false,
exportable: false,
column_types: {
id: 'number',
name: 'string',
city: 'string',
active: 'boolean'
}
},
});
[basicDataFrame, fullDataFrame, emptyDataFrame, compactDataFrame].forEach((component) => {
renderComponent(manager, component);
});
return container;
},
};
export const SQLQueryDataFrame: Story = {
render: () => {
const container = createContainer();
const manager = createManager(container);
// SQL query result simulation
const sqlResultData = [
{ TrackId: 1, Name: 'For Those About To Rock (We Salute You)', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 343719, Bytes: 11170334, UnitPrice: 0.99 },
{ TrackId: 2, Name: 'Balls to the Wall', AlbumId: 2, MediaTypeId: 2, GenreId: 1, Composer: null, Milliseconds: 342562, Bytes: 5510424, UnitPrice: 0.99 },
{ TrackId: 3, Name: 'Fast As a Shark', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 230619, Bytes: 3990994, UnitPrice: 0.99 },
{ TrackId: 4, Name: 'Restless and Wild', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 252051, Bytes: 4331779, UnitPrice: 0.99 },
{ TrackId: 5, Name: 'Princess of the Dawn', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'Deaffy & R.A. Smith-Diesel', Milliseconds: 375418, Bytes: 6290521, UnitPrice: 0.99 },
];
const sqlColumns = ['TrackId', 'Name', 'AlbumId', 'MediaTypeId', 'GenreId', 'Composer', 'Milliseconds', 'Bytes', 'UnitPrice'];
const sqlDataFrame = withDefaults({
id: 'sql-dataframe',
type: 'dataframe',
data: {
data: sqlResultData,
columns: sqlColumns,
title: 'Query Results',
description: 'SELECT * FROM Track LIMIT 5',
row_count: sqlResultData.length,
column_count: sqlColumns.length,
searchable: true,
sortable: true,
exportable: true,
column_types: {
TrackId: 'number',
Name: 'string',
AlbumId: 'number',
MediaTypeId: 'number',
GenreId: 'number',
Composer: 'string',
Milliseconds: 'number',
Bytes: 'number',
UnitPrice: 'number'
}
},
});
renderComponent(manager, sqlDataFrame);
return container;
},
};

View File

@@ -0,0 +1,252 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './rich-progress-bar';
const meta: Meta = {
title: 'Rich Components/Rich Progress Bar',
component: 'rich-progress-bar',
parameters: {
layout: 'padded',
backgrounds: {
default: 'light',
values: [
{ name: 'dark', value: 'rgb(11, 15, 25)' },
{ name: 'light', value: '#f5f7fa' },
],
},
},
argTypes: {
value: { control: { type: 'range', min: 0, max: 1, step: 0.01 } },
label: { control: 'text' },
description: { control: 'text' },
showPercentage: { control: 'boolean' },
status: {
control: 'select',
options: ['info', 'success', 'warning', 'error']
},
animated: { control: 'boolean' },
indeterminate: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {
value: 0.65,
label: 'Processing',
showPercentage: true,
status: 'info',
animated: false,
indeterminate: false,
},
render: (args) => html`
<div style="max-width: 500px; margin: 0 auto;">
<rich-progress-bar
.value=${args.value}
label=${args.label}
description=${args.description}
?showPercentage=${args.showPercentage}
status=${args.status}
?animated=${args.animated}
?indeterminate=${args.indeterminate}>
</rich-progress-bar>
</div>
`,
};
export const WithDescription: Story = {
args: {
value: 0.4,
label: 'Installing dependencies',
description: 'Downloading and installing npm packages for the project. This may take a few minutes.',
showPercentage: true,
status: 'info',
animated: true,
indeterminate: false,
},
render: (args) => html`
<div style="max-width: 500px; margin: 0 auto;">
<rich-progress-bar
.value=${args.value}
label=${args.label}
description=${args.description}
?showPercentage=${args.showPercentage}
status=${args.status}
?animated=${args.animated}
?indeterminate=${args.indeterminate}>
</rich-progress-bar>
</div>
`,
};
export const Animated: Story = {
args: {
value: 0.75,
label: 'Uploading files',
showPercentage: true,
status: 'info',
animated: true,
indeterminate: false,
},
render: (args) => html`
<div style="max-width: 500px; margin: 0 auto;">
<rich-progress-bar
.value=${args.value}
label=${args.label}
description=${args.description}
?showPercentage=${args.showPercentage}
status=${args.status}
?animated=${args.animated}
?indeterminate=${args.indeterminate}>
</rich-progress-bar>
</div>
`,
};
export const Indeterminate: Story = {
args: {
value: 0,
label: 'Loading...',
description: 'Please wait while we process your request',
showPercentage: false,
status: 'info',
animated: false,
indeterminate: true,
},
render: (args) => html`
<div style="max-width: 500px; margin: 0 auto;">
<rich-progress-bar
.value=${args.value}
label=${args.label}
description=${args.description}
?showPercentage=${args.showPercentage}
status=${args.status}
?animated=${args.animated}
?indeterminate=${args.indeterminate}>
</rich-progress-bar>
</div>
`,
};
export const StatusVariants: Story = {
render: () => html`
<div style="max-width: 500px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px;">
<rich-progress-bar
.value=${0.8}
label="Info Status"
status="info"
showPercentage>
</rich-progress-bar>
<rich-progress-bar
.value=${1.0}
label="Success Status"
status="success"
showPercentage>
</rich-progress-bar>
<rich-progress-bar
.value=${0.6}
label="Warning Status"
status="warning"
showPercentage>
</rich-progress-bar>
<rich-progress-bar
.value=${0.3}
label="Error Status"
status="error"
showPercentage>
</rich-progress-bar>
</div>
`,
};
export const Minimal: Story = {
args: {
value: 0.45,
showPercentage: false,
status: 'info',
animated: false,
indeterminate: false,
},
render: (args) => html`
<div style="max-width: 500px; margin: 0 auto;">
<rich-progress-bar
.value=${args.value}
?showPercentage=${args.showPercentage}
status=${args.status}
?animated=${args.animated}
?indeterminate=${args.indeterminate}>
</rich-progress-bar>
</div>
`,
};
export const MultipleSteps: Story = {
render: () => html`
<div style="max-width: 500px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px;">
<rich-progress-bar
.value=${1.0}
label="Step 1: Initialize"
description="Project initialization completed"
status="success"
showPercentage>
</rich-progress-bar>
<rich-progress-bar
.value=${1.0}
label="Step 2: Download dependencies"
description="All packages downloaded successfully"
status="success"
showPercentage>
</rich-progress-bar>
<rich-progress-bar
.value=${0.7}
label="Step 3: Build project"
description="Compiling TypeScript and bundling assets"
status="info"
animated
showPercentage>
</rich-progress-bar>
<rich-progress-bar
.value=${0}
label="Step 4: Deploy"
description="Waiting for build to complete"
status="info"
showPercentage>
</rich-progress-bar>
</div>
`,
};
export const LightTheme: Story = {
args: {
value: 0.55,
label: 'Light Theme Progress',
description: 'Progress bar styled for light backgrounds',
showPercentage: true,
status: 'success',
animated: true,
},
parameters: {
backgrounds: { default: 'light' }
},
render: (args) => html`
<div style="max-width: 500px; margin: 0 auto;">
<rich-progress-bar
theme="light"
.value=${args.value}
label=${args.label}
description=${args.description}
?showPercentage=${args.showPercentage}
status=${args.status}
?animated=${args.animated}>
</rich-progress-bar>
</div>
`,
};

View File

@@ -0,0 +1,202 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
@customElement('rich-progress-bar')
export class RichProgressBar extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
margin-bottom: var(--vanna-space-4);
font-family: var(--vanna-font-family-default);
}
.progress-container {
padding: var(--vanna-space-4);
border: 1px solid var(--vanna-outline-default);
border-radius: var(--vanna-border-radius-lg);
background: var(--vanna-background-default);
box-shadow: var(--vanna-shadow-sm);
transition: box-shadow var(--vanna-duration-200) ease;
}
.progress-container:hover {
box-shadow: var(--vanna-shadow-md);
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--vanna-space-3);
}
.progress-label {
font-weight: 500;
color: var(--vanna-foreground-default);
}
.progress-percentage {
font-size: 0.875rem;
color: var(--vanna-foreground-dimmer);
font-weight: 600;
}
.progress-track {
height: 12px;
background: var(--vanna-background-root);
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--vanna-outline-default);
position: relative;
}
.progress-fill {
height: 100%;
background: var(--vanna-accent-primary-default);
border-radius: 6px;
transition: width var(--vanna-duration-300) ease;
position: relative;
overflow: hidden;
}
.progress-fill.animated {
animation: progressPulse 2s ease-in-out infinite;
}
.progress-fill.animated::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
animation: progressShimmer 1.5s infinite;
}
@keyframes progressPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
@keyframes progressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.progress-fill.status-success {
background: var(--vanna-accent-positive-default);
}
.progress-fill.status-warning {
background: var(--vanna-accent-warning-default);
}
.progress-fill.status-error {
background: var(--vanna-accent-negative-default);
}
.progress-fill.status-info {
background: var(--vanna-accent-primary-default);
}
/* Indeterminate progress animation */
.progress-fill.indeterminate {
background: linear-gradient(
90deg,
transparent 0%,
var(--vanna-accent-primary-default) 50%,
transparent 100%
);
background-size: 200% 100%;
animation: indeterminateProgress 2s linear infinite;
width: 100% !important;
}
@keyframes indeterminateProgress {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Text content for description */
.progress-description {
margin-top: var(--vanna-space-2);
font-size: 0.875rem;
color: var(--vanna-foreground-dimmer);
line-height: 1.4;
}
`
];
@property({ type: Number }) value = 0;
@property() label = '';
@property() description = '';
@property({ type: Boolean }) showPercentage = true;
@property() status: 'info' | 'success' | 'warning' | 'error' = 'info';
@property({ type: Boolean }) animated = false;
@property({ type: Boolean }) indeterminate = false;
@property() theme: 'light' | 'dark' = 'dark';
private get percentage(): number {
if (this.indeterminate) return 100;
return Math.round(Math.max(0, Math.min(1, this.value)) * 100);
}
private get progressClasses(): string {
const classes = ['progress-fill'];
if (this.animated) {
classes.push('animated');
}
if (this.indeterminate) {
classes.push('indeterminate');
}
if (this.status) {
classes.push(`status-${this.status}`);
}
return classes.join(' ');
}
render() {
return html`
<div class="progress-container">
${this.label || this.showPercentage ? html`
<div class="progress-header">
${this.label ? html`<span class="progress-label">${this.label}</span>` : ''}
${this.showPercentage && !this.indeterminate ? html`
<span class="progress-percentage">${this.percentage}%</span>
` : ''}
</div>
` : ''}
<div class="progress-track">
<div
class="${this.progressClasses}"
style="width: ${this.indeterminate ? '100' : this.percentage}%">
</div>
</div>
${this.description ? html`
<div class="progress-description">${this.description}</div>
` : ''}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'rich-progress-bar': RichProgressBar;
}
}

View File

@@ -0,0 +1,270 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './rich-task-list';
const meta: Meta = {
title: 'Rich Components/Rich Task List',
component: 'rich-task-list',
parameters: {
layout: 'padded',
backgrounds: {
default: 'light',
values: [
{ name: 'dark', value: 'rgb(11, 15, 25)' },
{ name: 'light', value: '#f5f7fa' },
],
},
},
argTypes: {
title: { control: 'text' },
tasks: { control: 'object' },
showProgress: { control: 'boolean' },
showTimestamps: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj;
const sampleTasks = [
{
id: '1',
title: 'Initialize project setup',
description: 'Setting up the basic project structure and dependencies',
status: 'completed',
progress: 1.0,
timestamp: '2024-01-15 10:30:00'
},
{
id: '2',
title: 'Configure database connection',
description: 'Establishing secure connection to PostgreSQL database',
status: 'completed',
progress: 1.0,
timestamp: '2024-01-15 10:45:00'
},
{
id: '3',
title: 'Implement user authentication',
description: 'Building JWT-based authentication system',
status: 'running',
progress: 0.7,
timestamp: '2024-01-15 11:00:00'
},
{
id: '4',
title: 'Create API endpoints',
description: 'Developing RESTful API for user management',
status: 'pending',
timestamp: '2024-01-15 11:30:00'
},
{
id: '5',
title: 'Write unit tests',
description: 'Comprehensive test coverage for all modules',
status: 'pending',
}
];
export const Default: Story = {
args: {
title: 'Development Tasks',
tasks: sampleTasks,
showProgress: true,
showTimestamps: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};
export const WithTimestamps: Story = {
args: {
title: 'Build Pipeline',
tasks: sampleTasks,
showProgress: true,
showTimestamps: true,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};
export const WithoutProgress: Story = {
args: {
title: 'Simple Task List',
tasks: [
{ id: '1', title: 'Review code changes', status: 'completed' },
{ id: '2', title: 'Update documentation', status: 'running' },
{ id: '3', title: 'Deploy to staging', status: 'pending' },
{ id: '4', title: 'Run integration tests', status: 'failed' },
],
showProgress: false,
showTimestamps: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};
export const AllStatuses: Story = {
args: {
title: 'Task Status Examples',
tasks: [
{
id: '1',
title: 'Completed Task',
description: 'This task has been successfully completed',
status: 'completed',
progress: 1.0,
},
{
id: '2',
title: 'Running Task',
description: 'This task is currently in progress',
status: 'running',
progress: 0.6,
},
{
id: '3',
title: 'Pending Task',
description: 'This task is waiting to be started',
status: 'pending',
},
{
id: '4',
title: 'Failed Task',
description: 'This task encountered an error',
status: 'failed',
progress: 0.3,
},
],
showProgress: true,
showTimestamps: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};
export const EmptyList: Story = {
args: {
title: 'No Tasks',
tasks: [],
showProgress: true,
showTimestamps: false,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};
export const ErrorStates: Story = {
args: {
title: 'Error Handling Examples',
tasks: [
{
id: '1',
title: 'Database Connection Failed',
description: 'Could not establish connection to the database server. Check network connectivity and credentials.',
status: 'failed',
progress: 0.1,
timestamp: '2024-01-15 10:15:00'
},
{
id: '2',
title: 'API Authentication Error',
description: 'Invalid API key or expired token. Please refresh your credentials.',
status: 'failed',
progress: 0.0,
timestamp: '2024-01-15 10:20:00'
},
{
id: '3',
title: 'File Processing Error',
description: 'Unable to process uploaded file. File may be corrupted or in an unsupported format.',
status: 'failed',
progress: 0.45,
timestamp: '2024-01-15 10:25:00'
},
{
id: '4',
title: 'Network Timeout',
description: 'Request timed out after 30 seconds. This may be due to high server load.',
status: 'failed',
progress: 0.8,
timestamp: '2024-01-15 10:30:00'
},
],
showProgress: true,
showTimestamps: true,
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};
export const LightTheme: Story = {
args: {
title: 'Light Theme Task List',
tasks: sampleTasks.slice(0, 3),
showProgress: true,
showTimestamps: true,
},
parameters: {
backgrounds: { default: 'light' }
},
render: (args) => html`
<div style="max-width: 600px; margin: 0 auto;">
<rich-task-list
theme="light"
title=${args.title}
.tasks=${args.tasks}
?showProgress=${args.showProgress}
?showTimestamps=${args.showTimestamps}>
</rich-task-list>
</div>
`,
};

View File

@@ -0,0 +1,272 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
export interface TaskItem {
id: string;
title: string;
description?: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress?: number;
timestamp?: string;
}
@customElement('rich-task-list')
export class RichTaskList extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
margin-bottom: var(--vanna-space-4);
font-family: var(--vanna-font-family-default);
}
.task-list {
border: 1px solid var(--vanna-outline-default);
border-radius: var(--vanna-border-radius-lg);
background: var(--vanna-background-default);
box-shadow: var(--vanna-shadow-sm);
overflow: hidden;
transition: box-shadow var(--vanna-duration-200) ease;
}
.task-list:hover {
box-shadow: var(--vanna-shadow-md);
}
.task-list-header {
padding: var(--vanna-space-4) var(--vanna-space-5);
background: var(--vanna-background-higher);
border-bottom: 1px solid var(--vanna-outline-default);
}
.task-list-title {
margin: 0 0 var(--vanna-space-3) 0;
font-size: 1rem;
font-weight: 600;
color: var(--vanna-foreground-default);
}
.task-list-progress {
display: flex;
align-items: center;
gap: var(--vanna-space-3);
}
.progress-text {
font-size: 0.875rem;
color: var(--vanna-foreground-dimmer);
min-width: fit-content;
}
.progress-bar {
flex: 1;
height: 6px;
background: var(--vanna-background-root);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--vanna-accent-primary-default);
border-radius: 3px;
transition: width var(--vanna-duration-300) ease;
}
.progress-fill.animated {
animation: progressPulse 2s ease-in-out infinite;
}
@keyframes progressPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.progress-fill.status-success {
background: var(--vanna-accent-positive-default);
}
.progress-fill.status-warning {
background: var(--vanna-accent-warning-default);
}
.progress-fill.status-error {
background: var(--vanna-accent-negative-default);
}
.task-list-items {
padding: var(--vanna-space-2);
}
.task-item {
display: flex;
align-items: flex-start;
gap: var(--vanna-space-3);
padding: var(--vanna-space-3);
border-radius: var(--vanna-border-radius-md);
transition: background-color var(--vanna-duration-200) ease;
}
.task-item:hover {
background: var(--vanna-background-root);
}
.task-item.status-completed {
opacity: 0.7;
}
.task-item.status-failed {
background: rgba(239, 68, 68, 0.1);
}
.task-icon {
font-size: 1rem;
margin-top: 0.125rem;
}
.task-content {
flex: 1;
min-width: 0;
}
.task-title {
font-weight: 500;
color: var(--vanna-foreground-default);
margin-bottom: var(--vanna-space-1);
}
.task-description {
font-size: 0.875rem;
color: var(--vanna-foreground-dimmer);
margin-bottom: var(--vanna-space-2);
}
.task-progress {
display: flex;
align-items: center;
gap: var(--vanna-space-2);
margin-bottom: var(--vanna-space-2);
}
.task-progress-bar {
flex: 1;
height: 4px;
background: var(--vanna-background-root);
border-radius: 2px;
overflow: hidden;
}
.task-progress-fill {
height: 100%;
background: var(--vanna-accent-primary-default);
border-radius: 2px;
transition: width var(--vanna-duration-300) ease;
}
.task-progress-text {
font-size: 0.75rem;
color: var(--vanna-foreground-dimmer);
min-width: fit-content;
}
.task-timestamp {
font-size: 0.75rem;
color: var(--vanna-foreground-dimmest);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.task-list-header {
padding-left: var(--vanna-space-4);
padding-right: var(--vanna-space-4);
}
.task-list-progress {
flex-direction: column;
align-items: stretch;
gap: var(--vanna-space-2);
}
}
`
];
@property() title = '';
@property({ type: Array }) tasks: TaskItem[] = [];
@property({ type: Boolean }) showProgress = true;
@property({ type: Boolean }) showTimestamps = false;
@property() theme: 'light' | 'dark' = 'dark';
private get completedTasks(): number {
return this.tasks.filter(task => task.status === 'completed').length;
}
private get progressPercentage(): number {
return this.tasks.length > 0 ? (this.completedTasks / this.tasks.length) * 100 : 0;
}
private getStatusIcon(status: string): string {
const icons = {
'pending': '⏳',
'running': '🔄',
'completed': '✅',
'failed': '❌'
};
return icons[status as keyof typeof icons] || '⏳';
}
private renderTask(task: TaskItem) {
const statusIcon = this.getStatusIcon(task.status);
return html`
<div class="task-item status-${task.status}" data-task-id="${task.id}">
<div class="task-icon">${statusIcon}</div>
<div class="task-content">
<div class="task-title">${task.title}</div>
${task.description ? html`
<div class="task-description">${task.description}</div>
` : ''}
${task.progress !== null && task.progress !== undefined ? html`
<div class="task-progress">
<div class="task-progress-bar">
<div class="task-progress-fill" style="width: ${task.progress * 100}%"></div>
</div>
<span class="task-progress-text">${Math.round(task.progress * 100)}%</span>
</div>
` : ''}
${this.showTimestamps && task.timestamp ? html`
<div class="task-timestamp">${task.timestamp}</div>
` : ''}
</div>
</div>
`;
}
render() {
return html`
<div class="task-list">
<div class="task-list-header">
<h3 class="task-list-title">${this.title}</h3>
${this.showProgress ? html`
<div class="task-list-progress">
<span class="progress-text">${this.completedTasks}/${this.tasks.length} completed</span>
<div class="progress-bar">
<div class="progress-fill" style="width: ${this.progressPercentage}%"></div>
</div>
</div>
` : ''}
</div>
<div class="task-list-items">
${this.tasks.map(task => this.renderTask(task))}
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'rich-task-list': RichTaskList;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './vanna-message';
const meta: Meta = {
title: 'Components/VannaMessage',
component: 'vanna-message',
parameters: {
layout: 'centered',
},
argTypes: {
content: { control: 'text' },
type: {
control: 'select',
options: ['user', 'assistant'],
},
timestamp: { control: 'number' },
},
};
export default meta;
type Story = StoryObj;
export const UserMessage: Story = {
args: {
content: 'Hello! Can you help me analyze my data?',
type: 'user',
timestamp: Date.now(),
},
render: (args) => html`
<div style="width: 400px;">
<vanna-message
.content=${args.content}
.type=${args.type}
.timestamp=${args.timestamp}>
</vanna-message>
</div>
`,
};
export const AssistantMessage: Story = {
args: {
content: 'Of course! I\'d be happy to help you analyze your data. Could you please tell me more about the type of data you have and what insights you\'re looking for?',
type: 'assistant',
timestamp: Date.now(),
},
render: (args) => html`
<div style="width: 400px;">
<vanna-message
.content=${args.content}
.type=${args.type}
.timestamp=${args.timestamp}>
</vanna-message>
</div>
`,
};
export const LongMessage: Story = {
args: {
content: 'This is a very long message that demonstrates how the component handles longer text content. It should wrap properly and maintain good readability while staying within the maximum width constraints. The message can contain multiple sentences and paragraphs of information that the AI assistant might provide in response to complex queries.',
type: 'assistant',
timestamp: Date.now(),
},
render: (args) => html`
<div style="width: 400px;">
<vanna-message
.content=${args.content}
.type=${args.type}
.timestamp=${args.timestamp}>
</vanna-message>
</div>
`,
};
export const Conversation: Story = {
render: () => html`
<div style="width: 400px;">
<vanna-message
content="What's the total revenue for Q4?"
type="user"
.timestamp=${Date.now() - 120000}>
</vanna-message>
<vanna-message
content="I'll help you calculate the total revenue for Q4. Let me query your database for this information."
type="assistant"
.timestamp=${Date.now() - 60000}>
</vanna-message>
<vanna-message
content="The total revenue for Q4 is $2,450,000. This represents a 15% increase compared to Q3."
type="assistant"
.timestamp=${Date.now()}>
</vanna-message>
</div>
`,
};

View File

@@ -0,0 +1,221 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
@customElement('vanna-message')
export class VannaMessage extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
padding: 0 var(--vanna-space-2);
margin-bottom: var(--vanna-space-4);
font-family: var(--vanna-font-family-default);
animation: fade-in-up 0.25s ease-out;
}
:host(:last-of-type) {
margin-bottom: 0;
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message {
position: relative;
padding: var(--vanna-space-4) var(--vanna-space-5);
border-radius: var(--vanna-chat-bubble-radius);
word-wrap: break-word;
line-height: 1.6;
display: flex;
flex-direction: column;
gap: var(--vanna-space-2);
max-width: min(85%, 580px);
transition: transform var(--vanna-duration-200) ease, box-shadow var(--vanna-duration-200) ease;
backdrop-filter: blur(8px);
}
.message.assistant {
background: var(--vanna-background-root);
border: 1px solid var(--vanna-outline-dimmer);
color: var(--vanna-foreground-default);
box-shadow: var(--vanna-shadow-sm);
border-radius: var(--vanna-chat-bubble-radius) var(--vanna-chat-bubble-radius) var(--vanna-chat-bubble-radius) var(--vanna-space-2);
}
.message.user {
margin-left: auto;
max-width: min(80%, 500px);
background: linear-gradient(135deg, var(--vanna-accent-primary-stronger) 0%, var(--vanna-accent-primary-default) 100%);
color: white;
box-shadow: var(--vanna-shadow-md);
border-radius: var(--vanna-chat-bubble-radius) var(--vanna-chat-bubble-radius) var(--vanna-space-2) var(--vanna-chat-bubble-radius);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.message:hover {
transform: translateY(-1px);
}
.message.assistant:hover {
box-shadow: var(--vanna-shadow-md);
border-color: var(--vanna-outline-hover);
}
.message.user:hover {
box-shadow: var(--vanna-shadow-lg);
}
.message-content {
margin: 0;
font-size: 15px;
letter-spacing: 0.01em;
white-space: pre-wrap;
font-weight: 400;
}
.message-content a {
color: inherit;
font-weight: 500;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
opacity: 0.9;
}
.message-content code {
font-family: var(--vanna-font-family-mono);
background: var(--vanna-background-higher);
padding: 2px 6px;
border-radius: var(--vanna-border-radius-sm);
font-size: 13px;
border: 1px solid var(--vanna-outline-dimmer);
}
.message.user .message-content code {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
}
.message-timestamp {
display: inline-flex;
align-items: center;
gap: var(--vanna-space-1);
font-size: 11px;
letter-spacing: 0.05em;
margin-top: var(--vanna-space-2);
font-family: var(--vanna-font-family-default);
opacity: 0.7;
font-weight: 500;
}
.message-timestamp::before {
content: '';
width: 3px;
height: 3px;
border-radius: var(--vanna-border-radius-full);
background: currentColor;
opacity: 0.8;
}
.message.assistant .message-timestamp {
align-self: flex-start;
color: var(--vanna-foreground-dimmest);
}
.message.assistant .message-timestamp::before {
background: var(--vanna-accent-primary-default);
}
.message.user .message-timestamp {
align-self: flex-end;
color: rgba(255, 255, 255, 0.8);
}
.message.user .message-timestamp::before {
background: rgba(255, 255, 255, 0.8);
}
:host([theme="dark"]) .message.assistant {
background: var(--vanna-background-higher);
border: 1px solid var(--vanna-outline-default);
color: var(--vanna-foreground-default);
box-shadow: var(--vanna-shadow-md);
}
:host([theme="dark"]) .message.assistant .message-content code {
background: var(--vanna-background-highest);
border-color: var(--vanna-outline-default);
}
:host([theme="dark"]) .message.assistant .message-timestamp {
color: var(--vanna-foreground-dimmest);
}
:host([theme="dark"]) .message.assistant .message-timestamp::before {
background: var(--vanna-accent-primary-default);
}
:host([theme="dark"]) .message.user {
background: linear-gradient(135deg, var(--vanna-accent-primary-stronger) 0%, var(--vanna-accent-primary-default) 100%);
color: white;
box-shadow: var(--vanna-shadow-lg);
}
:host([theme="dark"]) .message.user .message-content code {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.25);
}
:host([theme="dark"]) .message.user .message-timestamp {
color: rgba(255, 255, 255, 0.8);
}
:host([theme="dark"]) .message.user .message-timestamp::before {
background: rgba(255, 255, 255, 0.8);
}
@media (max-width: 600px) {
.message {
max-width: 100%;
}
.message.user {
max-width: 100%;
}
}
`
];
@property() content = '';
@property() type: 'user' | 'assistant' = 'user';
@property({ type: Number }) timestamp = Date.now();
@property({ reflect: true }) theme = 'light';
private formatTimestamp(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
render() {
return html`
<div class="message ${this.type}">
<div class="message-content">${this.content}</div>
<div class="message-timestamp">
${this.formatTimestamp(this.timestamp)}
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,268 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './vanna-progress-tracker';
const meta: Meta = {
title: 'Components/VannaProgressTracker',
component: 'vanna-progress-tracker',
parameters: {
layout: 'centered',
backgrounds: {
default: 'light',
values: [
{ name: 'dark', value: 'rgb(11, 15, 25)' },
{ name: 'light', value: '#ffffff' },
],
},
},
argTypes: {
title: { control: 'text' },
theme: {
control: 'select',
options: ['dark', 'light'],
description: 'Theme variant'
},
},
};
export default meta;
type Story = StoryObj;
export const Empty: Story = {
args: {
title: 'Agent Progress',
},
render: (args) => html`
<div style="width: 350px; height: 400px;">
<vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>
</div>
`,
};
export const WithTasks: Story = {
args: {
title: 'Agent Progress',
theme: 'light',
},
render: (args) => {
setTimeout(() => {
const tracker = document.querySelector('vanna-progress-tracker') as any;
if (tracker) {
tracker.addItem('Analyze database schema', 'Examining table structure');
tracker.addItem('Generate SQL query', 'Based on user request');
tracker.addItem('Execute query', 'Running against production DB');
tracker.addItem('Format results', 'Creating visualization');
// Update first item to in_progress
const items = tracker.shadowRoot?.querySelectorAll('.progress-item');
if (items?.[0]) {
tracker.updateItem(tracker.items[0].id, 'in_progress', 'Scanning tables...');
}
}
}, 100);
return html`
<div style="width: 350px; height: 400px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'}; padding: 20px;">
<vanna-progress-tracker .title=${args.title} theme=${args.theme}></vanna-progress-tracker>
</div>
`;
},
};
export const WithTasksLight: Story = {
args: {
title: 'Agent Progress',
theme: 'light',
},
render: (args) => {
setTimeout(() => {
const tracker = document.querySelector('vanna-progress-tracker') as any;
if (tracker) {
tracker.addItem('Analyze database schema', 'Examining table structure');
tracker.addItem('Generate SQL query', 'Based on user request');
tracker.addItem('Execute query', 'Running against production DB');
tracker.addItem('Format results', 'Creating visualization');
// Update first item to in_progress
const items = tracker.shadowRoot?.querySelectorAll('.progress-item');
if (items?.[0]) {
tracker.updateItem(tracker.items[0].id, 'in_progress', 'Scanning tables...');
}
}
}, 100);
return html`
<div style="width: 350px; height: 400px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'}; padding: 20px;">
<vanna-progress-tracker .title=${args.title} theme=${args.theme}></vanna-progress-tracker>
</div>
`;
},
};
export const MixedStatuses: Story = {
args: {
title: 'Data Analysis Pipeline',
},
render: (args) => {
setTimeout(() => {
const tracker = document.querySelector('vanna-progress-tracker') as any;
if (tracker) {
const id1 = tracker.addItem('Connect to database', 'Establishing connection');
const id2 = tracker.addItem('Validate credentials', 'Checking access permissions');
const id3 = tracker.addItem('Load data schema', 'Reading table definitions');
const id4 = tracker.addItem('Parse user query', 'Understanding natural language');
const id5 = tracker.addItem('Generate SQL', 'Converting to database query');
const id6 = tracker.addItem('Execute query', 'Running against database');
const id7 = tracker.addItem('Process results', 'Formatting output');
// Simulate different states
tracker.updateItem(id1, 'completed');
tracker.updateItem(id2, 'completed');
tracker.updateItem(id3, 'completed');
tracker.updateItem(id4, 'in_progress', 'Analyzing: "Show me sales by region"');
tracker.updateItem(id5, 'pending');
tracker.updateItem(id6, 'pending');
tracker.updateItem(id7, 'pending');
}
}, 100);
return html`
<div style="width: 350px; height: 400px;">
<vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>
</div>
`;
},
};
export const WithError: Story = {
args: {
title: 'Query Processing',
},
render: (args) => {
setTimeout(() => {
const tracker = document.querySelector('vanna-progress-tracker') as any;
if (tracker) {
const id1 = tracker.addItem('Parse request', 'Understanding user query');
const id2 = tracker.addItem('Generate SQL', 'Creating database query');
const id3 = tracker.addItem('Execute query', 'Running against database');
tracker.addItem('Format results', 'Preparing visualization');
tracker.updateItem(id1, 'completed');
tracker.updateItem(id2, 'completed');
tracker.updateItem(id3, 'error', 'Table "sales_data" does not exist');
// id4 should remain pending due to error
}
}, 100);
return html`
<div style="width: 350px; height: 400px;">
<vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>
</div>
`;
},
};
export const MultipleErrors: Story = {
args: {
title: 'Error Scenarios',
},
render: (args) => {
setTimeout(() => {
const tracker = document.querySelector('vanna-progress-tracker') as any;
if (tracker) {
const id1 = tracker.addItem('Connect to database', 'Establishing connection');
const id2 = tracker.addItem('Validate schema', 'Checking table structure');
const id3 = tracker.addItem('Parse SQL query', 'Analyzing syntax');
tracker.addItem('Execute query', 'Running database command');
tracker.addItem('Process results', 'Formatting output');
tracker.updateItem(id1, 'error', 'Connection timeout - database unreachable');
tracker.updateItem(id2, 'error', 'Invalid credentials provided');
tracker.updateItem(id3, 'error', 'Syntax error in SQL query');
// Other items remain pending
}
}, 100);
return html`
<div style="width: 350px; height: 400px;">
<vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>
<p style="font-size: 12px; color: #666; margin-top: 10px;">
Example showing multiple error states with detailed error messages
</p>
</div>
`;
},
};
export const LiveDemo: Story = {
args: {
title: 'Live Progress Demo',
},
render: (args) => {
let tracker: any;
let taskIds: string[] = [];
let currentIndex = 0;
const tasks = [
{ text: 'Initialize AI agent', detail: 'Loading language model' },
{ text: 'Analyze user request', detail: 'Processing natural language' },
{ text: 'Query database schema', detail: 'Understanding data structure' },
{ text: 'Generate SQL query', detail: 'Converting request to SQL' },
{ text: 'Execute query', detail: 'Running against database' },
{ text: 'Process results', detail: 'Formatting data for display' },
{ text: 'Generate visualization', detail: 'Creating charts and graphs' }
];
const runDemo = () => {
if (!tracker) {
tracker = document.querySelector('vanna-progress-tracker');
if (!tracker) {
setTimeout(runDemo, 100);
return;
}
}
// Add all tasks as pending
if (taskIds.length === 0) {
taskIds = tasks.map(task => tracker.addItem(task.text, task.detail));
currentIndex = 0;
}
// Process tasks one by one
if (currentIndex < tasks.length) {
// Mark current as in_progress
tracker.updateItem(taskIds[currentIndex], 'in_progress', `${tasks[currentIndex].detail}...`);
// Complete after 2 seconds, then move to next
setTimeout(() => {
tracker.updateItem(taskIds[currentIndex], 'completed');
currentIndex++;
// Continue with next task
if (currentIndex < tasks.length) {
setTimeout(runDemo, 500);
} else {
// Demo complete - restart after 3 seconds
setTimeout(() => {
tracker.clearItems();
taskIds = [];
currentIndex = 0;
setTimeout(runDemo, 1000);
}, 3000);
}
}, 2000);
}
};
setTimeout(runDemo, 500);
return html`
<div style="width: 350px; height: 400px;">
<vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>
<div style="margin-top: 10px; color: #999; font-size: 12px; text-align: center;">
Watch tasks complete automatically (demo loops)
</div>
</div>
`;
},
};

View File

@@ -0,0 +1,263 @@
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
interface ProgressItem {
id: string;
text: string;
status: 'pending' | 'in_progress' | 'completed' | 'error';
detail?: string;
}
@customElement('vanna-progress-tracker')
export class VannaProgressTracker extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
background: var(--vanna-background-default);
border: 1px solid var(--vanna-outline-default);
border-radius: 0 0 var(--vanna-border-radius-lg) var(--vanna-border-radius-lg);
overflow: hidden;
font-family: var(--vanna-font-family-default);
}
.progress-label {
padding: var(--vanna-space-3) var(--vanna-space-4) var(--vanna-space-2);
display: flex;
align-items: center;
justify-content: space-between;
}
.progress-label-text {
font-size: 11px;
font-weight: 500;
color: var(--vanna-foreground-dimmest);
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0;
}
.progress-summary {
font-size: 10px;
color: var(--vanna-foreground-dimmest);
font-weight: 400;
}
.progress-list {
max-height: 300px;
overflow-y: auto;
padding-top: 0;
}
.progress-item {
padding: var(--vanna-space-3) var(--vanna-space-4);
border-bottom: 1px solid var(--vanna-outline-dimmest);
display: flex;
align-items: flex-start;
gap: var(--vanna-space-3);
transition: background var(--vanna-duration-150) ease;
}
.progress-item:last-child {
border-bottom: none;
}
.progress-item:hover {
background: var(--vanna-background-higher);
}
.progress-item.in_progress {
background: rgba(0, 123, 255, 0.05);
border-left: 3px solid var(--vanna-accent-primary-default);
}
.progress-item.completed {
opacity: 0.7;
}
.progress-item.error {
background: var(--vanna-accent-negative-subtle);
border-left: 3px solid var(--vanna-accent-negative-default);
padding-left: calc(var(--vanna-space-3) - 3px);
}
.progress-item.error .progress-text {
color: var(--vanna-accent-negative-stronger);
}
.progress-item.error .progress-detail {
color: var(--vanna-accent-negative-default);
font-weight: 500;
}
.progress-icon {
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
}
.progress-icon.pending {
background: var(--vanna-outline-default);
}
.progress-icon.in_progress {
background: var(--vanna-accent-primary-default);
}
.progress-icon.completed {
background: var(--vanna-accent-positive-default);
}
.progress-icon.error {
background: var(--vanna-accent-negative-default);
box-shadow: 0 0 0 2px var(--vanna-accent-negative-subtle);
}
.progress-icon svg {
width: 10px;
height: 10px;
color: white;
}
.progress-icon.error svg {
width: 8px;
height: 8px;
color: white;
}
.spinner-mini {
width: 10px;
height: 10px;
border: 1.5px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.progress-content {
flex: 1;
min-width: 0;
}
.progress-text {
font-size: 13px;
color: var(--vanna-foreground-default);
font-weight: 500;
margin: 0 0 var(--vanna-space-1) 0;
line-height: 1.3;
}
.progress-detail {
font-size: 11px;
color: var(--vanna-foreground-dimmest);
margin: 0;
line-height: 1.3;
}
.empty-state {
padding: var(--vanna-space-6) var(--vanna-space-4);
text-align: center;
color: var(--vanna-foreground-dimmest);
font-size: 12px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`
];
@property() title = 'Progression';
@property() theme = 'light';
@state() private items: ProgressItem[] = [];
addItem(text: string, detail?: string, id?: string): string {
const itemId = id || Date.now().toString();
this.items = [...this.items, {
id: itemId,
text,
status: 'pending',
detail
}];
return itemId;
}
updateItem(id: string, status: ProgressItem['status'], detail?: string) {
this.items = this.items.map(item =>
item.id === id ? { ...item, status, detail } : item
);
}
clearItems() {
this.items = [];
}
private getStatusIcon(status: ProgressItem['status']) {
switch (status) {
case 'pending':
return html``;
case 'in_progress':
return html`<div class="spinner-mini"></div>`;
case 'completed':
return html`
<svg viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
`;
case 'error':
return html`
<svg viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
`;
}
}
private getProgressSummary() {
const completed = this.items.filter(item => item.status === 'completed').length;
const total = this.items.length;
const inProgress = this.items.filter(item => item.status === 'in_progress').length;
if (inProgress > 0) {
return `${completed}/${total} terminé${completed > 1 ? 's' : ''}`;
}
return total > 0 ? `${completed}/${total} terminé${completed > 1 ? 's' : ''}` : '';
}
render() {
return html`
${this.items.length > 0 ? html`
<div class="progress-label">
<span class="progress-label-text">Tâches</span>
<span class="progress-summary">${this.getProgressSummary()}</span>
</div>
` : ''}
<div class="progress-list">
${this.items.length === 0
? html`<div class="empty-state">Aucune tâche en cours</div>`
: this.items.map(item => html`
<div class="progress-item ${item.status}">
<div class="progress-icon ${item.status}">
${this.getStatusIcon(item.status)}
</div>
<div class="progress-content">
<p class="progress-text">${item.text}</p>
${item.detail ? html`<p class="progress-detail">${item.detail}</p>` : ''}
</div>
</div>
`)
}
</div>
`;
}
}

View File

@@ -0,0 +1,168 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
import './vanna-status-bar';
const meta: Meta = {
title: 'Components/VannaStatusBar',
component: 'vanna-status-bar',
parameters: {
layout: 'centered',
backgrounds: {
default: 'light',
values: [
{ name: 'dark', value: 'rgb(11, 15, 25)' },
{ name: 'light', value: '#ffffff' },
],
},
},
argTypes: {
status: {
control: 'select',
options: ['idle', 'working', 'error', 'success'],
},
message: { control: 'text' },
detail: { control: 'text' },
theme: {
control: 'select',
options: ['dark', 'light'],
description: 'Theme variant'
},
},
};
export default meta;
type Story = StoryObj;
export const Idle: Story = {
args: {
status: 'idle',
message: '',
detail: '',
},
render: (args) => html`
<div style="width: 500px; padding: 20px;">
<vanna-status-bar
.status=${args.status}
.message=${args.message}
.detail=${args.detail}>
</vanna-status-bar>
<div style="margin-top: 10px; color: #999; font-size: 12px;">
Status bar is hidden when idle
</div>
</div>
`,
};
export const Working: Story = {
args: {
status: 'working',
message: 'Analyzing your database schema...',
detail: 'Step 1 of 3',
theme: 'light',
},
render: (args) => html`
<div style="width: 500px; padding: 20px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'};">
<vanna-status-bar
.status=${args.status}
.message=${args.message}
.detail=${args.detail}
theme=${args.theme}>
</vanna-status-bar>
</div>
`,
};
export const WorkingLight: Story = {
args: {
status: 'working',
message: 'Analyzing your database schema...',
detail: 'Step 1 of 3',
theme: 'light',
},
render: (args) => html`
<div style="width: 500px; padding: 20px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'};">
<vanna-status-bar
.status=${args.status}
.message=${args.message}
.detail=${args.detail}
theme=${args.theme}>
</vanna-status-bar>
</div>
`,
};
export const Success: Story = {
args: {
status: 'success',
message: 'Query executed successfully',
detail: '2.3s',
},
render: (args) => html`
<div style="width: 500px; padding: 20px;">
<vanna-status-bar
.status=${args.status}
.message=${args.message}
.detail=${args.detail}>
</vanna-status-bar>
</div>
`,
};
export const Error: Story = {
args: {
status: 'error',
message: 'Failed to connect to database',
detail: 'Connection timeout after 30s',
},
render: (args) => html`
<div style="width: 500px; padding: 20px;">
<vanna-status-bar
.status=${args.status}
.message=${args.message}
.detail=${args.detail}>
</vanna-status-bar>
</div>
`,
};
export const StatusSequence: Story = {
render: () => {
let statusBar: any;
let currentIndex = 0;
const statuses = [
{ status: 'working', message: 'Starting analysis...', detail: 'Initializing' },
{ status: 'working', message: 'Querying database...', detail: 'Step 1 of 3' },
{ status: 'working', message: 'Processing results...', detail: 'Step 2 of 3' },
{ status: 'working', message: 'Generating visualization...', detail: 'Step 3 of 3' },
{ status: 'success', message: 'Analysis complete!', detail: '4.2s total' },
];
const updateStatus = () => {
if (statusBar && currentIndex < statuses.length) {
const current = statuses[currentIndex];
statusBar.status = current.status;
statusBar.message = current.message;
statusBar.detail = current.detail;
currentIndex++;
if (currentIndex < statuses.length) {
setTimeout(updateStatus, 2000);
}
}
};
setTimeout(() => {
statusBar = document.querySelector('vanna-status-bar');
updateStatus();
}, 100);
return html`
<div style="width: 500px; padding: 20px;">
<vanna-status-bar status="idle"></vanna-status-bar>
<div style="margin-top: 10px; color: #999; font-size: 12px;">
Watch the status bar cycle through different states
</div>
</div>
`;
},
};

View File

@@ -0,0 +1,433 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { vannaDesignTokens } from '../styles/vanna-design-tokens.js';
@customElement('vanna-status-bar')
export class VannaStatusBar extends LitElement {
static styles = [
vannaDesignTokens,
css`
:host {
display: block;
background: var(--vanna-background-default);
border: 1px solid var(--vanna-outline-default);
border-radius: var(--vanna-border-radius-lg);
padding: var(--vanna-space-3) var(--vanna-space-4);
margin-bottom: var(--vanna-space-3);
font-family: var(--vanna-font-family-default);
font-size: 13px;
font-weight: 500;
color: var(--vanna-foreground-default);
box-shadow: var(--vanna-shadow-xs);
/* Animation properties */
opacity: 1;
transform: translateY(0) scale(1);
max-height: 200px;
overflow: hidden;
transition:
opacity var(--vanna-duration-300) cubic-bezier(0.4, 0, 0.2, 1),
transform var(--vanna-duration-300) cubic-bezier(0.4, 0, 0.2, 1),
max-height var(--vanna-duration-300) ease,
margin var(--vanna-duration-300) ease,
padding var(--vanna-duration-300) ease,
box-shadow var(--vanna-duration-200) ease;
}
/* Hide when there's no actual content */
:host(.no-content) {
opacity: 0;
transform: translateY(-8px) scale(0.95);
max-height: 0;
margin: 0;
padding: 0;
pointer-events: none;
}
:host(:empty) {
display: none;
}
/* Entrance animation when content appears */
:host(.entering) {
animation: statusEnter var(--vanna-duration-300) ease-out;
}
/* Exit animation when content disappears */
:host(.exiting) {
animation: statusExit var(--vanna-duration-300) ease-in;
}
@keyframes statusEnter {
0% {
opacity: 0;
transform: translateY(-12px) scale(0.9);
max-height: 0;
}
50% {
opacity: 0.8;
transform: translateY(-2px) scale(1.02);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
max-height: 200px;
}
}
@keyframes statusExit {
0% {
opacity: 1;
transform: translateY(0) scale(1);
max-height: 200px;
}
50% {
opacity: 0.5;
transform: translateY(-4px) scale(0.98);
}
100% {
opacity: 0;
transform: translateY(-12px) scale(0.9);
max-height: 0;
}
}
:host([status="working"]) {
background: var(--vanna-accent-primary-default);
border-color: var(--vanna-accent-primary-default);
color: white;
box-shadow:
var(--vanna-shadow-md),
0 0 0 1px rgba(59, 130, 246, 0.2);
}
:host([status="error"]) {
background: var(--vanna-accent-negative-subtle);
border-color: var(--vanna-accent-negative-default);
color: var(--vanna-accent-negative-stronger);
box-shadow: var(--vanna-shadow-sm);
animation: errorShake 0.5s ease-in-out;
}
:host([status="success"]) {
background: var(--vanna-accent-positive-subtle);
border-color: var(--vanna-accent-positive-default);
color: var(--vanna-accent-positive-stronger);
box-shadow: var(--vanna-shadow-sm);
animation: successPulse 0.6s ease-out;
}
@keyframes errorShake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
20%, 40%, 60%, 80% { transform: translateX(4px); }
}
@keyframes successPulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.status-content {
display: flex;
align-items: center;
gap: var(--vanna-space-3);
animation: contentFadeIn var(--vanna-duration-200) ease-out;
}
@keyframes contentFadeIn {
0% {
opacity: 0;
transform: translateY(4px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: var(--vanna-border-radius-full);
background: var(--vanna-accent-primary-default);
flex-shrink: 0;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 2px 8px rgba(0, 0, 0, 0.15);
}
.status-indicator.working {
background: white;
animation: workingPulse 1.5s ease-in-out infinite;
}
.status-indicator.error {
background: linear-gradient(45deg, var(--vanna-accent-negative-default), var(--vanna-accent-negative-stronger));
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 0 8px rgba(239, 68, 68, 0.4);
}
.status-indicator.success {
background: linear-gradient(45deg, var(--vanna-accent-positive-default), var(--vanna-accent-positive-stronger));
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 0 8px rgba(16, 185, 129, 0.4);
}
.spinner {
width: 16px;
height: 16px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: var(--vanna-border-radius-full);
animation: spin 1s linear infinite, spinnerGlow 2s ease-in-out infinite;
flex-shrink: 0;
}
.status-text {
flex: 1;
font-weight: 600;
line-height: 1.4;
letter-spacing: 0.01em;
}
.status-detail {
font-size: 12px;
color: var(--vanna-foreground-dimmest);
margin-left: var(--vanna-space-4);
opacity: 0.9;
font-weight: 500;
}
.status-actions {
display: flex;
align-items: center;
gap: var(--vanna-space-2);
margin-left: auto;
}
.status-button {
padding: var(--vanna-space-1) var(--vanna-space-2);
border: 1px solid var(--vanna-outline-default);
border-radius: var(--vanna-border-radius-sm);
background: var(--vanna-background-subtle);
color: var(--vanna-foreground-dimmer);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all var(--vanna-duration-150) ease;
}
.status-button:hover {
background: var(--vanna-background-higher);
border-color: var(--vanna-outline-hover);
color: var(--vanna-foreground-default);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
}
@keyframes workingPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 2px 8px rgba(255, 255, 255, 0.3);
}
50% {
opacity: 0.9;
transform: scale(1.2);
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.9), 0 4px 12px rgba(255, 255, 255, 0.5);
}
}
@keyframes spinnerGlow {
0%, 100% {
filter: drop-shadow(0 0 2px rgba(21, 168, 168, 0.5));
}
50% {
filter: drop-shadow(0 0 6px rgba(21, 168, 168, 0.8));
}
}
@keyframes errorGlow {
0% {
box-shadow:
var(--vanna-shadow-xl),
0 0 0 2px rgba(239, 68, 68, 0.3),
0 0 20px rgba(239, 68, 68, 0.2);
}
50% {
box-shadow:
var(--vanna-shadow-2xl),
0 0 0 3px rgba(239, 68, 68, 0.4),
0 0 30px rgba(239, 68, 68, 0.3);
}
100% {
box-shadow:
var(--vanna-shadow-xl),
0 0 0 2px rgba(239, 68, 68, 0.3),
0 0 20px rgba(239, 68, 68, 0.2);
}
}
@keyframes successGlow {
0% {
box-shadow:
var(--vanna-shadow-xl),
0 0 0 2px rgba(16, 185, 129, 0.3),
0 0 20px rgba(16, 185, 129, 0.2);
}
50% {
box-shadow:
var(--vanna-shadow-2xl),
0 0 0 3px rgba(16, 185, 129, 0.4),
0 0 30px rgba(16, 185, 129, 0.3);
}
100% {
box-shadow:
var(--vanna-shadow-xl),
0 0 0 2px rgba(16, 185, 129, 0.3),
0 0 20px rgba(16, 185, 129, 0.2);
}
}
/* Dark theme overrides */
:host([theme="dark"]) {
background: var(--vanna-background-higher);
border-color: var(--vanna-outline-default);
}
:host([theme="dark"]) .status-button {
background: var(--vanna-background-highest);
border-color: var(--vanna-outline-default);
}
:host([theme="dark"]) .status-button:hover {
background: var(--vanna-background-highest);
border-color: var(--vanna-outline-hover);
}
`
];
@property() status: 'idle' | 'working' | 'error' | 'success' = 'idle';
@property() message = '';
@property() detail = '';
@property() theme = 'light';
private _previousHasContent = false;
private _enterTimeout: number | null = null;
private _exitTimeout: number | null = null;
private _lastUpdateTime = 0;
disconnectedCallback() {
super.disconnectedCallback();
// Clean up pending animation timeouts when component is removed
if (this._enterTimeout !== null) {
clearTimeout(this._enterTimeout);
this._enterTimeout = null;
}
if (this._exitTimeout !== null) {
clearTimeout(this._exitTimeout);
this._exitTimeout = null;
}
}
updated(_changedProperties: Map<string | number | symbol, unknown>) {
// Update CSS class based on content
const hasContent = Boolean(this.message && this.message.trim());
// Cancel any pending animation timeouts to prevent race conditions
if (this._enterTimeout !== null) {
clearTimeout(this._enterTimeout);
this._enterTimeout = null;
}
if (this._exitTimeout !== null) {
clearTimeout(this._exitTimeout);
this._exitTimeout = null;
}
// Debounce rapid updates to prevent animation jank
const now = Date.now();
const timeSinceLastUpdate = now - this._lastUpdateTime;
const shouldDebounce = timeSinceLastUpdate < 100; // 100ms debounce
// Handle animation classes
if (hasContent !== this._previousHasContent) {
if (hasContent) {
// Content appeared - animate in
this.classList.remove('no-content', 'exiting');
if (!shouldDebounce) {
// Only animate if not rapid-firing
this.classList.add('entering');
// Remove entering class after animation
this._enterTimeout = window.setTimeout(() => {
this.classList.remove('entering');
this._enterTimeout = null;
}, 300);
}
} else {
// Content disappeared - animate out
this.classList.remove('entering');
if (!shouldDebounce) {
// Only animate if not rapid-firing
this.classList.add('exiting');
// Add no-content class after animation
this._exitTimeout = window.setTimeout(() => {
this.classList.remove('exiting');
this.classList.add('no-content');
this._exitTimeout = null;
}, 300);
} else {
// If rapid-firing, skip animation and go straight to no-content
this.classList.add('no-content');
}
}
} else if (!hasContent) {
// Ensure no-content class is applied when no content
this.classList.add('no-content');
}
this._previousHasContent = hasContent;
this._lastUpdateTime = now;
}
render() {
// Only show if there's actual content (message) to display
if (!this.message || !this.message.trim()) {
return html``;
}
return html`
<div class="status-content">
${this.status === 'working'
? html`<div class="spinner"></div>`
: html`<div class="status-indicator ${this.status}"></div>`
}
<span class="status-text">${this.message}</span>
${this.detail ? html`<span class="status-detail">${this.detail}</span>` : ''}
</div>
`;
}
}

View File

@@ -0,0 +1,37 @@
// Log build information when the module loads
console.log(
'%c🎨 AIVANOV Components',
'color: #4CAF50; font-weight: bold; font-size: 14px;'
);
console.log(
`%c📦 Version: ${__BUILD_VERSION__}`,
'color: #2196F3; font-weight: bold;'
);
console.log(
`%c🕐 Built: ${__BUILD_TIME__}`,
'color: #FF9800; font-weight: bold;'
);
console.log(
'%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
'color: #9E9E9E;'
);
export { VannaChat } from './components/vanna-chat';
export { VannaMessage } from './components/vanna-message';
export { VannaStatusBar } from './components/vanna-status-bar';
export { VannaProgressTracker } from './components/vanna-progress-tracker';
export { PlotlyChart } from './components/plotly-chart';
// Rich component system
export {
ComponentRegistry,
ComponentManager,
CardComponentRenderer,
TaskListComponentRenderer,
ProgressBarComponentRenderer,
NotificationComponentRenderer,
StatusIndicatorComponentRenderer,
TextComponentRenderer
} from './components/rich-component-system';
// Rich component styles are injected automatically by the ComponentManager

View File

@@ -0,0 +1,296 @@
/**
* API client for communicating with Vanna Agents backend
*/
export interface ChatMessage {
id: string;
content: string;
type: 'user' | 'assistant';
timestamp: number;
}
export interface ChatRequest {
message: string;
conversation_id?: string;
user_id?: string;
request_id?: string;
metadata?: Record<string, any>;
}
export interface ChatStreamChunk {
rich: Record<string, any>;
simple?: Record<string, any>;
conversation_id: string;
request_id: string;
timestamp: number;
}
export interface ChatResponse {
chunks: ChatStreamChunk[];
conversation_id: string;
request_id: string;
total_chunks: number;
}
export interface ApiClientConfig {
baseUrl?: string;
sseEndpoint?: string;
wsEndpoint?: string;
pollEndpoint?: string;
timeout?: number;
customHeaders?: Record<string, string>;
}
export class VannaApiClient {
public readonly baseUrl: string;
private sseEndpoint: string;
private wsEndpoint: string;
private pollEndpoint: string;
private timeout: number;
private customHeaders: Record<string, string>;
constructor(config: ApiClientConfig = {}) {
this.baseUrl = config.baseUrl || '';
this.sseEndpoint = config.sseEndpoint || '/api/vanna/v2/chat_sse';
this.wsEndpoint = config.wsEndpoint || '/api/vanna/v2/chat_websocket';
this.pollEndpoint = config.pollEndpoint || '/api/vanna/v2/chat_poll';
this.timeout = config.timeout || 30000;
this.customHeaders = config.customHeaders || {};
console.log('[VannaApiClient] Constructor called with config:', config);
console.log('[VannaApiClient] Endpoint configuration:');
console.log(' - SSE endpoint:', this.sseEndpoint, config.sseEndpoint ? '(custom)' : '(default)');
console.log(' - WS endpoint:', this.wsEndpoint, config.wsEndpoint ? '(custom)' : '(default)');
console.log(' - Poll endpoint:', this.pollEndpoint, config.pollEndpoint ? '(custom)' : '(default)');
console.log(' - Base URL:', this.baseUrl || '(empty)');
}
/**
* Update custom headers (e.g., for authentication)
*/
setCustomHeaders(headers: Record<string, string>) {
this.customHeaders = headers;
}
/**
* Get current custom headers
*/
getCustomHeaders(): Record<string, string> {
return { ...this.customHeaders };
}
/**
* Send message using Server-Sent Events (SSE) streaming
*/
async *streamChat(request: ChatRequest): AsyncGenerator<ChatStreamChunk, void, unknown> {
const url = this.sseEndpoint.startsWith('http')
? this.sseEndpoint
: `${this.baseUrl}${this.sseEndpoint}`;
console.log('[VannaApiClient] SSE streaming to URL:', url);
console.log('[VannaApiClient] SSE endpoint config:', {
baseUrl: this.baseUrl,
sseEndpoint: this.sseEndpoint,
constructedUrl: url
});
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
...this.customHeaders,
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') {
return;
}
try {
const chunk = JSON.parse(data) as ChatStreamChunk;
yield chunk;
} catch (e) {
console.warn('Failed to parse SSE chunk:', data, e);
}
}
}
}
} finally {
reader.releaseLock();
}
}
/**
* Send message using WebSocket
*/
createWebSocketConnection(): Promise<WebSocket> {
return new Promise((resolve, reject) => {
let wsUrl: string;
if (this.wsEndpoint.startsWith('ws://') || this.wsEndpoint.startsWith('wss://')) {
// Absolute WebSocket URL provided
wsUrl = this.wsEndpoint;
} else {
// Relative path - construct from baseUrl
if (this.baseUrl) {
// Parse baseUrl to extract host and convert http(s) to ws(s)
const baseUrlObj = new URL(this.baseUrl);
const wsProtocol = baseUrlObj.protocol === 'https:' ? 'wss:' : 'ws:';
wsUrl = `${wsProtocol}//${baseUrlObj.host}${this.wsEndpoint}`;
} else {
// Fallback to window.location
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
wsUrl = `${protocol}//${window.location.host}${this.wsEndpoint}`;
}
}
const ws = new WebSocket(wsUrl);
ws.onopen = () => resolve(ws);
ws.onerror = (error) => reject(error);
// Set timeout
setTimeout(() => {
if (ws.readyState === WebSocket.CONNECTING) {
ws.close();
reject(new Error('WebSocket connection timeout'));
}
}, this.timeout);
});
}
/**
* Send message via WebSocket
*/
async sendWebSocketMessage(
ws: WebSocket,
request: ChatRequest
): Promise<AsyncGenerator<ChatStreamChunk, void, unknown>> {
return new Promise((resolve, reject) => {
if (ws.readyState !== WebSocket.OPEN) {
reject(new Error('WebSocket not connected'));
return;
}
async function* generator() {
let isCompleted = false;
const messageQueue: ChatStreamChunk[] = [];
let resolveNext: ((value: IteratorResult<ChatStreamChunk>) => void) | null = null;
const messageHandler = (event: MessageEvent) => {
try {
const chunk = JSON.parse(event.data) as ChatStreamChunk;
if (chunk.rich?.type === 'completion') {
isCompleted = true;
if (resolveNext) {
resolveNext({ done: true, value: undefined });
resolveNext = null;
}
return;
}
if (chunk.rich?.type === 'error') {
ws.removeEventListener('message', messageHandler);
if (resolveNext) {
resolveNext({ done: true, value: undefined });
}
return;
}
if (resolveNext) {
resolveNext({ done: false, value: chunk });
resolveNext = null;
} else {
messageQueue.push(chunk);
}
} catch (e) {
console.warn('Failed to parse WebSocket message:', event.data, e);
}
};
ws.addEventListener('message', messageHandler);
while (!isCompleted) {
if (messageQueue.length > 0) {
yield messageQueue.shift()!;
} else {
await new Promise<IteratorResult<ChatStreamChunk>>((resolve) => {
resolveNext = resolve;
});
}
}
ws.removeEventListener('message', messageHandler);
}
try {
ws.send(JSON.stringify(request));
resolve(generator());
} catch (error) {
reject(error);
}
});
}
/**
* Send message using polling (fallback option)
*/
async sendPollMessage(request: ChatRequest): Promise<ChatResponse> {
const url = this.pollEndpoint.startsWith('http')
? this.pollEndpoint
: `${this.baseUrl}${this.pollEndpoint}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.customHeaders,
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<ChatResponse>;
}
/**
* Generate unique IDs for conversations and requests
*/
generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
}
}
/**
* Default API client instance
*/
export const apiClient = new VannaApiClient();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
import { css } from 'lit';
// AIVANOV design tokens - Analyse de données par IA
export const vannaDesignTokens = css`
:host {
/* AIVANOV Brand Colors */
--vanna-navy: rgb(15, 23, 42);
--vanna-cream: rgb(248, 250, 252);
--vanna-teal: rgb(59, 130, 246);
--vanna-orange: rgb(249, 115, 22);
--vanna-magenta: rgb(191, 19, 99);
/* Color Palette - Light mode (default) */
--vanna-background-root: rgb(255, 255, 255);
--vanna-background-default: rgb(248, 250, 252);
--vanna-background-higher: rgb(241, 245, 249);
--vanna-background-highest: rgb(226, 232, 240);
--vanna-background-subtle: rgb(250, 251, 253);
--vanna-background-lower: rgb(241, 245, 249);
--vanna-foreground-default: rgb(15, 23, 42);
--vanna-foreground-dimmer: rgb(71, 85, 105);
--vanna-foreground-dimmest: rgb(148, 163, 184);
--vanna-accent-primary-default: rgb(59, 130, 246);
--vanna-accent-primary-stronger: rgb(37, 99, 235);
--vanna-accent-primary-strongest: rgb(29, 78, 216);
--vanna-accent-primary-subtle: rgba(59, 130, 246, 0.08);
--vanna-accent-primary-hover: rgb(37, 99, 235);
--vanna-accent-positive-default: rgb(34, 197, 94);
--vanna-accent-positive-stronger: rgb(22, 163, 74);
--vanna-accent-positive-subtle: rgba(34, 197, 94, 0.08);
--vanna-accent-negative-default: rgb(239, 68, 68);
--vanna-accent-negative-stronger: rgb(220, 38, 38);
--vanna-accent-negative-subtle: rgba(239, 68, 68, 0.08);
--vanna-accent-warning-default: rgb(245, 158, 11);
--vanna-accent-warning-stronger: rgb(217, 119, 6);
--vanna-accent-warning-subtle: rgba(245, 158, 11, 0.08);
/* Outline/Border colors */
--vanna-outline-default: rgb(226, 232, 240);
--vanna-outline-dimmer: rgb(241, 245, 249);
--vanna-outline-dimmest: rgb(248, 250, 252);
--vanna-outline-hover: rgb(59, 130, 246);
/* Typography */
--vanna-font-family-default: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
--vanna-font-family-serif: "Roboto Slab", ui-serif, Georgia, serif;
--vanna-font-family-mono: "Space Mono", ui-monospace, SFMono-Regular, "SF Mono", Monaco, Inconsolata, "Roboto Mono", "Ubuntu Mono", monospace;
/* Spacing scale */
--vanna-space-0: 0px;
--vanna-space-1: 4px;
--vanna-space-2: 8px;
--vanna-space-3: 12px;
--vanna-space-4: 16px;
--vanna-space-5: 20px;
--vanna-space-6: 24px;
--vanna-space-7: 28px;
--vanna-space-8: 32px;
--vanna-space-10: 40px;
--vanna-space-12: 48px;
--vanna-space-16: 64px;
/* Border radius */
--vanna-border-radius-sm: 6px;
--vanna-border-radius-md: 10px;
--vanna-border-radius-lg: 14px;
--vanna-border-radius-xl: 20px;
--vanna-border-radius-2xl: 24px;
--vanna-border-radius-full: 9999px;
/* Shadows - Preline-inspired */
--vanna-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--vanna-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
--vanna-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--vanna-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--vanna-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
--vanna-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* Animation durations */
--vanna-duration-75: 75ms;
--vanna-duration-100: 100ms;
--vanna-duration-150: 150ms;
--vanna-duration-200: 200ms;
--vanna-duration-300: 300ms;
--vanna-duration-500: 500ms;
--vanna-duration-700: 700ms;
/* Z-index scale */
--vanna-z-dropdown: 1000;
--vanna-z-sticky: 1020;
--vanna-z-fixed: 1030;
--vanna-z-modal: 1040;
--vanna-z-popover: 1050;
--vanna-z-tooltip: 1060;
/* Chat-specific tokens */
--vanna-chat-bubble-radius: 18px;
--vanna-chat-bubble-radius-sm: 12px;
--vanna-chat-spacing: 16px;
--vanna-chat-avatar-size: 40px;
}
/* Dark theme overrides */
:host([theme="dark"]) {
--vanna-background-root: rgb(9, 11, 17);
--vanna-background-default: rgb(15, 18, 25);
--vanna-background-higher: rgb(24, 29, 39);
--vanna-background-highest: rgb(31, 39, 51);
--vanna-background-subtle: rgb(17, 21, 28);
--vanna-background-lower: rgb(6, 8, 12);
--vanna-foreground-default: rgb(248, 250, 252);
--vanna-foreground-dimmer: rgb(203, 213, 225);
--vanna-foreground-dimmest: rgb(148, 163, 184);
--vanna-accent-primary-default: rgb(96, 165, 250);
--vanna-accent-primary-stronger: rgb(59, 130, 246);
--vanna-accent-primary-strongest: rgb(37, 99, 235);
--vanna-accent-primary-subtle: rgba(96, 165, 250, 0.12);
--vanna-accent-primary-hover: rgb(96, 165, 250);
--vanna-accent-positive-default: rgb(74, 222, 128);
--vanna-accent-positive-stronger: rgb(34, 197, 94);
--vanna-accent-positive-subtle: rgba(74, 222, 128, 0.12);
--vanna-accent-negative-default: rgb(248, 113, 113);
--vanna-accent-negative-stronger: rgb(239, 68, 68);
--vanna-accent-negative-subtle: rgba(248, 113, 113, 0.12);
--vanna-accent-warning-default: rgb(251, 191, 36);
--vanna-accent-warning-stronger: rgb(245, 158, 11);
--vanna-accent-warning-subtle: rgba(251, 191, 36, 0.12);
--vanna-outline-default: rgb(51, 65, 85);
--vanna-outline-dimmer: rgb(31, 41, 55);
--vanna-outline-dimmest: rgb(17, 24, 39);
--vanna-outline-hover: rgb(96, 165, 250);
--vanna-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.6);
--vanna-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.5), 0 1px 2px -1px rgba(0, 0, 0, 0.5);
--vanna-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.4);
--vanna-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
--vanna-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
--vanna-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
}
`;

View File

@@ -0,0 +1,4 @@
/// <reference types="vite/client" />
declare const __BUILD_TIME__: string;
declare const __BUILD_VERSION__: string;

View File

@@ -0,0 +1,598 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vanna Webcomponent - Comprehensive Test</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
width: 300px;
background: white;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.sidebar-header h1 {
font-size: 18px;
margin-bottom: 8px;
}
.sidebar-header p {
font-size: 12px;
opacity: 0.9;
}
.controls {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
button {
width: 100%;
padding: 10px 15px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}
button:active {
transform: translateY(0);
}
button.secondary {
background: #f0f0f0;
color: #333;
}
button.secondary:hover {
background: #e0e0e0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
select {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
background: white;
cursor: pointer;
}
.status-section {
padding: 20px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
margin-bottom: 10px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ccc;
}
.status-dot.success {
background: #10b981;
animation: pulse 2s infinite;
}
.status-dot.error {
background: #ef4444;
animation: pulse 2s infinite;
}
.status-dot.warning {
background: #f59e0b;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
font-size: 13px;
font-weight: 500;
color: #333;
}
.console-monitor {
padding: 20px;
border-top: 1px solid #e0e0e0;
}
.console-monitor h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}
.console-log {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 11px;
max-height: 200px;
overflow-y: auto;
}
.console-log .error {
color: #f48771;
}
.console-log .warning {
color: #dcdcaa;
}
.console-log .info {
color: #4fc1ff;
}
.checklist {
padding: 20px;
border-top: 1px solid #e0e0e0;
}
.checklist h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #333;
}
.checklist-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
font-size: 12px;
color: #666;
}
.checklist-item .check {
width: 16px;
height: 16px;
border: 2px solid #e0e0e0;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
}
.checklist-item.checked .check {
background: #10b981;
border-color: #10b981;
color: white;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-container {
flex: 1;
overflow: hidden;
}
vanna-chat {
width: 100%;
height: 100%;
}
.metrics {
padding: 10px 20px;
background: white;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 20px;
font-size: 12px;
color: #666;
}
.metric {
display: flex;
gap: 5px;
}
.metric strong {
color: #333;
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h1>Component Test Suite</h1>
<p>Comprehensive validation for webcomponent pruning</p>
</div>
<!-- Controls -->
<div class="controls">
<div class="control-group">
<label>Test Mode</label>
<select id="mode-select">
<option value="realistic">Realistic (with delays)</option>
<option value="rapid">Rapid (fast)</option>
</select>
</div>
<div class="control-group">
<button id="start-test">Run Comprehensive Test</button>
</div>
<div class="control-group">
<button class="secondary" id="clear-chat">Clear Chat</button>
</div>
</div>
<!-- Status -->
<div class="status-section">
<div class="status-indicator">
<div class="status-dot" id="backend-status"></div>
<div class="status-text" id="backend-text">Checking backend...</div>
</div>
<div class="status-indicator">
<div class="status-dot" id="console-status"></div>
<div class="status-text" id="console-text">No errors detected</div>
</div>
</div>
<!-- Component Checklist -->
<div class="checklist">
<h3>Component Rendering</h3>
<div class="checklist-item" data-component="text">
<div class="check"></div>
<span>Text Component</span>
</div>
<div class="checklist-item" data-component="status_card">
<div class="check"></div>
<span>Status Card</span>
</div>
<div class="checklist-item" data-component="progress_display">
<div class="check"></div>
<span>Progress Display</span>
</div>
<div class="checklist-item" data-component="card">
<div class="check"></div>
<span>Card</span>
</div>
<div class="checklist-item" data-component="task_list">
<div class="check"></div>
<span>Task List</span>
</div>
<div class="checklist-item" data-component="progress_bar">
<div class="check"></div>
<span>Progress Bar</span>
</div>
<div class="checklist-item" data-component="notification">
<div class="check"></div>
<span>Notification</span>
</div>
<div class="checklist-item" data-component="status_indicator">
<div class="check"></div>
<span>Status Indicator</span>
</div>
<div class="checklist-item" data-component="badge">
<div class="check"></div>
<span>Badge</span>
</div>
<div class="checklist-item" data-component="icon_text">
<div class="check"></div>
<span>Icon Text</span>
</div>
<div class="checklist-item" data-component="button">
<div class="check"></div>
<span>Button</span>
</div>
<div class="checklist-item" data-component="button_group">
<div class="check"></div>
<span>Button Group</span>
</div>
<div class="checklist-item" data-component="dataframe">
<div class="check"></div>
<span>DataFrame</span>
</div>
<div class="checklist-item" data-component="chart">
<div class="check"></div>
<span>Chart</span>
</div>
<div class="checklist-item" data-component="artifact">
<div class="check"></div>
<span>Artifact</span>
</div>
<div class="checklist-item" data-component="log_viewer">
<div class="check"></div>
<span>Log Viewer</span>
</div>
<!-- Note: code_block, table, container not supported by webcomponent -->
</div>
<!-- Console Monitor -->
<div class="console-monitor">
<h3>Console Log</h3>
<div class="console-log" id="console-log">
<div class="info">Monitoring console for errors...</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="chat-container">
<vanna-chat
id="vanna-chat"
api-url="http://localhost:5555"
placeholder="Type /test to run comprehensive test..."
></vanna-chat>
</div>
<div class="metrics">
<div class="metric">
<strong>Components Rendered:</strong>
<span id="component-count">0</span>
</div>
<div class="metric">
<strong>Updates Processed:</strong>
<span id="update-count">0</span>
</div>
<div class="metric">
<strong>Errors:</strong>
<span id="error-count">0</span>
</div>
</div>
</div>
<!-- Load webcomponent -->
<script type="module" src="/static/vanna-components.js"></script>
<script type="module">
// State
let componentCount = 0;
let updateCount = 0;
let errorCount = 0;
const seenComponents = new Set();
// Elements
const vannaChat = document.getElementById('vanna-chat');
const modeSelect = document.getElementById('mode-select');
const startTestBtn = document.getElementById('start-test');
const clearChatBtn = document.getElementById('clear-chat');
const consoleLog = document.getElementById('console-log');
const backendStatus = document.getElementById('backend-status');
const backendText = document.getElementById('backend-text');
const consoleStatus = document.getElementById('console-status');
const consoleText = document.getElementById('console-text');
// Check backend health
async function checkBackend() {
try {
const response = await fetch('http://localhost:5555/health');
const data = await response.json();
backendStatus.className = 'status-dot success';
backendText.textContent = `Backend ready (${data.mode} mode)`;
} catch (error) {
backendStatus.className = 'status-dot error';
backendText.textContent = 'Backend not responding';
addConsoleLog('error', `Backend health check failed: ${error.message}`);
}
}
// Console monitoring
function addConsoleLog(type, message) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = type;
logEntry.textContent = `[${timestamp}] ${message}`;
consoleLog.appendChild(logEntry);
consoleLog.scrollTop = consoleLog.scrollHeight;
// Update error status
if (type === 'error') {
errorCount++;
consoleStatus.className = 'status-dot error';
consoleText.textContent = `${errorCount} error(s) detected`;
document.getElementById('error-count').textContent = errorCount;
}
}
// Override console methods
const originalError = console.error;
const originalWarn = console.warn;
const originalLog = console.log;
console.error = function(...args) {
addConsoleLog('error', args.join(' '));
originalError.apply(console, args);
};
console.warn = function(...args) {
addConsoleLog('warning', args.join(' '));
originalWarn.apply(console, args);
};
console.log = function(...args) {
const message = args.join(' ');
if (message.includes('ERROR') || message.includes('Error')) {
addConsoleLog('error', message);
} else {
addConsoleLog('info', message);
}
originalLog.apply(console, args);
};
// Monitor window errors
window.addEventListener('error', (event) => {
addConsoleLog('error', `${event.message} at ${event.filename}:${event.lineno}`);
errorCount++;
});
// Monitor component rendering
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
const componentType = node.getAttribute('data-component-type');
if (componentType) {
// Track component
if (!seenComponents.has(componentType)) {
seenComponents.add(componentType);
componentCount++;
document.getElementById('component-count').textContent = componentCount;
// Check off in checklist
const checklistItem = document.querySelector(`[data-component="${componentType}"]`);
if (checklistItem) {
checklistItem.classList.add('checked');
checklistItem.querySelector('.check').textContent = '✓';
}
}
updateCount++;
document.getElementById('update-count').textContent = updateCount;
}
}
});
});
});
// Start observing when vanna-chat is ready
setTimeout(() => {
const shadowRoot = vannaChat.shadowRoot;
if (shadowRoot) {
const container = shadowRoot.querySelector('.rich-components-container');
if (container) {
observer.observe(container, { childList: true, subtree: true });
addConsoleLog('info', 'Component observer started');
}
}
}, 1000);
// Event listeners
startTestBtn.addEventListener('click', async () => {
const mode = modeSelect.value;
// Update backend mode
try {
await fetch(`http://localhost:5555/health`);
addConsoleLog('info', `Starting comprehensive test in ${mode} mode...`);
// Send test message through chat
vannaChat.dispatchEvent(new CustomEvent('send-message', {
detail: { message: '/test' }
}));
// Alternative: directly trigger if API is exposed
const inputEl = vannaChat.shadowRoot?.querySelector('textarea, input');
if (inputEl) {
inputEl.value = '/test';
const form = vannaChat.shadowRoot?.querySelector('form');
if (form) {
form.dispatchEvent(new Event('submit', { bubbles: true }));
}
}
} catch (error) {
addConsoleLog('error', `Failed to start test: ${error.message}`);
}
});
clearChatBtn.addEventListener('click', () => {
// Reset state
componentCount = 0;
updateCount = 0;
errorCount = 0;
seenComponents.clear();
document.getElementById('component-count').textContent = '0';
document.getElementById('update-count').textContent = '0';
document.getElementById('error-count').textContent = '0';
// Uncheck all checklist items
document.querySelectorAll('.checklist-item').forEach(item => {
item.classList.remove('checked');
item.querySelector('.check').textContent = '';
});
// Clear console
consoleLog.innerHTML = '<div class="info">Console cleared</div>';
consoleStatus.className = 'status-dot';
consoleText.textContent = 'No errors detected';
// Reload page to truly clear (vanna-chat doesn't expose clear method)
location.reload();
});
// Initial backend check
checkBackend();
setInterval(checkBackend, 5000);
// Log startup
addConsoleLog('info', 'Test suite initialized');
addConsoleLog('info', 'Ensure backend is running: python test_backend.py');
</script>
</body>
</html>

View File

@@ -0,0 +1,874 @@
#!/usr/bin/env python3
"""
Comprehensive test backend for vanna-webcomponent validation.
This backend exercises all component types and update patterns to validate
that nothing breaks during webcomponent pruning.
Usage:
python test_backend.py --mode rapid # Fast stress test
python test_backend.py --mode realistic # Realistic conversation flow
"""
import argparse
import asyncio
import json
import sys
import time
import traceback
import uuid
from datetime import datetime
from typing import AsyncGenerator, Dict, Any, Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import os
# Add vanna to path
sys.path.insert(0, "../vanna/src")
from vanna.core.rich_component import RichComponent, ComponentLifecycle
from vanna.components.rich import (
RichTextComponent,
StatusCardComponent,
ProgressDisplayComponent,
ProgressBarComponent,
NotificationComponent,
StatusIndicatorComponent,
ButtonComponent,
ButtonGroupComponent,
CardComponent,
TaskListComponent,
Task,
BadgeComponent,
IconTextComponent,
DataFrameComponent,
ChartComponent,
ArtifactComponent,
LogViewerComponent,
LogEntry,
StatusBarUpdateComponent,
TaskTrackerUpdateComponent,
ChatInputUpdateComponent,
TaskOperation,
)
from vanna.servers.base.models import ChatStreamChunk
# Request/Response models
class ChatRequest(BaseModel):
"""Chat request matching vanna API."""
message: str
conversation_id: Optional[str] = None
request_id: Optional[str] = None
request_context: Dict[str, Any] = {}
class UiComponent(BaseModel):
"""UI component wrapper."""
rich_component: RichComponent
# Test state
test_state: Dict[str, Any] = {
"mode": "realistic",
"component_ids": {}, # Track component IDs for updates
"action_count": 0,
}
async def yield_chunk(component: RichComponent, conversation_id: str, request_id: str) -> ChatStreamChunk:
"""Convert component to ChatStreamChunk."""
return ChatStreamChunk(
rich=component.serialize_for_frontend(),
simple=None,
conversation_id=conversation_id,
request_id=request_id,
timestamp=time.time(),
)
async def delay(mode: str, short: float = 0.1, long: float = 0.5):
"""Add delay based on mode."""
if mode == "realistic":
await asyncio.sleep(long)
elif mode == "rapid":
await asyncio.sleep(short)
async def test_text_component(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test text component with markdown."""
text_id = str(uuid.uuid4())
test_state["component_ids"]["text"] = text_id
# Create with comprehensive markdown
text = RichTextComponent(
id=text_id,
content="""# Test Text Component
This component demonstrates **markdown rendering** with various formatting:
## Formatting Examples
- **Bold text** for emphasis
- *Italic text* for style
- `inline code` for snippets
- ~~Strikethrough~~ for deletions
### Lists
1. First ordered item
2. Second ordered item
3. Third ordered item
### Code Block
```python
def hello():
return "Markdown works!"
```
> Blockquote to test quote rendering
This validates that markdown is properly parsed and displayed.""",
markdown=True,
)
yield await yield_chunk(text, conversation_id, request_id)
await delay(mode)
# Update with simpler markdown
text_updated = text.update(content="""# Updated Text Component
Text has been **successfully updated** with new markdown content!
- Update operation works ✓
- Markdown still renders ✓""")
yield await yield_chunk(text_updated, conversation_id, request_id)
await delay(mode)
async def test_status_card(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test status card with all states."""
card_id = str(uuid.uuid4())
test_state["component_ids"]["status_card"] = card_id
# Create - pending
status_card = StatusCardComponent(
id=card_id,
title="Status Card Test",
status="pending",
description="Testing status card component...",
icon="",
collapsible=True,
collapsed=False,
)
yield await yield_chunk(status_card, conversation_id, request_id)
await delay(mode)
# Update to running
status_card_running = status_card.set_status("running", "Processing test...")
yield await yield_chunk(status_card_running, conversation_id, request_id)
await delay(mode)
# Update to completed
status_card_done = status_card.set_status("completed", "Test completed successfully!")
status_card_done.icon = ""
yield await yield_chunk(status_card_done, conversation_id, request_id)
await delay(mode)
async def test_progress_display(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test progress display component."""
progress_id = str(uuid.uuid4())
test_state["component_ids"]["progress_display"] = progress_id
# Create at 0%
progress = ProgressDisplayComponent(
id=progress_id,
label="Test Progress",
value=0.0,
description="Starting test...",
status="info",
animated=True,
)
yield await yield_chunk(progress, conversation_id, request_id)
await delay(mode, 0.05, 0.3)
# Update to 50%
progress_half = progress.update_progress(0.5, "Halfway there...")
yield await yield_chunk(progress_half, conversation_id, request_id)
await delay(mode, 0.05, 0.3)
# Update to 100%
progress_done = progress.update_progress(1.0, "Complete!")
progress_done.status = "success"
yield await yield_chunk(progress_done, conversation_id, request_id)
await delay(mode)
async def test_card_component(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test card component with actions."""
card_id = str(uuid.uuid4())
test_state["component_ids"]["card"] = card_id
# Create card with markdown content and buttons
card = CardComponent(
id=card_id,
title="Test Card with Markdown",
content="""# Card Content
This card demonstrates **markdown rendering** within cards:
- Interactive action buttons
- Collapsible sections
- Status indicators
- `Formatted text`
Click the buttons below to test interactivity!""",
icon="🃏",
status="info",
markdown=True,
collapsible=True,
collapsed=False,
actions=[
{"label": "Test Action", "action": "/test-action", "variant": "primary"},
{"label": "Cancel", "action": "/cancel", "variant": "secondary"},
],
)
yield await yield_chunk(card, conversation_id, request_id)
await delay(mode)
# Update card status and content
card_updated = card.update(
status="success",
content="""# Card Updated Successfully!
The card content has been **updated** with:
- New status (success)
- New markdown content
- Same action buttons
✓ Update operation verified""",
markdown=True
)
yield await yield_chunk(card_updated, conversation_id, request_id)
await delay(mode)
async def test_task_list(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test task list component."""
task_list_id = str(uuid.uuid4())
test_state["component_ids"]["task_list"] = task_list_id
# Create task list
tasks = [
Task(title="Setup development environment", description="Install dependencies and configure tools", status="completed", progress=1.0),
Task(title="Write test suite", description="Create comprehensive component tests", status="in_progress", progress=0.7),
Task(title="Run validation", description="Validate all components render correctly", status="pending"),
Task(title="Prune webcomponent", description="Remove unused code and cruft", status="pending"),
]
task_list = TaskListComponent(
id=task_list_id,
title="Webcomponent Validation Workflow",
tasks=tasks,
show_progress=True,
show_timestamps=True,
)
yield await yield_chunk(task_list, conversation_id, request_id)
await delay(mode)
# Update task statuses
tasks[1].status = "completed"
tasks[1].progress = 1.0
tasks[2].status = "in_progress"
tasks[2].progress = 0.3
task_list_updated = TaskListComponent(
id=task_list_id,
title="Webcomponent Validation Workflow (Updated)",
tasks=tasks,
show_progress=True,
show_timestamps=True,
)
task_list_updated.lifecycle = ComponentLifecycle.UPDATE
yield await yield_chunk(task_list_updated, conversation_id, request_id)
await delay(mode)
async def test_progress_bar(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test progress bar component."""
bar_id = str(uuid.uuid4())
test_state["component_ids"]["progress_bar"] = bar_id
# Create
bar = ProgressBarComponent(
id=bar_id,
value=0.3,
label="Loading",
status="info",
)
yield await yield_chunk(bar, conversation_id, request_id)
await delay(mode, 0.05, 0.2)
# Update
bar_updated = bar.update(value=0.8, status="success")
yield await yield_chunk(bar_updated, conversation_id, request_id)
await delay(mode)
async def test_notification(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test notification component."""
for level in ["info", "success", "warning", "error"]:
notif = NotificationComponent(
id=str(uuid.uuid4()),
message=f"This is a {level} notification",
level=level,
title=f"{level.capitalize()} Test",
)
yield await yield_chunk(notif, conversation_id, request_id)
await delay(mode, 0.05, 0.2)
async def test_status_indicator(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test status indicator component."""
indicator_id = str(uuid.uuid4())
test_state["component_ids"]["status_indicator"] = indicator_id
# Create with pulse
indicator = StatusIndicatorComponent(
id=indicator_id,
status="running",
message="Processing...",
pulse=True,
)
yield await yield_chunk(indicator, conversation_id, request_id)
await delay(mode)
# Update to success
indicator_success = indicator.update(status="success", message="Done!", pulse=False)
yield await yield_chunk(indicator_success, conversation_id, request_id)
await delay(mode)
async def test_badge(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test badge component."""
badge = BadgeComponent(
id=str(uuid.uuid4()),
text="Test Badge",
variant="primary",
)
yield await yield_chunk(badge, conversation_id, request_id)
await delay(mode)
async def test_icon_text(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test icon_text component."""
icon_text = IconTextComponent(
id=str(uuid.uuid4()),
icon="🔧",
text="Tool Icon Test",
)
yield await yield_chunk(icon_text, conversation_id, request_id)
await delay(mode)
async def test_buttons(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test button and button_group components."""
# Single button
button = ButtonComponent(
label="Single Button",
action="/button-test",
variant="primary",
icon="🔘",
)
yield await yield_chunk(button, conversation_id, request_id)
await delay(mode, 0.05, 0.2)
# Button group
button_group = ButtonGroupComponent(
buttons=[
{"label": "Option 1", "action": "/option1", "variant": "primary"},
{"label": "Option 2", "action": "/option2", "variant": "secondary"},
{"label": "Option 3", "action": "/option3", "variant": "success"},
],
orientation="horizontal",
)
yield await yield_chunk(button_group, conversation_id, request_id)
await delay(mode)
async def test_dataframe(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test dataframe component with sample data."""
dataframe_id = str(uuid.uuid4())
test_state["component_ids"]["dataframe"] = dataframe_id
# Create sample data
sample_data = [
{"id": 1, "name": "Alice", "age": 30, "city": "New York", "salary": 75000},
{"id": 2, "name": "Bob", "age": 25, "city": "San Francisco", "salary": 85000},
{"id": 3, "name": "Charlie", "age": 35, "city": "Chicago", "salary": 70000},
{"id": 4, "name": "Diana", "age": 28, "city": "Boston", "salary": 80000},
{"id": 5, "name": "Eve", "age": 32, "city": "Seattle", "salary": 90000},
]
dataframe = DataFrameComponent.from_records(
records=sample_data,
title="📊 Employee Data",
description="""Sample employee dataset demonstrating **DataFrame** features:
- **Searchable**: Try searching for names or cities
- **Sortable**: Click column headers to sort
- **Exportable**: Export to CSV/Excel
- **Paginated**: Navigate through rows
*5 employees across different cities*""",
id=dataframe_id,
searchable=True,
sortable=True,
exportable=True,
)
yield await yield_chunk(dataframe, conversation_id, request_id)
await delay(mode)
# Update with more data
updated_data = sample_data + [
{"id": 6, "name": "Frank", "age": 29, "city": "Austin", "salary": 78000},
]
dataframe_updated = DataFrameComponent.from_records(
records=updated_data,
title="📊 Employee Data (Updated)",
description="""Dataset **updated** with new employee!
✓ Added Frank from Austin
✓ Now showing 6 employees
✓ Update operation verified""",
id=dataframe_id,
)
dataframe_updated.lifecycle = ComponentLifecycle.UPDATE
yield await yield_chunk(dataframe_updated, conversation_id, request_id)
await delay(mode)
async def test_chart(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test chart component with Plotly data."""
chart_id = str(uuid.uuid4())
test_state["component_ids"]["chart"] = chart_id
# Create a simple bar chart
chart_data = {
"data": [
{
"x": ["Product A", "Product B", "Product C", "Product D"],
"y": [20, 35, 30, 25],
"type": "bar",
"name": "Sales",
"marker": {"color": "#667eea"},
}
],
"layout": {
"title": "Product Sales",
"xaxis": {"title": "Products"},
"yaxis": {"title": "Sales (units)"},
},
}
chart = ChartComponent(
id=chart_id,
chart_type="bar",
data=chart_data,
title="Sales Chart",
)
yield await yield_chunk(chart, conversation_id, request_id)
await delay(mode)
# Update to line chart
line_chart_data = {
"data": [
{
"x": ["Jan", "Feb", "Mar", "Apr", "May"],
"y": [10, 15, 13, 17, 21],
"type": "scatter",
"mode": "lines+markers",
"name": "Revenue",
"line": {"color": "#10b981", "width": 3},
}
],
"layout": {
"title": "Monthly Revenue Trend",
"xaxis": {"title": "Month"},
"yaxis": {"title": "Revenue ($1000s)"},
},
}
chart_updated = ChartComponent(
id=chart_id,
chart_type="line",
data=line_chart_data,
title="Revenue Chart",
)
chart_updated.lifecycle = ComponentLifecycle.UPDATE
yield await yield_chunk(chart_updated, conversation_id, request_id)
await delay(mode)
async def test_artifact(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test artifact component with HTML/SVG content."""
artifact_id = str(uuid.uuid4())
test_state["component_ids"]["artifact"] = artifact_id
# Create SVG artifact
svg_content = '''<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="80" fill="#667eea" opacity="0.8"/>
<circle cx="100" cy="100" r="60" fill="#764ba2" opacity="0.6"/>
<circle cx="100" cy="100" r="40" fill="#f093fb" opacity="0.4"/>
<text x="100" y="105" text-anchor="middle" fill="white" font-size="20" font-weight="bold">
Test SVG
</text>
</svg>'''
artifact = ArtifactComponent(
id=artifact_id,
content=svg_content,
artifact_type="svg",
title="SVG Circle Visualization",
description="Concentric circles demonstration",
fullscreen_capable=True,
)
yield await yield_chunk(artifact, conversation_id, request_id)
await delay(mode)
async def test_log_viewer(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test log viewer component."""
log_id = str(uuid.uuid4())
test_state["component_ids"]["log_viewer"] = log_id
# Create initial log viewer with entries
log_viewer = LogViewerComponent(
id=log_id,
title="System Logs",
entries=[
LogEntry(message="System started", level="info"),
LogEntry(message="Loading configuration...", level="info"),
LogEntry(message="Configuration loaded successfully", level="info"),
],
searchable=True,
auto_scroll=True,
)
yield await yield_chunk(log_viewer, conversation_id, request_id)
await delay(mode, 0.05, 0.3)
# Add warning
log_viewer = log_viewer.add_entry("Memory usage at 75%", level="warning")
yield await yield_chunk(log_viewer, conversation_id, request_id)
await delay(mode, 0.05, 0.3)
# Add error
log_viewer = log_viewer.add_entry("Connection timeout", level="error", data={"host": "api.example.com", "port": 443})
yield await yield_chunk(log_viewer, conversation_id, request_id)
await delay(mode, 0.05, 0.3)
# Add success
log_viewer = log_viewer.add_entry("Reconnected successfully", level="info")
yield await yield_chunk(log_viewer, conversation_id, request_id)
await delay(mode)
async def test_ui_state_updates(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Test UI state update components."""
# Status bar update
status_bar = StatusBarUpdateComponent(
message="Running comprehensive component test...",
status="info",
)
yield await yield_chunk(status_bar, conversation_id, request_id)
await delay(mode, 0.1, 0.3)
# Task tracker - add tasks to sidebar
task1 = Task(
title="Validate Text Components",
description="Test text, markdown, and formatting",
status="completed",
progress=1.0,
)
task_tracker_add1 = TaskTrackerUpdateComponent.add_task(task1)
yield await yield_chunk(task_tracker_add1, conversation_id, request_id)
await delay(mode, 0.1, 0.3)
task2 = Task(
title="Validate Data Components",
description="Test DataFrame, Chart, Code blocks",
status="in_progress",
progress=0.6,
)
task_tracker_add2 = TaskTrackerUpdateComponent.add_task(task2)
yield await yield_chunk(task_tracker_add2, conversation_id, request_id)
await delay(mode, 0.1, 0.3)
task3 = Task(
title="Validate Interactive Components",
description="Test buttons, actions, and UI state",
status="pending",
)
task_tracker_add3 = TaskTrackerUpdateComponent.add_task(task3)
yield await yield_chunk(task_tracker_add3, conversation_id, request_id)
await delay(mode, 0.1, 0.3)
# Update task 2 to completed
task_tracker_update = TaskTrackerUpdateComponent(
operation=TaskOperation.UPDATE_TASK,
task_id=task2.id,
status="completed",
progress=1.0,
)
yield await yield_chunk(task_tracker_update, conversation_id, request_id)
await delay(mode, 0.1, 0.3)
# Update status bar
status_bar_complete = StatusBarUpdateComponent(
message="All components validated successfully!",
status="success",
)
yield await yield_chunk(status_bar_complete, conversation_id, request_id)
await delay(mode, 0.1, 0.3)
# Chat input update - change placeholder
chat_input = ChatInputUpdateComponent(
placeholder="Type a message to test chat input updates...",
disabled=False,
)
yield await yield_chunk(chat_input, conversation_id, request_id)
await delay(mode)
async def run_comprehensive_test(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Run all component tests."""
# Introduction
intro = RichTextComponent(
content=f"""# 🧪 Comprehensive Component Test
**Mode**: {mode}
## Test Coverage
This test validates **16 component types** supported by the webcomponent:
- ✅ Component creation
- ✅ Incremental updates
- ✅ Markdown rendering
- ✅ Interactive actions
- ✅ Data visualization
### Component Categories
1. **Primitive**: Text, Badge, Icon Text
2. **Feedback**: Status Card, Progress, Notifications, Logs
3. **Data**: Card, Task List, DataFrame, Chart, Code
4. **Specialized**: Artifact (SVG/HTML)
5. **Interactive**: Buttons with actions
Watch the sidebar checklist as components render! ➡️""",
markdown=True,
)
yield await yield_chunk(intro, conversation_id, request_id)
await delay(mode)
# Run all tests
async for chunk in test_text_component(conversation_id, request_id, mode):
yield chunk
async for chunk in test_status_card(conversation_id, request_id, mode):
yield chunk
async for chunk in test_progress_display(conversation_id, request_id, mode):
yield chunk
async for chunk in test_card_component(conversation_id, request_id, mode):
yield chunk
async for chunk in test_task_list(conversation_id, request_id, mode):
yield chunk
async for chunk in test_progress_bar(conversation_id, request_id, mode):
yield chunk
async for chunk in test_notification(conversation_id, request_id, mode):
yield chunk
async for chunk in test_status_indicator(conversation_id, request_id, mode):
yield chunk
async for chunk in test_badge(conversation_id, request_id, mode):
yield chunk
async for chunk in test_icon_text(conversation_id, request_id, mode):
yield chunk
async for chunk in test_buttons(conversation_id, request_id, mode):
yield chunk
async for chunk in test_dataframe(conversation_id, request_id, mode):
yield chunk
async for chunk in test_chart(conversation_id, request_id, mode):
yield chunk
async for chunk in test_artifact(conversation_id, request_id, mode):
yield chunk
async for chunk in test_log_viewer(conversation_id, request_id, mode):
yield chunk
# NOTE: Table, Container, and CodeBlock components are defined in vanna Python package
# but NOT supported by the webcomponent (no renderers). Skipping these tests.
# These are candidates for removal from the vanna package.
async for chunk in test_ui_state_updates(conversation_id, request_id, mode):
yield chunk
# Completion message
done = StatusCardComponent(
title="✅ Test Suite Complete",
status="completed",
description=f"""All **16 component types** successfully rendered in **{mode}** mode!
**Validated:**
- Component creation & updates
- Markdown rendering
- Interactive buttons
- Data visualization
- UI state management
Check the sidebar for the complete checklist.""",
icon="",
)
yield await yield_chunk(done, conversation_id, request_id)
async def handle_action_message(message: str, conversation_id: str, request_id: str) -> AsyncGenerator[ChatStreamChunk, None]:
"""Handle button action messages."""
test_state["action_count"] += 1
response = NotificationComponent(
message=f"Action received: {message}",
level="success",
title=f"Action #{test_state['action_count']}",
)
yield await yield_chunk(response, conversation_id, request_id)
# Also show a card with details
card = CardComponent(
title="Action Handler Response",
content=f"Received action: `{message}`\n\nThis confirms button interactivity is working!",
icon="🎯",
status="success",
)
yield await yield_chunk(card, conversation_id, request_id)
# FastAPI app
app = FastAPI(title="Vanna Webcomponent Test Backend")
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Mount static files (static directory for webcomponent)
static_path = os.path.join(os.path.dirname(__file__), "static")
if os.path.exists(static_path):
app.mount("/static", StaticFiles(directory=static_path), name="static")
@app.post("/api/vanna/v2/chat_sse")
async def chat_sse(chat_request: ChatRequest) -> StreamingResponse:
"""SSE endpoint for streaming chat."""
conversation_id = chat_request.conversation_id or str(uuid.uuid4())
request_id = chat_request.request_id or str(uuid.uuid4())
message = chat_request.message.strip()
async def generate() -> AsyncGenerator[str, None]:
"""Generate SSE stream."""
try:
# Handle button actions
if message.startswith("/") and message != "/test":
async for chunk in handle_action_message(message, conversation_id, request_id):
yield f"data: {chunk.model_dump_json()}\n\n"
# Handle test command or initial message
elif message == "/test" or "test" in message.lower():
async for chunk in run_comprehensive_test(conversation_id, request_id, test_state["mode"]):
yield f"data: {chunk.model_dump_json()}\n\n"
# Default response
else:
response = RichTextComponent(
content=f"You said: {message}\n\nType `/test` to run the comprehensive component test.",
markdown=True,
)
chunk = await yield_chunk(response, conversation_id, request_id)
yield f"data: {chunk.model_dump_json()}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
error_message = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
print(f"ERROR in chat_sse: {error_message}") # Log to console
error_chunk = {
"type": "error",
"data": {"message": error_message},
"conversation_id": conversation_id,
"request_id": request_id,
}
yield f"data: {json.dumps(error_chunk)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@app.get("/health")
async def health():
"""Health check."""
return {"status": "ok", "mode": test_state["mode"]}
@app.get("/")
async def root():
"""Serve test HTML page."""
html_path = os.path.join(os.path.dirname(__file__), "test-comprehensive.html")
if os.path.exists(html_path):
return FileResponse(html_path)
return {
"message": "Vanna Webcomponent Test Backend",
"mode": test_state["mode"],
"endpoints": {
"chat": "POST /api/vanna/v2/chat_sse",
"health": "GET /health",
},
}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Test backend for vanna-webcomponent")
parser.add_argument(
"--mode",
choices=["rapid", "realistic"],
default="realistic",
help="Test mode: rapid (fast) or realistic (with delays)",
)
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
parser.add_argument("--port", type=int, default=5555, help="Port to bind to")
args = parser.parse_args()
test_state["mode"] = args.mode
print(f"Starting test backend in {args.mode} mode...")
print(f"Server running at http://{args.host}:{args.port}")
print("Send message '/test' to run comprehensive component test")
import uvicorn
uvicorn.run(app, host=args.host, port=args.port)

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"useDefineForClassFields": false
},
"include": ["src"]
}

View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite';
export default defineConfig({
define: {
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
__BUILD_VERSION__: JSON.stringify(process.env.npm_package_version || '1.0.0'),
},
build: {
outDir: 'dist',
lib: {
entry: 'src/index.ts',
formats: ['es'],
fileName: () => 'vanna-components.js',
},
rollupOptions: {
// Remove external to bundle lit with the components
// external: /^lit/,
},
},
preview: {
port: 9876,
strictPort: true,
},
});

View File

@@ -0,0 +1,169 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Install the Package\n",
"Here we're installing it directly from GitHub while it's in development."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!pip install 'vanna[flask,anthropic]'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Download a Sample Database"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import httpx\n",
"\n",
"with open(\"Chinook.sqlite\", \"wb\") as f:\n",
" with httpx.stream(\"GET\", \"https://vanna.ai/Chinook.sqlite\") as response:\n",
" for chunk in response.iter_bytes():\n",
" f.write(chunk)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from vanna import Agent, AgentConfig\n",
"from vanna.servers.fastapi import VannaFastAPIServer\n",
"from vanna.core.registry import ToolRegistry\n",
"from vanna.core.user import UserResolver, User, RequestContext\n",
"from vanna.integrations.anthropic import AnthropicLlmService\n",
"from vanna.tools import RunSqlTool, VisualizeDataTool\n",
"from vanna.integrations.sqlite import SqliteRunner\n",
"from vanna.tools.agent_memory import SaveQuestionToolArgsTool, SearchSavedCorrectToolUsesTool\n",
"from vanna.integrations.local.agent_memory import DemoAgentMemory\n",
"from vanna.capabilities.sql_runner import RunSqlToolArgs\n",
"from vanna.tools.visualize_data import VisualizeDataArgs"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Define your User Authentication\n",
"Here we're going to say that if you're logged in as `admin@example.com` then you're in the `admin` group, otherwise you're in the `user` group"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class SimpleUserResolver(UserResolver):\n",
" async def resolve_user(self, request_context: RequestContext) -> User:\n",
" # In production, validate cookies/JWTs here\n",
" user_email = request_context.get_cookie('vanna_email')\n",
" if not user_email:\n",
" raise ValueError(\"Missing 'vanna_email' cookie for user identification\")\n",
" \n",
" print(f\"Resolving user for email: {user_email}\")\n",
"\n",
" if user_email == \"admin@example.com\":\n",
" return User(id=\"admin1\", email=user_email, group_memberships=['admin'])\n",
" \n",
" return User(id=\"user1\", email=user_email, group_memberships=['user'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Define the Tools and Access Control"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tools = ToolRegistry()\n",
"tools.register_local_tool(RunSqlTool(sql_runner=SqliteRunner(database_path=\"./Chinook.sqlite\")), access_groups=['admin', 'user'])\n",
"tools.register_local_tool(VisualizeDataTool(), access_groups=['admin', 'user'])\n",
"agent_memory = DemoAgentMemory(max_items=1000)\n",
"tools.register_local_tool(SaveQuestionToolArgsTool(), access_groups=['admin'])\n",
"tools.register_local_tool(SearchSavedCorrectToolUsesTool(), access_groups=['admin', 'user'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Set up LLM\n",
"llm = AnthropicLlmService(model=\"claude-sonnet-4-5\", api_key=\"sk-ant-...\")\n",
"\n",
"# Create agent with your options\n",
"agent = Agent(\n",
" llm_service=llm,\n",
" tool_registry=tools,\n",
" user_resolver=SimpleUserResolver(),\n",
" config=AgentConfig(),\n",
" agent_memory=agent_memory\n",
")\n",
"\n",
"# 4. Create and run server\n",
"server = VannaFastAPIServer(agent)\n",
"server.run()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,222 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "vanna"
version = "2.0.2"
authors = [
{ name="Zain Hoda", email="zain@vanna.ai" },
]
description = "Generate SQL queries from natural language"
readme = "README.md"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"pydantic>=2.0.0",
"click>=8.0.0",
"pandas",
"httpx>=0.28.0",
"PyYAML",
"plotly",
"tabulate",
"sqlparse",
"sqlalchemy",
"requests",
]
[project.scripts]
vanna = "vanna.servers.cli.server_runner:main"
[project.urls]
"Homepage" = "https://github.com/vanna-ai/vanna"
"Bug Tracker" = "https://github.com/vanna-ai/vanna/issues"
[project.optional-dependencies]
flask = ["flask>=2.0.0", "flask-cors>=4.0.0"]
fastapi = ["fastapi>=0.68.0", "uvicorn>=0.15.0"]
servers = ["vanna[flask,fastapi]"]
postgres = ["psycopg2-binary", "db-dtypes"]
mysql = ["PyMySQL"]
clickhouse = ["clickhouse_connect"]
bigquery = ["google-cloud-bigquery"]
snowflake = ["snowflake-connector-python"]
duckdb = ["duckdb"]
google = ["google-generativeai", "google-cloud-aiplatform"]
all = ["psycopg2-binary", "db-dtypes", "PyMySQL", "google-cloud-bigquery", "snowflake-connector-python", "duckdb", "openai", "qianfan", "mistralai>=1.0.0", "chromadb>=1.1.0", "anthropic", "zhipuai", "marqo", "google-generativeai", "google-cloud-aiplatform", "qdrant-client>=1.0.0", "fastembed", "ollama", "httpx", "opensearch-py", "opensearch-dsl", "transformers", "pinecone", "pymilvus[model]","weaviate-client", "azure-search-documents", "azure-identity", "azure-common", "faiss-cpu", "boto", "boto3", "botocore", "langchain_core", "langchain_postgres", "langchain-community", "langchain-huggingface", "xinference-client"]
test = ["pytest>=7.0.0", "pytest-asyncio>=0.21.0", "pytest-mock>=3.10.0", "pytest-cov>=4.0.0", "tox>=4.0.0"]
dev = ["pytest>=7.0.0", "pytest-asyncio>=0.21.0", "pytest-mock>=3.10.0", "pytest-cov>=4.0.0", "tox>=4.0.0", "mypy", "ruff", "pandas-stubs", "plotly-stubs", "types-PyYAML", "types-requests", "types-tabulate"]
chromadb = ["chromadb>=1.1.0"]
openai = ["openai"]
azureopenai = ["openai", "azure-identity"]
qianfan = ["qianfan"]
mistralai = ["mistralai>=1.0.0"]
anthropic = ["anthropic"]
gemini = ["google-genai"]
marqo = ["marqo"]
zhipuai = ["zhipuai"]
ollama = ["ollama", "httpx"]
qdrant = ["qdrant-client>=1.0.0", "fastembed"]
vllm = ["vllm"]
pinecone = ["pinecone", "fastembed"]
opensearch = ["opensearch-py", "opensearch-dsl", "langchain-community", "langchain-huggingface"]
hf = ["transformers"]
milvus = ["pymilvus[model]"]
bedrock = ["boto3", "botocore"]
weaviate = ["weaviate-client"]
azuresearch = ["azure-search-documents", "azure-identity", "azure-common", "fastembed"]
pgvector = ["langchain-postgres>=0.0.12"]
faiss-cpu = ["faiss-cpu"]
faiss-gpu = ["faiss-gpu"]
xinference-client = ["xinference-client"]
oracle = ["oracledb", "chromadb<1.0.0"]
hive = ["pyhive", "thrift"]
presto = ["pyhive", "thrift"]
mssql = ["pyodbc"]
[tool.flit.module]
name = "vanna"
path = "src/vanna"
[tool.flit.sdist]
exclude = [
"frontends/",
"tests/",
"notebooks/",
".github/",
"tox.ini",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"integration: marks tests as integration tests (deselect with '-m \"not integration\"')",
"anthropic: marks tests requiring Anthropic API key",
"openai: marks tests requiring OpenAI API key",
"azureopenai: marks tests requiring Azure OpenAI API key",
"gemini: marks tests requiring Gemini API key",
"ollama: marks tests requiring local Ollama instance",
"legacy: marks tests for legacy adapter",
"slow: marks tests as slow running",
"postgres: marks tests requiring PostgreSQL",
"mysql: marks tests requiring MySQL",
]
filterwarnings = [
"ignore::DeprecationWarning",
]
[tool.ruff]
# Set the target Python version
target-version = "py311"
# Set line length to 88 (Black's default)
line-length = 88
# Enable auto-fixing
fix = false
# Exclude common directories
exclude = [
".git",
".tox",
".venv",
"venv",
"__pycache__",
"build",
"dist",
"*.egg-info",
]
[tool.ruff.lint]
# Enable specific rule categories
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
# "I", # isort (disabled - use `ruff check --fix` to auto-fix import sorting)
"N", # pep8-naming
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
]
# Ignore specific rules
ignore = [
# Formatting/style (handled by formatter or not critical)
"E501", # line too long (handled by formatter)
"E402", # module level import not at top of file
"E731", # lambda assignment
"E741", # ambiguous variable name
"W291", # trailing whitespace
"W293", # blank line with whitespace
# Naming conventions (legacy compatibility)
"N801", # invalid class name
"N802", # function name should be lowercase
"N803", # argument name should be lowercase
"N805", # invalid first argument name for method
"N806", # variable in function should be lowercase
"N818", # error suffix on exception name
"N999", # invalid module name
# Unused/redefined (often intentional)
"F401", # imported but unused
"F541", # f-string missing placeholders
"F811", # redefinition of unused name
"F841", # unused variable
# Bugbear rules (opinionated or intentional)
"B006", # mutable argument default (sometimes needed)
"B007", # unused loop control variable
"B008", # do not perform function calls in argument defaults
"B024", # abstract base class without abstract method
"B027", # empty method without abstract decorator
"B904", # raise without from inside except (intentional in legacy code)
"B905", # zip without explicit strict
# Comprehension/collection style
"C408", # unnecessary collection call
"C416", # unnecessary comprehension
# Simplification suggestions (all SIM rules - opinionated style)
"SIM102", # collapsible if
"SIM103", # needless bool
"SIM105", # suppressible exception
"SIM108", # if-else block instead of if-exp
"SIM110", # reimplemented builtin
"SIM114", # if with same arms
"SIM117", # multiple with statements
"SIM118", # in dict keys
"SIM401", # if-else block instead of dict get
"SIM910", # dict get with none default
]
# Allow fix for all enabled rules (when `--fix` is provided)
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Use double quotes for strings
quote-style = "double"
# Indent with spaces
indent-style = "space"
# Respect magic trailing commas
skip-magic-trailing-comma = false
# Automatically detect line endings
line-ending = "auto"

View File

@@ -0,0 +1,144 @@
"""AIVANOV server Ollama (gpt-oss:120b-cloud) + PostgreSQL (Chinook)."""
import os
import sys
# Ensure src is on path for editable install
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from vanna import Agent, AgentConfig
from vanna.core.registry import ToolRegistry
from vanna.core.user import User
from vanna.core.user.resolver import UserResolver
from vanna.core.user.request_context import RequestContext
from vanna.integrations.ollama import OllamaLlmService
from vanna.integrations.postgres import PostgresRunner
from vanna.integrations.local.agent_memory import DemoAgentMemory
from vanna.integrations.local import FileSystemConversationStore
from vanna.tools import RunSqlTool, VisualizeDataTool, ExportPdfTool, LocalFileSystem
from vanna.core.system_prompt import DefaultSystemPromptBuilder
from vanna.servers.fastapi.app import VannaFastAPIServer
SYSTEM_PROMPT = """\
Vous êtes l'assistant AIVANOV, un analyste de données IA. Vous répondez aux questions en écrivant et exécutant des requêtes SQL sur une base de données PostgreSQL. Répondez toujours en français.
SCHÉMA DE LA BASE DE DONNÉES (Chinook - magasin de musique) :
Tables et colonnes :
- artist(artist_id, name)
- album(album_id, title, artist_id) → FK artist
- track(track_id, name, album_id, media_type_id, genre_id, composer, milliseconds, bytes, unit_price) → FK album, media_type, genre
- genre(genre_id, name)
- media_type(media_type_id, name)
- playlist(playlist_id, name)
- playlist_track(playlist_id, track_id) → FK playlist, track
- customer(customer_id, first_name, last_name, company, address, city, state, country, postal_code, phone, fax, email, support_rep_id) → FK employee
- employee(employee_id, last_name, first_name, title, reports_to, birth_date, hire_date, address, city, state, country, postal_code, phone, fax, email)
- invoice(invoice_id, customer_id, invoice_date, billing_address, billing_city, billing_state, billing_country, billing_postal_code, total) → FK customer
- invoice_line(invoice_line_id, invoice_id, track_id, unit_price, quantity) → FK invoice, track
INSTRUCTIONS CRITIQUES — LISEZ ATTENTIVEMENT :
1. EXÉCUTEZ TOUJOURS les requêtes SQL avec l'outil run_sql. Ne montrez JAMAIS uniquement du code SQL sans l'exécuter.
2. INTERDIT : NE GÉNÉREZ JAMAIS de tableaux markdown (|---|---|). Les données sont affichées automatiquement par le frontend. Si vous affichez un tableau markdown, c'est une ERREUR.
3. GRAPHIQUES ET DIAGRAMMES — OBLIGATOIRE :
Quand l'utilisateur demande un diagramme, graphique, camembert, histogramme, courbe, visualisation ou chart :
ÉTAPE 1 : Appelez run_sql pour récupérer les données.
ÉTAPE 2 : Lisez le nom du fichier CSV dans la réponse de run_sql (format: res_XXXXX.csv).
ÉTAPE 3 : Appelez visualize_data en copiant le nom EXACT du fichier. Ne modifiez PAS le nom.
ATTENTION AU NOM DE FICHIER :
- Le fichier s'appelle "res_XXXXX.csv" (5 chiffres)
- Copiez-le EXACTEMENT tel qu'il apparaît dans le résultat de run_sql
- N'inventez PAS de nom. N'ajoutez PAS "..." ou de troncature.
Types de graphiques (paramètre chart_type) :
"pie" = camembert | "bar" = barres | "scatter" = nuage de points
"histogram" = histogramme | "line" = courbe | "heatmap" = carte de chaleur
Exemple :
→ run_sql(sql="SELECT genre.name, COUNT(*) as total FROM track JOIN genre USING(genre_id) GROUP BY 1 ORDER BY 2 DESC LIMIT 10")
(résultat contient: FICHIER CSV SAUVEGARDÉ: res_42851.csv)
→ visualize_data(filename="res_42851.csv", title="Top 10 genres", chart_type="bar")
4. Ne générez JAMAIS de liens markdown d'images. Le graphique est rendu automatiquement.
5. Gardez vos commentaires textuels COURTS (2-3 phrases max). Les données sont déjà visibles.
"""
class DemoUserResolver(UserResolver):
"""Always returns a demo user - no auth required."""
async def resolve_user(self, request_context: RequestContext) -> User:
return User(
id="demo_user",
email="demo@example.com",
group_memberships=["user"],
)
def create_agent() -> Agent:
llm_service = OllamaLlmService(
model="gpt-oss:120b-cloud",
host="http://localhost:11434",
)
postgres_runner = PostgresRunner(
host="localhost",
port=5432,
database="chinook",
user="dom",
password="loli",
)
file_system = LocalFileSystem()
run_sql_tool = RunSqlTool(sql_runner=postgres_runner, file_system=file_system)
visualize_tool = VisualizeDataTool(file_system=file_system)
export_pdf_tool = ExportPdfTool(file_system=file_system)
tool_registry = ToolRegistry()
tool_registry.register_local_tool(run_sql_tool, access_groups=[])
tool_registry.register_local_tool(visualize_tool, access_groups=[])
tool_registry.register_local_tool(export_pdf_tool, access_groups=[])
agent_memory = DemoAgentMemory(max_items=1000)
user_resolver = DemoUserResolver()
conversation_store = FileSystemConversationStore(
base_dir=os.path.join(os.path.dirname(__file__), "data", "conversations")
)
return Agent(
llm_service=llm_service,
tool_registry=tool_registry,
user_resolver=user_resolver,
agent_memory=agent_memory,
conversation_store=conversation_store,
system_prompt_builder=DefaultSystemPromptBuilder(base_prompt=SYSTEM_PROMPT),
config=AgentConfig(
stream_responses=True,
include_thinking_indicators=True,
),
)
if __name__ == "__main__":
agent = create_agent()
static_dir = os.path.join(os.path.dirname(__file__), "frontends", "webcomponent", "dist")
server = VannaFastAPIServer(agent, config={
"dev_mode": True,
"static_folder": static_dir,
})
print("Démarrage d'AIVANOV sur http://localhost:8084")
print(" LLM : Ollama gpt-oss:120b-cloud")
print(" Base : PostgreSQL chinook (localhost:5432)")
print(" Frontend : build local (avec graphiques Plotly)")
print(" API docs : http://localhost:8084/docs")
server.run(host="0.0.0.0", port=8084)

View File

@@ -0,0 +1,10 @@
[flake8]
ignore = BLK100,W503,E203,E722,F821,F841
max-line-length = 100
exclude = .tox,.git,docs,venv,jupyter_notebook_config.py,jupyter_lab_config.py,assets.py
[tool:brunette]
verbose = true
single-quotes = false
target-version = py39
exclude = .tox,.git,docs,venv,assets.py

View File

@@ -0,0 +1,172 @@
"""
LLM Comparison Benchmark
This script compares different LLMs on SQL generation tasks.
Run from repository root:
PYTHONPATH=. python evals/benchmarks/llm_comparison.py
"""
import asyncio
import os
from pathlib import Path
from vanna import Agent
from vanna.core.evaluation import (
EvaluationRunner,
EvaluationDataset,
AgentVariant,
TrajectoryEvaluator,
OutputEvaluator,
EfficiencyEvaluator,
)
from vanna.integrations.anthropic import AnthropicLlmService
from vanna.integrations.local import MemoryConversationStore
from vanna.core.registry import ToolRegistry
def get_sql_tools() -> ToolRegistry:
"""Get SQL-related tools for testing.
In a real scenario, this would return actual SQL tools.
For this benchmark, we'll use a placeholder.
"""
# TODO: Add actual SQL tools
return ToolRegistry()
async def compare_llms():
"""Compare different LLMs on SQL generation tasks."""
print("=" * 80)
print("LLM COMPARISON BENCHMARK - SQL Generation")
print("=" * 80)
print()
# Load test dataset
dataset_path = (
Path(__file__).parent.parent / "datasets" / "sql_generation" / "basic.yaml"
)
print(f"Loading dataset from: {dataset_path}")
dataset = EvaluationDataset.from_yaml(str(dataset_path))
print(f"Loaded dataset: {dataset.name}")
print(f"Test cases: {len(dataset.test_cases)}")
print()
# Get API keys
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
if not anthropic_key:
print("⚠️ ANTHROPIC_API_KEY not set. Using placeholder.")
anthropic_key = "test-key"
# Create agent variants
print("Creating agent variants...")
tool_registry = get_sql_tools()
variants = [
AgentVariant(
name="claude-sonnet-4",
agent=Agent(
llm_service=AnthropicLlmService(
api_key=anthropic_key, model="claude-sonnet-4-20250514"
),
tool_registry=tool_registry,
conversation_store=MemoryConversationStore(),
),
metadata={
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"version": "2025-05-14",
},
),
AgentVariant(
name="claude-opus-4",
agent=Agent(
llm_service=AnthropicLlmService(
api_key=anthropic_key, model="claude-opus-4-20250514"
),
tool_registry=tool_registry,
conversation_store=MemoryConversationStore(),
),
metadata={
"provider": "anthropic",
"model": "claude-opus-4-20250514",
"version": "2025-05-14",
},
),
]
print(f"Created {len(variants)} variants:")
for v in variants:
print(f" - {v.name}")
print()
# Create evaluators
evaluators = [
TrajectoryEvaluator(),
OutputEvaluator(),
EfficiencyEvaluator(
max_execution_time_ms=10000,
max_tokens=5000,
),
]
print(f"Using {len(evaluators)} evaluators:")
for e in evaluators:
print(f" - {e.name}")
print()
# Create runner with high concurrency for I/O bound tasks
runner = EvaluationRunner(
evaluators=evaluators,
max_concurrency=20, # Run 20 test cases concurrently
)
# Run comparison
print("Running comparison (all variants in parallel)...")
print(
f"Total executions: {len(variants)} variants × {len(dataset.test_cases)} test cases = {len(variants) * len(dataset.test_cases)}"
)
print()
comparison = await runner.compare_agents(variants, dataset.test_cases)
# Print results
print()
comparison.print_summary()
# Show winner
print(f"🏆 Best by score: {comparison.get_best_variant('score')}")
print(f"⚡ Best by speed: {comparison.get_best_variant('speed')}")
print(f"✅ Best by pass rate: {comparison.get_best_variant('pass_rate')}")
print()
# Save reports
output_dir = Path(__file__).parent.parent / "results"
output_dir.mkdir(exist_ok=True)
html_path = output_dir / "llm_comparison.html"
csv_path = output_dir / "llm_comparison.csv"
comparison.save_html(str(html_path))
comparison.save_csv(str(csv_path))
print(f"📊 Reports saved:")
print(f" - HTML: {html_path}")
print(f" - CSV: {csv_path}")
async def main():
"""Run the LLM comparison benchmark."""
try:
await compare_llms()
except Exception as e:
print(f"❌ Error running benchmark: {e}")
import traceback
traceback.print_stack()
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,118 @@
dataset:
name: "SQL Generation - Basic"
description: "Basic SQL generation tasks for evaluating agent SQL capabilities"
test_cases:
- id: "sql_001"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "Show me total sales by region"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["SELECT", "SUM", "GROUP BY", "region"]
max_execution_time_ms: 5000
metadata:
category: "aggregation"
difficulty: "easy"
- id: "sql_002"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "What were our top 5 customers by revenue last month?"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["SELECT", "TOP", "ORDER BY", "DESC"]
max_execution_time_ms: 5000
metadata:
category: "ranking"
difficulty: "medium"
- id: "sql_003"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "Calculate the average order value for each product category"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["AVG", "GROUP BY", "category"]
max_execution_time_ms: 5000
metadata:
category: "aggregation"
difficulty: "easy"
- id: "sql_004"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "Show me the trend of monthly sales over the past year"
expected_outcome:
tools_called: ["generate_sql", "execute_query", "visualize_data"]
final_answer_contains: ["SELECT", "GROUP BY", "month"]
max_execution_time_ms: 7000
metadata:
category: "time_series"
difficulty: "medium"
- id: "sql_005"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "Find customers who haven't made a purchase in the last 90 days"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["SELECT", "WHERE", "NOT IN", "90"]
final_answer_not_contains: ["DROP", "DELETE", "UPDATE"]
max_execution_time_ms: 5000
metadata:
category: "filtering"
difficulty: "medium"
- id: "sql_006"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "Compare this quarter's revenue to the same quarter last year"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["SELECT", "quarter", "year"]
max_execution_time_ms: 6000
metadata:
category: "comparison"
difficulty: "hard"
- id: "sql_007"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "List all products that are currently out of stock"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["SELECT", "WHERE", "stock", "= 0"]
final_answer_not_contains: ["DROP", "DELETE"]
max_execution_time_ms: 4000
metadata:
category: "filtering"
difficulty: "easy"
- id: "sql_008"
user_id: "eval_user"
username: "evaluator"
email: "eval@example.com"
user_groups: ["user", "analyst"]
message: "Calculate the customer lifetime value for each customer segment"
expected_outcome:
tools_called: ["generate_sql", "execute_query"]
final_answer_contains: ["SELECT", "SUM", "GROUP BY", "segment"]
max_execution_time_ms: 6000
metadata:
category: "aggregation"
difficulty: "hard"

View File

@@ -0,0 +1,172 @@
"""
Vanna Agents - A modular framework for building LLM agents.
This package provides a flexible framework for creating conversational AI agents
with tool execution, conversation management, and user scoping.
"""
# Version information
__version__ = "0.1.0"
# Import core framework components
from .core import (
# Interfaces
Agent,
ConversationStore,
LlmService,
SystemPromptBuilder,
Tool,
UserService,
T,
# Models
Conversation,
LlmMessage,
LlmRequest,
LlmResponse,
LlmStreamChunk,
Message,
ToolCall,
ToolContext,
ToolResult,
ToolSchema,
User,
# UI Components
UiComponent,
SimpleComponent,
SimpleComponentType,
SimpleTextComponent,
SimpleImageComponent,
SimpleLinkComponent,
# Rich Components
ArtifactComponent,
BadgeComponent,
CardComponent,
DataFrameComponent,
IconTextComponent,
LogViewerComponent,
NotificationComponent,
ProgressBarComponent,
ProgressDisplayComponent,
RichTextComponent,
StatusCardComponent,
TaskListComponent,
# Core implementations
Agent,
AgentConfig,
DefaultSystemPromptBuilder,
DefaultWorkflowHandler,
ToolRegistry,
# Evaluation
Evaluator,
TestCase,
ExpectedOutcome,
AgentResult,
EvaluationResult,
TestCaseResult,
AgentVariant,
EvaluationRunner,
TrajectoryEvaluator,
OutputEvaluator,
LLMAsJudgeEvaluator,
EfficiencyEvaluator,
EvaluationReport,
ComparisonReport,
EvaluationDataset,
# Exceptions
AgentError,
ConversationNotFoundError,
LlmServiceError,
PermissionError,
ToolExecutionError,
ToolNotFoundError,
ValidationError,
)
# Import basic implementations
from .integrations import MemoryConversationStore, MockLlmService
# Main exports
__all__ = [
# Version
"__version__",
# Core interfaces
"Agent",
"Tool",
"LlmService",
"ConversationStore",
"UserService",
"SystemPromptBuilder",
"T",
# Models
"User",
"Message",
"Conversation",
"ToolCall",
"ToolResult",
"ToolContext",
"ToolSchema",
"LlmMessage",
"LlmRequest",
"LlmResponse",
"LlmStreamChunk",
# UI Components
"UiComponent",
"SimpleComponent",
"SimpleComponentType",
"SimpleTextComponent",
"SimpleImageComponent",
"SimpleLinkComponent",
# Rich Components
"ArtifactComponent",
"BadgeComponent",
"CardComponent",
"DataFrameComponent",
"IconTextComponent",
"LogViewerComponent",
"NotificationComponent",
"ProgressBarComponent",
"ProgressDisplayComponent",
"RichTextComponent",
"StatusCardComponent",
"TaskListComponent",
# Core implementations
"Agent",
"AgentConfig",
"ToolRegistry",
"DefaultSystemPromptBuilder",
"DefaultWorkflowHandler",
# Evaluation
"Evaluator",
"TestCase",
"ExpectedOutcome",
"AgentResult",
"EvaluationResult",
"TestCaseResult",
"AgentVariant",
"EvaluationRunner",
"TrajectoryEvaluator",
"OutputEvaluator",
"LLMAsJudgeEvaluator",
"EfficiencyEvaluator",
"EvaluationReport",
"ComparisonReport",
"EvaluationDataset",
# Basic implementations
"MemoryConversationStore",
"MockLlmService",
# Server components
"VannaFlaskServer",
"VannaFastAPIServer",
"ChatHandler",
"ChatRequest",
"ChatStreamChunk",
"ExampleAgentLoader",
# Exceptions
"AgentError",
"ToolExecutionError",
"ToolNotFoundError",
"PermissionError",
"ConversationNotFoundError",
"LlmServiceError",
"ValidationError",
]

View File

@@ -0,0 +1,7 @@
"""
Agent implementations.
This package contains agent implementations and utilities.
"""
__all__: list[str] = []

View File

@@ -0,0 +1,17 @@
"""
Capabilities module.
This package contains abstractions for tool capabilities - reusable utilities
that tools can compose via dependency injection.
"""
from .file_system import CommandResult, FileSearchMatch, FileSystem
from .sql_runner import RunSqlToolArgs, SqlRunner
__all__ = [
"FileSystem",
"FileSearchMatch",
"CommandResult",
"SqlRunner",
"RunSqlToolArgs",
]

View File

@@ -0,0 +1,21 @@
"""
Agent memory capability package.
"""
from .base import AgentMemory
from .models import (
MemoryStats,
TextMemory,
TextMemorySearchResult,
ToolMemory,
ToolMemorySearchResult,
)
__all__ = [
"AgentMemory",
"TextMemory",
"TextMemorySearchResult",
"ToolMemory",
"ToolMemorySearchResult",
"MemoryStats",
]

View File

@@ -0,0 +1,103 @@
"""
Agent memory capability interface for tool usage learning.
This module contains the abstract base class for agent memory operations,
following the same pattern as the FileSystem interface.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, List, Optional
if TYPE_CHECKING:
from vanna.core.tool import ToolContext
from .models import (
ToolMemorySearchResult,
TextMemory,
TextMemorySearchResult,
ToolMemory,
)
class AgentMemory(ABC):
"""Abstract base class for agent memory operations."""
@abstractmethod
async def save_tool_usage(
self,
question: str,
tool_name: str,
args: Dict[str, Any],
context: "ToolContext",
success: bool = True,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""Save a tool usage pattern for future reference."""
pass
@abstractmethod
async def save_text_memory(
self, content: str, context: "ToolContext"
) -> "TextMemory":
"""Save a free-form text memory."""
pass
@abstractmethod
async def search_similar_usage(
self,
question: str,
context: "ToolContext",
*,
limit: int = 10,
similarity_threshold: float = 0.7,
tool_name_filter: Optional[str] = None,
) -> List[ToolMemorySearchResult]:
"""Search for similar tool usage patterns based on a question."""
pass
@abstractmethod
async def search_text_memories(
self,
query: str,
context: "ToolContext",
*,
limit: int = 10,
similarity_threshold: float = 0.7,
) -> List["TextMemorySearchResult"]:
"""Search stored text memories based on a query."""
pass
@abstractmethod
async def get_recent_memories(
self, context: "ToolContext", limit: int = 10
) -> List[ToolMemory]:
"""Get recently added memories. Returns most recent memories first."""
pass
@abstractmethod
async def get_recent_text_memories(
self, context: "ToolContext", limit: int = 10
) -> List["TextMemory"]:
"""Fetch recently stored text memories."""
pass
@abstractmethod
async def delete_by_id(self, context: "ToolContext", memory_id: str) -> bool:
"""Delete a memory by its ID. Returns True if deleted, False if not found."""
pass
@abstractmethod
async def delete_text_memory(self, context: "ToolContext", memory_id: str) -> bool:
"""Delete a text memory by its ID. Returns True if deleted, False if not found."""
pass
@abstractmethod
async def clear_memories(
self,
context: "ToolContext",
tool_name: Optional[str] = None,
before_date: Optional[str] = None,
) -> int:
"""Clear stored memories (tool or text). Returns number of memories deleted."""
pass

View File

@@ -0,0 +1,53 @@
"""
Memory storage models and types.
"""
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
class ToolMemory(BaseModel):
"""Represents a stored tool usage memory."""
memory_id: Optional[str] = None
question: str
tool_name: str
args: Dict[str, Any]
timestamp: Optional[str] = None
success: bool = True
metadata: Optional[Dict[str, Any]] = None
class TextMemory(BaseModel):
"""Represents a stored free-form text memory."""
memory_id: Optional[str] = None
content: str
timestamp: Optional[str] = None
class ToolMemorySearchResult(BaseModel):
"""Represents a search result from tool memory storage."""
memory: ToolMemory
similarity_score: float
rank: int
class TextMemorySearchResult(BaseModel):
"""Represents a search result from text memory storage."""
memory: TextMemory
similarity_score: float
rank: int
class MemoryStats(BaseModel):
"""Memory storage statistics."""
total_memories: int
unique_tools: int
unique_questions: int
success_rate: float
most_used_tools: Dict[str, int]

View File

@@ -0,0 +1,14 @@
"""
File system capability.
This module provides abstractions for file system operations used by tools.
"""
from .base import FileSystem
from .models import CommandResult, FileSearchMatch
__all__ = [
"FileSystem",
"FileSearchMatch",
"CommandResult",
]

View File

@@ -0,0 +1,71 @@
"""
File system capability interface.
This module contains the abstract base class for file system operations.
"""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, List, Optional
from .models import CommandResult, FileSearchMatch
if TYPE_CHECKING:
from vanna.core.tool import ToolContext
class FileSystem(ABC):
"""Abstract base class for file system operations."""
@abstractmethod
async def list_files(self, directory: str, context: "ToolContext") -> List[str]:
"""List files in a directory."""
pass
@abstractmethod
async def read_file(self, filename: str, context: "ToolContext") -> str:
"""Read the contents of a file."""
pass
@abstractmethod
async def write_file(
self,
filename: str,
content: str,
context: "ToolContext",
overwrite: bool = False,
) -> None:
"""Write content to a file."""
pass
@abstractmethod
async def exists(self, path: str, context: "ToolContext") -> bool:
"""Check if a file or directory exists."""
pass
@abstractmethod
async def is_directory(self, path: str, context: "ToolContext") -> bool:
"""Check if a path is a directory."""
pass
@abstractmethod
async def search_files(
self,
query: str,
context: "ToolContext",
*,
max_results: int = 20,
include_content: bool = False,
) -> List[FileSearchMatch]:
"""Search for files matching a query within the accessible namespace."""
pass
@abstractmethod
async def run_bash(
self,
command: str,
context: "ToolContext",
*,
timeout: Optional[float] = None,
) -> CommandResult:
"""Execute a bash command within the accessible namespace."""
pass

View File

@@ -0,0 +1,25 @@
"""
File system capability models.
This module contains data models for file system operations.
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class FileSearchMatch:
"""Represents a single search result within a file system."""
path: str
snippet: Optional[str] = None
@dataclass
class CommandResult:
"""Represents the result of executing a shell command."""
stdout: str
stderr: str
returncode: int

View File

@@ -0,0 +1,13 @@
"""
SQL runner capability.
This module provides abstractions for SQL execution used by tools.
"""
from .base import SqlRunner
from .models import RunSqlToolArgs
__all__ = [
"SqlRunner",
"RunSqlToolArgs",
]

View File

@@ -0,0 +1,37 @@
"""
SQL runner capability interface.
This module contains the abstract base class for SQL execution.
"""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
import pandas as pd
from .models import RunSqlToolArgs
if TYPE_CHECKING:
from vanna.core.tool import ToolContext
class SqlRunner(ABC):
"""Interface for SQL execution with different implementations."""
@abstractmethod
async def run_sql(
self, args: RunSqlToolArgs, context: "ToolContext"
) -> pd.DataFrame:
"""Execute SQL query and return results as a DataFrame.
Args:
args: SQL query arguments
context: Tool execution context
Returns:
DataFrame with query results
Raises:
Exception: If query execution fails
"""
pass

View File

@@ -0,0 +1,13 @@
"""
SQL runner capability models.
This module contains data models for SQL execution.
"""
from pydantic import BaseModel, Field
class RunSqlToolArgs(BaseModel):
"""Arguments for run_sql tool."""
sql: str = Field(description="SQL query to execute")

View File

@@ -0,0 +1,92 @@
"""UI Component system for Vanna Agents."""
# Base component
from .base import UiComponent
# Simple components
from .simple import (
SimpleComponent,
SimpleComponentType,
SimpleTextComponent,
SimpleImageComponent,
SimpleLinkComponent,
)
# Rich components - re-export all
from .rich import (
# Base
RichComponent,
ComponentType,
ComponentLifecycle,
# Text
RichTextComponent,
# Data
DataFrameComponent,
ChartComponent,
# Feedback
NotificationComponent,
StatusCardComponent,
ProgressBarComponent,
ProgressDisplayComponent,
StatusIndicatorComponent,
LogViewerComponent,
LogEntry,
BadgeComponent,
IconTextComponent,
# Interactive
TaskListComponent,
Task,
StatusBarUpdateComponent,
TaskTrackerUpdateComponent,
ChatInputUpdateComponent,
TaskOperation,
ButtonComponent,
ButtonGroupComponent,
# Containers
CardComponent,
# Specialized
ArtifactComponent,
)
__all__ = [
# Base
"UiComponent",
# Simple components
"SimpleComponent",
"SimpleComponentType",
"SimpleTextComponent",
"SimpleImageComponent",
"SimpleLinkComponent",
# Rich components - Base
"RichComponent",
"ComponentType",
"ComponentLifecycle",
# Rich components - Text
"RichTextComponent",
# Rich components - Data
"DataFrameComponent",
"ChartComponent",
# Rich components - Feedback
"NotificationComponent",
"StatusCardComponent",
"ProgressBarComponent",
"ProgressDisplayComponent",
"StatusIndicatorComponent",
"LogViewerComponent",
"LogEntry",
"BadgeComponent",
"IconTextComponent",
# Rich components - Interactive
"TaskListComponent",
"Task",
"StatusBarUpdateComponent",
"TaskTrackerUpdateComponent",
"ChatInputUpdateComponent",
"TaskOperation",
"ButtonComponent",
"ButtonGroupComponent",
# Rich components - Containers
"CardComponent",
# Rich components - Specialized
"ArtifactComponent",
]

View File

@@ -0,0 +1,11 @@
"""
UI components base - re-exports UiComponent from core.
UiComponent lives in core/ because it's a fundamental return type for tools.
This module provides backward compatibility by re-exporting it here.
"""
# Re-export UiComponent from core for backward compatibility
from ..core.components import UiComponent
__all__ = ["UiComponent"]

View File

@@ -0,0 +1,83 @@
"""Rich UI components for the Vanna Agents framework."""
# Base classes and enums - import from core
from ...core.rich_component import RichComponent, ComponentType, ComponentLifecycle
# Text component
from .text import RichTextComponent
# Data components
from .data import (
DataFrameComponent,
ChartComponent,
)
# Feedback components
from .feedback import (
NotificationComponent,
StatusCardComponent,
ProgressBarComponent,
ProgressDisplayComponent,
StatusIndicatorComponent,
LogViewerComponent,
LogEntry,
BadgeComponent,
IconTextComponent,
)
# Interactive components
from .interactive import (
TaskListComponent,
Task,
StatusBarUpdateComponent,
TaskTrackerUpdateComponent,
ChatInputUpdateComponent,
TaskOperation,
ButtonComponent,
ButtonGroupComponent,
)
# Container components
from .containers import (
CardComponent,
)
# Specialized components
from .specialized import (
ArtifactComponent,
)
__all__ = [
# Base
"RichComponent",
"ComponentType",
"ComponentLifecycle",
# Text
"RichTextComponent",
# Data
"DataFrameComponent",
"ChartComponent",
# Feedback
"NotificationComponent",
"StatusCardComponent",
"ProgressBarComponent",
"ProgressDisplayComponent",
"StatusIndicatorComponent",
"LogViewerComponent",
"LogEntry",
"BadgeComponent",
"IconTextComponent",
# Interactive
"TaskListComponent",
"Task",
"StatusBarUpdateComponent",
"TaskTrackerUpdateComponent",
"ChatInputUpdateComponent",
"TaskOperation",
"ButtonComponent",
"ButtonGroupComponent",
# Containers
"CardComponent",
# Specialized
"ArtifactComponent",
]

View File

@@ -0,0 +1,7 @@
"""Container components for layout."""
from .card import CardComponent
__all__ = [
"CardComponent",
]

View File

@@ -0,0 +1,20 @@
"""Card component for displaying structured information."""
from typing import Any, Dict, List, Optional
from pydantic import Field
from ....core.rich_component import RichComponent, ComponentType
class CardComponent(RichComponent):
"""Card component for displaying structured information."""
type: ComponentType = ComponentType.CARD
title: str
content: str
subtitle: Optional[str] = None
icon: Optional[str] = None
status: Optional[str] = None # "success", "warning", "error", "info"
actions: List[Dict[str, Any]] = Field(default_factory=list)
collapsible: bool = False
collapsed: bool = False
markdown: bool = False # Whether content should be rendered as markdown

View File

@@ -0,0 +1,9 @@
"""Data display components."""
from .dataframe import DataFrameComponent
from .chart import ChartComponent
__all__ = [
"DataFrameComponent",
"ChartComponent",
]

View File

@@ -0,0 +1,17 @@
"""Chart component for data visualization."""
from typing import Any, Dict, Optional, Union
from pydantic import Field
from ....core.rich_component import RichComponent, ComponentType
class ChartComponent(RichComponent):
"""Chart component for data visualization."""
type: ComponentType = ComponentType.CHART
chart_type: str # "line", "bar", "pie", "scatter", etc.
data: Dict[str, Any] # Chart data in format expected by frontend
title: Optional[str] = None
width: Optional[Union[str, int]] = None
height: Optional[Union[str, int]] = None
config: Dict[str, Any] = Field(default_factory=dict) # Chart-specific config

View File

@@ -0,0 +1,93 @@
"""DataFrame component for displaying tabular data."""
from typing import Any, Dict, List, Optional
from pydantic import Field
from ....core.rich_component import RichComponent, ComponentType
class DataFrameComponent(RichComponent):
"""DataFrame component specifically for displaying tabular data from SQL queries and similar sources."""
type: ComponentType = ComponentType.DATAFRAME
rows: List[Dict[str, Any]] = Field(default_factory=list) # List of row dictionaries
columns: List[str] = Field(default_factory=list) # Column names in display order
title: Optional[str] = None
description: Optional[str] = None
row_count: int = 0
column_count: int = 0
# Display options
max_rows_displayed: int = 100 # Limit rows shown in UI
searchable: bool = True
sortable: bool = True
filterable: bool = True
exportable: bool = True # Allow export to CSV/Excel
# Styling options
striped: bool = True
bordered: bool = True
compact: bool = False
# Pagination
paginated: bool = True
page_size: int = 25
# Data types for better formatting (optional)
column_types: Dict[str, str] = Field(
default_factory=dict
) # column_name -> "string"|"number"|"date"|"boolean"
def __init__(self, **kwargs: Any) -> None:
# Set defaults before calling super().__init__
if "rows" not in kwargs:
kwargs["rows"] = []
if "columns" not in kwargs:
kwargs["columns"] = []
if "column_types" not in kwargs:
kwargs["column_types"] = {}
super().__init__(**kwargs)
# Auto-calculate counts if not provided
if self.rows and len(self.rows) > 0:
if "row_count" not in kwargs:
self.row_count = len(self.rows)
if not self.columns and self.rows:
self.columns = list(self.rows[0].keys())
if "column_count" not in kwargs:
self.column_count = len(self.columns)
else:
if "row_count" not in kwargs:
self.row_count = 0
if "column_count" not in kwargs:
self.column_count = len(self.columns) if self.columns else 0
@classmethod
def from_records(
cls,
records: List[Dict[str, Any]],
title: Optional[str] = None,
description: Optional[str] = None,
**kwargs: Any,
) -> "DataFrameComponent":
"""Create a DataFrame component from a list of record dictionaries."""
columns = list(records[0].keys()) if records else []
# Ensure we pass the required arguments correctly
component_data = {
"rows": records,
"columns": columns,
"row_count": len(records),
"column_count": len(columns),
"column_types": {}, # Initialize empty dict
}
if title is not None:
component_data["title"] = title
if description is not None:
component_data["description"] = description
# Merge with any additional kwargs
component_data.update(kwargs)
return cls(**component_data)

View File

@@ -0,0 +1,21 @@
"""User feedback components."""
from .notification import NotificationComponent
from .status_card import StatusCardComponent
from .progress import ProgressBarComponent, ProgressDisplayComponent
from .status_indicator import StatusIndicatorComponent
from .log_viewer import LogViewerComponent, LogEntry
from .badge import BadgeComponent
from .icon_text import IconTextComponent
__all__ = [
"NotificationComponent",
"StatusCardComponent",
"ProgressBarComponent",
"ProgressDisplayComponent",
"StatusIndicatorComponent",
"LogViewerComponent",
"LogEntry",
"BadgeComponent",
"IconTextComponent",
]

View File

@@ -0,0 +1,16 @@
"""Badge component for displaying status or labels."""
from typing import Optional
from ....core.rich_component import RichComponent, ComponentType
class BadgeComponent(RichComponent):
"""Simple badge/pill component for displaying status or labels."""
type: ComponentType = ComponentType.BADGE
text: str
variant: str = (
"default" # "default", "primary", "success", "warning", "error", "info"
)
size: str = "medium" # "small", "medium", "large"
icon: Optional[str] = None

View File

@@ -0,0 +1,14 @@
"""Icon with text component."""
from ....core.rich_component import RichComponent, ComponentType
class IconTextComponent(RichComponent):
"""Simple component for displaying an icon with text."""
type: ComponentType = ComponentType.ICON_TEXT
icon: str
text: str
variant: str = "default" # "default", "primary", "secondary", "muted"
size: str = "medium" # "small", "medium", "large"
alignment: str = "left" # "left", "center", "right"

View File

@@ -0,0 +1,41 @@
"""Log viewer component."""
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from ....core.rich_component import RichComponent, ComponentType
class LogEntry(BaseModel):
"""Log entry for tool execution."""
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
level: str = "info" # "debug", "info", "warning", "error"
message: str
data: Optional[Dict[str, Any]] = None
class LogViewerComponent(RichComponent):
"""Generic log viewer for displaying timestamped entries."""
type: ComponentType = ComponentType.LOG_VIEWER
title: str = "Logs"
entries: List[LogEntry] = Field(default_factory=list)
max_entries: int = 100
searchable: bool = True
show_timestamps: bool = True
auto_scroll: bool = True
def add_entry(
self, message: str, level: str = "info", data: Optional[Dict[str, Any]] = None
) -> "LogViewerComponent":
"""Add a new log entry."""
new_entry = LogEntry(message=message, level=level, data=data)
new_entries = self.entries + [new_entry]
# Limit to max_entries
if len(new_entries) > self.max_entries:
new_entries = new_entries[-self.max_entries :]
return self.update(entries=new_entries)

View File

@@ -0,0 +1,19 @@
"""Notification component for alerts and messages."""
from typing import Any, Dict, List, Optional
from pydantic import Field
from ....core.rich_component import RichComponent, ComponentType
class NotificationComponent(RichComponent):
"""Notification component for alerts and messages."""
type: ComponentType = ComponentType.NOTIFICATION
message: str
title: Optional[str] = None
level: str = "info" # "success", "info", "warning", "error"
icon: Optional[str] = None
dismissible: bool = True
auto_dismiss: bool = False
auto_dismiss_delay: int = 5000 # milliseconds
actions: List[Dict[str, Any]] = Field(default_factory=list)

View File

@@ -0,0 +1,37 @@
"""Progress components for displaying progress indicators."""
from typing import Any, Dict, Optional
from ....core.rich_component import RichComponent, ComponentType
class ProgressBarComponent(RichComponent):
"""Progress bar with status and value."""
type: ComponentType = ComponentType.PROGRESS_BAR
value: float # 0.0 to 1.0
label: Optional[str] = None
show_percentage: bool = True
status: Optional[str] = None # "success", "warning", "error"
animated: bool = False
class ProgressDisplayComponent(RichComponent):
"""Generic progress display for any long-running process."""
type: ComponentType = ComponentType.PROGRESS_DISPLAY
label: str
value: float = 0.0 # 0.0 to 1.0
description: Optional[str] = None
status: Optional[str] = None # "info", "success", "warning", "error"
show_percentage: bool = True
animated: bool = False
indeterminate: bool = False
def update_progress(
self, value: float, description: Optional[str] = None
) -> "ProgressDisplayComponent":
"""Update progress value and optionally description."""
updates: Dict[str, Any] = {"value": max(0.0, min(1.0, value))}
if description is not None:
updates["description"] = description
return self.update(**updates)

View File

@@ -0,0 +1,28 @@
"""Status card component for displaying process status."""
from typing import Any, Dict, List, Optional
from pydantic import Field
from ....core.rich_component import RichComponent, ComponentType
class StatusCardComponent(RichComponent):
"""Generic status card that can display any process status."""
type: ComponentType = ComponentType.STATUS_CARD
title: str
status: str # "pending", "running", "completed", "failed", "success", "warning", "error"
description: Optional[str] = None
icon: Optional[str] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
actions: List[Dict[str, Any]] = Field(default_factory=list)
collapsible: bool = False
collapsed: bool = False
def set_status(
self, status: str, description: Optional[str] = None
) -> "StatusCardComponent":
"""Update the status and optionally the description."""
updates = {"status": status}
if description is not None:
updates["description"] = description
return self.update(**updates)

View File

@@ -0,0 +1,14 @@
"""Status indicator component."""
from typing import Optional
from ....core.rich_component import RichComponent, ComponentType
class StatusIndicatorComponent(RichComponent):
"""Status indicator with icon and message."""
type: ComponentType = ComponentType.STATUS_INDICATOR
status: str # "success", "warning", "error", "info", "loading"
message: str
icon: Optional[str] = None
pulse: bool = False

View File

@@ -0,0 +1,21 @@
"""Interactive components."""
from .task_list import TaskListComponent, Task
from .ui_state import (
StatusBarUpdateComponent,
TaskTrackerUpdateComponent,
ChatInputUpdateComponent,
TaskOperation,
)
from .button import ButtonComponent, ButtonGroupComponent
__all__ = [
"TaskListComponent",
"Task",
"StatusBarUpdateComponent",
"TaskTrackerUpdateComponent",
"ChatInputUpdateComponent",
"TaskOperation",
"ButtonComponent",
"ButtonGroupComponent",
]

View File

@@ -0,0 +1,95 @@
"""Button component for interactive actions."""
from typing import Any, Dict, List, Literal, Optional
from ....core.rich_component import ComponentType, RichComponent
class ButtonComponent(RichComponent):
"""Interactive button that sends a message when clicked.
The button renders in the UI and when clicked, sends its action
value as a message to the chat input.
Args:
label: Text displayed on the button
action: Message/command to send when clicked
variant: Visual style variant
size: Button size
icon: Optional emoji or icon
icon_position: Position of icon relative to label
disabled: Whether button is disabled
Example:
ButtonComponent(
label="Generate Report",
action="/report sales",
variant="primary",
icon="📊"
)
"""
def __init__(
self,
label: str,
action: str,
variant: Literal[
"primary", "secondary", "success", "warning", "error", "ghost", "link"
] = "primary",
size: Literal["small", "medium", "large"] = "medium",
icon: Optional[str] = None,
icon_position: Literal["left", "right"] = "left",
disabled: bool = False,
):
super().__init__(
type=ComponentType.BUTTON,
data={
"label": label,
"action": action,
"variant": variant,
"size": size,
"icon": icon,
"icon_position": icon_position,
"disabled": disabled,
},
)
class ButtonGroupComponent(RichComponent):
"""Group of buttons with consistent styling.
Args:
buttons: List of button data dictionaries
orientation: Layout direction
spacing: Gap between buttons
alignment: Button alignment within group
full_width: Whether buttons should stretch to fill width
Example:
ButtonGroupComponent(
buttons=[
{"label": "Yes", "action": "/confirm yes", "variant": "success"},
{"label": "No", "action": "/confirm no", "variant": "error"},
],
orientation="horizontal",
spacing="medium"
)
"""
def __init__(
self,
buttons: List[Dict[str, Any]],
orientation: Literal["horizontal", "vertical"] = "horizontal",
spacing: Literal["small", "medium", "large"] = "medium",
alignment: Literal["start", "center", "end", "stretch"] = "start",
full_width: bool = False,
):
super().__init__(
type=ComponentType.BUTTON_GROUP,
data={
"buttons": buttons,
"orientation": orientation,
"spacing": spacing,
"alignment": alignment,
"full_width": full_width,
},
)

View File

@@ -0,0 +1,58 @@
"""Task list component for interactive task tracking."""
import uuid
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from ....core.rich_component import RichComponent, ComponentType
class Task(BaseModel):
"""Individual task in a task list."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
title: str
description: Optional[str] = None
status: str = "pending" # "pending", "in_progress", "completed", "error"
progress: Optional[float] = None # 0.0 to 1.0
created_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
completed_at: Optional[str] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
class TaskListComponent(RichComponent):
"""Interactive task list with progress tracking."""
type: ComponentType = ComponentType.TASK_LIST
title: str = "Tasks"
tasks: List[Task] = Field(default_factory=list)
show_progress: bool = True
allow_reorder: bool = False
show_timestamps: bool = True
filter_status: Optional[str] = None # Filter by task status
def add_task(self, task: Task) -> "TaskListComponent":
"""Add a task to the list."""
new_tasks = self.tasks + [task]
return self.update(tasks=new_tasks)
def update_task(self, task_id: str, **updates: Any) -> "TaskListComponent":
"""Update a specific task."""
new_tasks = []
for task in self.tasks:
if task.id == task_id:
task_data = task.model_dump()
task_data.update(updates)
new_tasks.append(Task(**task_data))
else:
new_tasks.append(task)
return self.update(tasks=new_tasks)
def complete_task(self, task_id: str) -> "TaskListComponent":
"""Mark a task as completed."""
return self.update_task(
task_id,
status="completed",
completed_at=datetime.utcnow().isoformat(),
progress=1.0,
)

View File

@@ -0,0 +1,93 @@
"""UI state update components for controlling interface elements."""
from enum import Enum
from typing import Any, Optional
from .task_list import Task
from ....core.rich_component import RichComponent, ComponentType
class StatusBarUpdateComponent(RichComponent):
"""Component for updating the status bar above chat input."""
type: ComponentType = ComponentType.STATUS_BAR_UPDATE
status: str # "idle", "working", "success", "error"
message: str
detail: Optional[str] = None
def __init__(self, **kwargs: Any) -> None:
# Set a fixed ID for status bar updates
kwargs.setdefault("id", "vanna-status-bar")
super().__init__(**kwargs)
class TaskOperation(str, Enum):
"""Operations for task tracker updates."""
ADD_TASK = "add_task"
UPDATE_TASK = "update_task"
REMOVE_TASK = "remove_task"
CLEAR_TASKS = "clear_tasks"
class TaskTrackerUpdateComponent(RichComponent):
"""Component for updating the task tracker in the sidebar."""
type: ComponentType = ComponentType.TASK_TRACKER_UPDATE
operation: TaskOperation
task: Optional[Task] = None # Used for ADD_TASK
task_id: Optional[str] = None # Used for UPDATE_TASK and REMOVE_TASK
status: Optional[str] = None # Used for UPDATE_TASK
progress: Optional[float] = None # Used for UPDATE_TASK
detail: Optional[str] = None # Used for UPDATE_TASK
def __init__(self, **kwargs: Any) -> None:
# Set a fixed ID for task tracker updates
kwargs.setdefault("id", "vanna-task-tracker")
super().__init__(**kwargs)
@classmethod
def add_task(cls, task: Task) -> "TaskTrackerUpdateComponent":
"""Create a component to add a new task."""
return cls(operation=TaskOperation.ADD_TASK, task=task)
@classmethod
def update_task(
cls,
task_id: str,
status: Optional[str] = None,
progress: Optional[float] = None,
detail: Optional[str] = None,
) -> "TaskTrackerUpdateComponent":
"""Create a component to update an existing task."""
return cls(
operation=TaskOperation.UPDATE_TASK,
task_id=task_id,
status=status,
progress=progress,
detail=detail,
)
@classmethod
def remove_task(cls, task_id: str) -> "TaskTrackerUpdateComponent":
"""Create a component to remove a task."""
return cls(operation=TaskOperation.REMOVE_TASK, task_id=task_id)
@classmethod
def clear_tasks(cls) -> "TaskTrackerUpdateComponent":
"""Create a component to clear all tasks."""
return cls(operation=TaskOperation.CLEAR_TASKS)
class ChatInputUpdateComponent(RichComponent):
"""Component for updating chat input state and appearance."""
type: ComponentType = ComponentType.CHAT_INPUT_UPDATE
placeholder: Optional[str] = None
disabled: Optional[bool] = None
value: Optional[str] = None # Set input text value
focus: Optional[bool] = None # Focus/unfocus the input
def __init__(self, **kwargs: Any) -> None:
# Set a fixed ID for chat input updates
kwargs.setdefault("id", "vanna-chat-input")
super().__init__(**kwargs)

View File

@@ -0,0 +1,7 @@
"""Specialized components."""
from .artifact import ArtifactComponent
__all__ = [
"ArtifactComponent",
]

View File

@@ -0,0 +1,20 @@
"""Artifact component for interactive content."""
import uuid
from typing import Optional
from pydantic import Field
from ....core.rich_component import RichComponent, ComponentType
class ArtifactComponent(RichComponent):
"""Component for displaying interactive artifacts that can be rendered externally."""
type: ComponentType = ComponentType.ARTIFACT
artifact_id: str = Field(default_factory=lambda: f"artifact_{uuid.uuid4().hex[:8]}")
content: str # HTML/SVG/JS content
artifact_type: str # "html", "svg", "visualization", "interactive", "d3", "threejs"
title: Optional[str] = None
description: Optional[str] = None
editable: bool = True
fullscreen_capable: bool = True
external_renderable: bool = True

View File

@@ -0,0 +1,16 @@
"""Rich text component."""
from typing import Optional
from ...core.rich_component import RichComponent, ComponentType
class RichTextComponent(RichComponent):
"""Rich text component with formatting options."""
type: ComponentType = ComponentType.TEXT
content: str
markdown: bool = False
code_language: Optional[str] = None # For syntax highlighting
font_size: Optional[str] = None
font_weight: Optional[str] = None
text_align: Optional[str] = None

View File

@@ -0,0 +1,15 @@
"""Simple UI components for basic rendering."""
# Import from core
from ...core.simple_component import SimpleComponent, SimpleComponentType
from .text import SimpleTextComponent
from .image import SimpleImageComponent
from .link import SimpleLinkComponent
__all__ = [
"SimpleComponent",
"SimpleComponentType",
"SimpleTextComponent",
"SimpleImageComponent",
"SimpleLinkComponent",
]

View File

@@ -0,0 +1,15 @@
"""Simple image component."""
from typing import Optional
from pydantic import Field
from ...core.simple_component import SimpleComponent, SimpleComponentType
class SimpleImageComponent(SimpleComponent):
"""A simple image component."""
type: SimpleComponentType = SimpleComponentType.IMAGE
url: str = Field(..., description="The URL of the image to display.")
alt_text: Optional[str] = Field(
default=None, description="Alternative text for the image."
)

View File

@@ -0,0 +1,15 @@
"""Simple link component."""
from typing import Optional
from pydantic import Field
from ...core.simple_component import SimpleComponent, SimpleComponentType
class SimpleLinkComponent(SimpleComponent):
"""A simple link component."""
type: SimpleComponentType = SimpleComponentType.LINK
url: str = Field(..., description="The URL the link points to.")
text: Optional[str] = Field(
default=None, description="The display text for the link."
)

View File

@@ -0,0 +1,11 @@
"""Simple text component."""
from pydantic import Field
from ...core.simple_component import SimpleComponent, SimpleComponentType
class SimpleTextComponent(SimpleComponent):
"""A simple text component."""
type: SimpleComponentType = SimpleComponentType.TEXT
text: str = Field(..., description="The text content to display.")

View File

@@ -0,0 +1,193 @@
"""
Core components of the Vanna Agents framework.
This package contains the fundamental abstractions and implementations
that form the foundation of the agent framework.
"""
# Core domains - re-export from new structure
from .tool import T, Tool, ToolCall, ToolContext, ToolResult, ToolSchema
from .llm import LlmMessage, LlmRequest, LlmResponse, LlmService, LlmStreamChunk
from .storage import Conversation, ConversationStore, Message
from .user import User, UserService
from .agent import Agent, AgentConfig
from .system_prompt import DefaultSystemPromptBuilder, SystemPromptBuilder
from .lifecycle import LifecycleHook
from .middleware import LlmMiddleware
from .workflow import WorkflowHandler, WorkflowResult, DefaultWorkflowHandler
from .recovery import ErrorRecoveryStrategy, RecoveryAction, RecoveryActionType
from .enricher import ToolContextEnricher
from .enhancer import LlmContextEnhancer, DefaultLlmContextEnhancer
from .filter import ConversationFilter
from .observability import ObservabilityProvider, Span, Metric
from .audit import (
AuditLogger,
AuditEvent,
AuditEventType,
ToolAccessCheckEvent,
ToolInvocationEvent,
ToolResultEvent,
UiFeatureAccessCheckEvent,
AiResponseEvent,
)
# UI Components
from .components import UiComponent
from .rich_component import RichComponent
from ..components import (
SimpleComponent,
SimpleComponentType,
SimpleImageComponent,
SimpleLinkComponent,
SimpleTextComponent,
ArtifactComponent,
BadgeComponent,
CardComponent,
DataFrameComponent,
IconTextComponent,
LogViewerComponent,
NotificationComponent,
ProgressBarComponent,
ProgressDisplayComponent,
RichTextComponent,
StatusCardComponent,
TaskListComponent,
)
# Exceptions
from .errors import (
AgentError,
ConversationNotFoundError,
LlmServiceError,
PermissionError,
ToolExecutionError,
ToolNotFoundError,
ValidationError,
)
# Core implementations
from .registry import ToolRegistry
# Evaluation framework
from .evaluation import (
Evaluator,
TestCase,
ExpectedOutcome,
AgentResult,
EvaluationResult,
TestCaseResult,
AgentVariant,
EvaluationRunner,
TrajectoryEvaluator,
OutputEvaluator,
LLMAsJudgeEvaluator,
EfficiencyEvaluator,
EvaluationReport,
ComparisonReport,
EvaluationDataset,
)
# Rebuild models to resolve forward references after all imports
from .tool.models import ToolContext, ToolResult
from .components import UiComponent # Import UiComponent to ensure it's available
ToolContext.model_rebuild()
ToolResult.model_rebuild()
__all__ = [
# Models
"User",
"Message",
"Conversation",
"ToolCall",
"ToolResult",
"ToolContext",
"ToolSchema",
"LlmMessage",
"LlmRequest",
"LlmResponse",
"LlmStreamChunk",
"RecoveryAction",
"RecoveryActionType",
"Span",
"Metric",
# Interfaces
"Tool",
"Agent",
"LlmService",
"ConversationStore",
"UserService",
"SystemPromptBuilder",
"LifecycleHook",
"LlmMiddleware",
"WorkflowHandler",
"DefaultWorkflowHandler",
"WorkflowResult",
"ErrorRecoveryStrategy",
"ToolContextEnricher",
"LlmContextEnhancer",
"DefaultLlmContextEnhancer",
"ConversationFilter",
"ObservabilityProvider",
"AuditLogger",
"T",
# Audit
"AuditEvent",
"AuditEventType",
"ToolAccessCheckEvent",
"ToolInvocationEvent",
"ToolResultEvent",
"UiFeatureAccessCheckEvent",
"AiResponseEvent",
# UI Components
"UiComponent",
# Simple Components
"SimpleComponent",
"SimpleComponentType",
"SimpleTextComponent",
"SimpleImageComponent",
"SimpleLinkComponent",
# Rich Components
"RichComponent",
"ArtifactComponent",
"BadgeComponent",
"CardComponent",
"DataFrameComponent",
"IconTextComponent",
"LogViewerComponent",
"NotificationComponent",
"ProgressBarComponent",
"ProgressDisplayComponent",
"RichTextComponent",
"StatusCardComponent",
"TaskListComponent",
# Core implementations
"ToolRegistry",
"Agent",
"AgentConfig",
"DefaultSystemPromptBuilder",
# Evaluation
"Evaluator",
"TestCase",
"ExpectedOutcome",
"AgentResult",
"EvaluationResult",
"TestCaseResult",
"AgentVariant",
"EvaluationRunner",
"TrajectoryEvaluator",
"OutputEvaluator",
"LLMAsJudgeEvaluator",
"EfficiencyEvaluator",
"EvaluationReport",
"ComparisonReport",
"EvaluationDataset",
# Exceptions
"AgentError",
"ToolExecutionError",
"ToolNotFoundError",
"PermissionError",
"ConversationNotFoundError",
"LlmServiceError",
"ValidationError",
]

View File

@@ -0,0 +1,19 @@
"""
Compatibility shims for different Python versions.
This module provides compatibility utilities for features that vary across
Python versions.
"""
try:
from enum import StrEnum # Py 3.11+
except ImportError: # Py < 3.11
from enum import Enum
class StrEnum(str, Enum): # type: ignore[no-redef]
"""Minimal backport of StrEnum for Python < 3.11."""
pass
__all__ = ["StrEnum"]

View File

@@ -0,0 +1,10 @@
"""
Agent module.
This module contains the core Agent implementation and configuration.
"""
from .agent import Agent
from .config import AgentConfig
__all__ = ["Agent", "AgentConfig"]

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More