Initial commit
This commit is contained in:
1
aivanov_project/vanna/.gitattributes
vendored
Normal file
1
aivanov_project/vanna/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.ipynb linguist-detectable=false
|
||||
28
aivanov_project/vanna/.gitignore
vendored
Normal file
28
aivanov_project/vanna/.gitignore
vendored
Normal 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/
|
||||
19
aivanov_project/vanna/.pre-commit-config.yaml
Normal file
19
aivanov_project/vanna/.pre-commit-config.yaml
Normal 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" ]
|
||||
485
aivanov_project/vanna/CONTRIBUTING.md
Normal file
485
aivanov_project/vanna/CONTRIBUTING.md
Normal 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! 🎉
|
||||
21
aivanov_project/vanna/LICENSE
Normal file
21
aivanov_project/vanna/LICENSE
Normal 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.
|
||||
296
aivanov_project/vanna/MIGRATION_GUIDE.md
Normal file
296
aivanov_project/vanna/MIGRATION_GUIDE.md
Normal 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! 🚀
|
||||
311
aivanov_project/vanna/README.md
Normal file
311
aivanov_project/vanna/README.md
Normal 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.
|
||||
|
||||
[](https://python.org)
|
||||
[](LICENSE)
|
||||
[](https://github.com/psf/black)
|
||||
|
||||
https://github.com/user-attachments/assets/476cd421-d0b0-46af-8b29-0f40c73d6d83
|
||||
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
183
aivanov_project/vanna/README_AIVANOV.md
Normal file
183
aivanov_project/vanna/README_AIVANOV.md
Normal 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.
|
||||
270
aivanov_project/vanna/README_LEGACY.md
Normal file
270
aivanov_project/vanna/README_LEGACY.md
Normal file
@@ -0,0 +1,270 @@
|
||||
|
||||
|
||||
| GitHub | PyPI | Documentation | Gurubase |
|
||||
| ------ | ---- | ------------- | -------- |
|
||||
| [](https://github.com/vanna-ai/vanna) | [](https://pypi.org/project/vanna/) | [](https://vanna.ai/docs/) | [](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
|
||||
|
||||

|
||||
|
||||
## How Vanna works
|
||||
|
||||

|
||||
|
||||
|
||||
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**.
|
||||
|
||||

|
||||
|
||||
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:
|
||||

|
||||
|
||||
## 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.**
|
||||
- Vanna’s 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)
|
||||
137
aivanov_project/vanna/examples/chromadb_gpu_example.py
Normal file
137
aivanov_project/vanna/examples/chromadb_gpu_example.py
Normal 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)
|
||||
156
aivanov_project/vanna/examples/transform_args_example.py
Normal file
156
aivanov_project/vanna/examples/transform_args_example.py
Normal 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())
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -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;
|
||||
422
aivanov_project/vanna/frontends/webcomponent/TEST_README.md
Normal file
422
aivanov_project/vanna/frontends/webcomponent/TEST_README.md
Normal 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!** 🧪
|
||||
57
aivanov_project/vanna/frontends/webcomponent/package.json
Normal file
57
aivanov_project/vanna/frontends/webcomponent/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
`,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`,
|
||||
};
|
||||
@@ -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
@@ -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>
|
||||
`,
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
37
aivanov_project/vanna/frontends/webcomponent/src/index.ts
Normal file
37
aivanov_project/vanna/frontends/webcomponent/src/index.ts
Normal 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
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
`;
|
||||
4
aivanov_project/vanna/frontends/webcomponent/src/vite-env.d.ts
vendored
Normal file
4
aivanov_project/vanna/frontends/webcomponent/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __BUILD_TIME__: string;
|
||||
declare const __BUILD_VERSION__: string;
|
||||
@@ -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>
|
||||
874
aivanov_project/vanna/frontends/webcomponent/test_backend.py
Normal file
874
aivanov_project/vanna/frontends/webcomponent/test_backend.py
Normal 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)
|
||||
20
aivanov_project/vanna/frontends/webcomponent/tsconfig.json
Normal file
20
aivanov_project/vanna/frontends/webcomponent/tsconfig.json
Normal 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"]
|
||||
}
|
||||
24
aivanov_project/vanna/frontends/webcomponent/vite.config.ts
Normal file
24
aivanov_project/vanna/frontends/webcomponent/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
169
aivanov_project/vanna/notebooks/quickstart.ipynb
Normal file
169
aivanov_project/vanna/notebooks/quickstart.ipynb
Normal 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
|
||||
}
|
||||
222
aivanov_project/vanna/pyproject.toml
Normal file
222
aivanov_project/vanna/pyproject.toml
Normal 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"
|
||||
144
aivanov_project/vanna/run_server.py
Normal file
144
aivanov_project/vanna/run_server.py
Normal 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)
|
||||
10
aivanov_project/vanna/setup.cfg
Normal file
10
aivanov_project/vanna/setup.cfg
Normal 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
|
||||
172
aivanov_project/vanna/src/evals/benchmarks/llm_comparison.py
Normal file
172
aivanov_project/vanna/src/evals/benchmarks/llm_comparison.py
Normal 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())
|
||||
@@ -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"
|
||||
172
aivanov_project/vanna/src/vanna/__init__.py
Normal file
172
aivanov_project/vanna/src/vanna/__init__.py
Normal 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",
|
||||
]
|
||||
7
aivanov_project/vanna/src/vanna/agents/__init__.py
Normal file
7
aivanov_project/vanna/src/vanna/agents/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Agent implementations.
|
||||
|
||||
This package contains agent implementations and utilities.
|
||||
"""
|
||||
|
||||
__all__: list[str] = []
|
||||
17
aivanov_project/vanna/src/vanna/capabilities/__init__.py
Normal file
17
aivanov_project/vanna/src/vanna/capabilities/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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")
|
||||
92
aivanov_project/vanna/src/vanna/components/__init__.py
Normal file
92
aivanov_project/vanna/src/vanna/components/__init__.py
Normal 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",
|
||||
]
|
||||
11
aivanov_project/vanna/src/vanna/components/base.py
Normal file
11
aivanov_project/vanna/src/vanna/components/base.py
Normal 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"]
|
||||
83
aivanov_project/vanna/src/vanna/components/rich/__init__.py
Normal file
83
aivanov_project/vanna/src/vanna/components/rich/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Container components for layout."""
|
||||
|
||||
from .card import CardComponent
|
||||
|
||||
__all__ = [
|
||||
"CardComponent",
|
||||
]
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Data display components."""
|
||||
|
||||
from .dataframe import DataFrameComponent
|
||||
from .chart import ChartComponent
|
||||
|
||||
__all__ = [
|
||||
"DataFrameComponent",
|
||||
"ChartComponent",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Specialized components."""
|
||||
|
||||
from .artifact import ArtifactComponent
|
||||
|
||||
__all__ = [
|
||||
"ArtifactComponent",
|
||||
]
|
||||
@@ -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
|
||||
16
aivanov_project/vanna/src/vanna/components/rich/text.py
Normal file
16
aivanov_project/vanna/src/vanna/components/rich/text.py
Normal 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
|
||||
@@ -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",
|
||||
]
|
||||
15
aivanov_project/vanna/src/vanna/components/simple/image.py
Normal file
15
aivanov_project/vanna/src/vanna/components/simple/image.py
Normal 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."
|
||||
)
|
||||
15
aivanov_project/vanna/src/vanna/components/simple/link.py
Normal file
15
aivanov_project/vanna/src/vanna/components/simple/link.py
Normal 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."
|
||||
)
|
||||
11
aivanov_project/vanna/src/vanna/components/simple/text.py
Normal file
11
aivanov_project/vanna/src/vanna/components/simple/text.py
Normal 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.")
|
||||
193
aivanov_project/vanna/src/vanna/core/__init__.py
Normal file
193
aivanov_project/vanna/src/vanna/core/__init__.py
Normal 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",
|
||||
]
|
||||
19
aivanov_project/vanna/src/vanna/core/_compat.py
Normal file
19
aivanov_project/vanna/src/vanna/core/_compat.py
Normal 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"]
|
||||
10
aivanov_project/vanna/src/vanna/core/agent/__init__.py
Normal file
10
aivanov_project/vanna/src/vanna/core/agent/__init__.py
Normal 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"]
|
||||
1407
aivanov_project/vanna/src/vanna/core/agent/agent.py
Normal file
1407
aivanov_project/vanna/src/vanna/core/agent/agent.py
Normal file
File diff suppressed because it is too large
Load Diff
123
aivanov_project/vanna/src/vanna/core/agent/config.py
Normal file
123
aivanov_project/vanna/src/vanna/core/agent/config.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Agent configuration.
|
||||
|
||||
This module contains configuration models that control agent behavior.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .._compat import StrEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..user import User
|
||||
|
||||
|
||||
class UiFeature(StrEnum):
|
||||
UI_FEATURE_SHOW_TOOL_NAMES = "tool_names"
|
||||
UI_FEATURE_SHOW_TOOL_ARGUMENTS = "tool_arguments"
|
||||
UI_FEATURE_SHOW_TOOL_ERROR = "tool_error"
|
||||
UI_FEATURE_SHOW_TOOL_INVOCATION_MESSAGE_IN_CHAT = "tool_invocation_message_in_chat"
|
||||
UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS = "memory_detailed_results"
|
||||
|
||||
|
||||
# Optional: you can also define defaults if you want a shared baseline
|
||||
DEFAULT_UI_FEATURES: Dict[str, List[str]] = {
|
||||
UiFeature.UI_FEATURE_SHOW_TOOL_NAMES: ["admin", "user"],
|
||||
UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS: ["admin"],
|
||||
UiFeature.UI_FEATURE_SHOW_TOOL_ERROR: ["admin"],
|
||||
UiFeature.UI_FEATURE_SHOW_TOOL_INVOCATION_MESSAGE_IN_CHAT: ["admin"],
|
||||
UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS: ["admin"],
|
||||
}
|
||||
|
||||
|
||||
class UiFeatures(BaseModel):
|
||||
"""UI features with group-based access control using the same pattern as tools.
|
||||
|
||||
Each field specifies which groups can access that UI feature.
|
||||
Empty list means the feature is accessible to all users.
|
||||
Uses the same intersection logic as tool access control.
|
||||
"""
|
||||
|
||||
# Custom features for extensibility
|
||||
feature_group_access: Dict[str, List[str]] = Field(
|
||||
default_factory=lambda: DEFAULT_UI_FEATURES.copy(),
|
||||
description="Which groups can access UI features",
|
||||
)
|
||||
|
||||
def can_user_access_feature(self, feature_name: str, user: "User") -> bool:
|
||||
"""Check if user can access a UI feature using same logic as tools.
|
||||
|
||||
Args:
|
||||
feature_name: Name of the UI feature to check
|
||||
user: User object with group_memberships
|
||||
|
||||
Returns:
|
||||
True if user has access, False otherwise
|
||||
"""
|
||||
# Then try custom features
|
||||
if feature_name in self.feature_group_access:
|
||||
allowed_groups = self.feature_group_access[feature_name]
|
||||
else:
|
||||
# Feature doesn't exist, deny access
|
||||
return False
|
||||
|
||||
# Empty list means all users can access (same as tools)
|
||||
if not allowed_groups:
|
||||
return True
|
||||
|
||||
# Same intersection logic as tool access control
|
||||
user_groups = set(user.group_memberships)
|
||||
feature_groups = set(allowed_groups)
|
||||
return bool(user_groups & feature_groups)
|
||||
|
||||
def register_feature(self, name: str, access_groups: List[str]) -> None:
|
||||
"""Register a custom UI feature with group access control.
|
||||
|
||||
Args:
|
||||
name: Name of the custom feature
|
||||
access_groups: List of groups that can access this feature
|
||||
"""
|
||||
self.feature_group_access[name] = access_groups
|
||||
|
||||
|
||||
class AuditConfig(BaseModel):
|
||||
"""Configuration for audit logging."""
|
||||
|
||||
enabled: bool = Field(default=True, description="Enable audit logging")
|
||||
log_tool_access_checks: bool = Field(
|
||||
default=True, description="Log tool access permission checks"
|
||||
)
|
||||
log_tool_invocations: bool = Field(
|
||||
default=True, description="Log tool invocations with parameters"
|
||||
)
|
||||
log_tool_results: bool = Field(
|
||||
default=True, description="Log tool execution results"
|
||||
)
|
||||
log_ui_feature_checks: bool = Field(
|
||||
default=False, description="Log UI feature access checks (can be noisy)"
|
||||
)
|
||||
log_ai_responses: bool = Field(
|
||||
default=True, description="Log AI-generated responses"
|
||||
)
|
||||
include_full_ai_responses: bool = Field(
|
||||
default=False,
|
||||
description="Include full AI response text in logs (privacy concern)",
|
||||
)
|
||||
sanitize_tool_parameters: bool = Field(
|
||||
default=True, description="Sanitize sensitive parameters (passwords, tokens)"
|
||||
)
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""Configuration for agent behavior."""
|
||||
|
||||
max_tool_iterations: int = Field(default=10, gt=0)
|
||||
stream_responses: bool = Field(default=True)
|
||||
auto_save_conversations: bool = Field(default=True)
|
||||
include_thinking_indicators: bool = Field(default=True)
|
||||
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
||||
max_tokens: Optional[int] = Field(default=None, gt=0)
|
||||
ui_features: UiFeatures = Field(default_factory=UiFeatures)
|
||||
audit_config: AuditConfig = Field(default_factory=AuditConfig)
|
||||
28
aivanov_project/vanna/src/vanna/core/audit/__init__.py
Normal file
28
aivanov_project/vanna/src/vanna/core/audit/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Audit logging for the Vanna Agents framework.
|
||||
|
||||
This module provides interfaces and models for audit logging, enabling
|
||||
tracking of user actions, tool invocations, and access control decisions.
|
||||
"""
|
||||
|
||||
from .base import AuditLogger
|
||||
from .models import (
|
||||
AiResponseEvent,
|
||||
AuditEvent,
|
||||
AuditEventType,
|
||||
ToolAccessCheckEvent,
|
||||
ToolInvocationEvent,
|
||||
ToolResultEvent,
|
||||
UiFeatureAccessCheckEvent,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AuditLogger",
|
||||
"AuditEvent",
|
||||
"AuditEventType",
|
||||
"ToolAccessCheckEvent",
|
||||
"ToolInvocationEvent",
|
||||
"ToolResultEvent",
|
||||
"UiFeatureAccessCheckEvent",
|
||||
"AiResponseEvent",
|
||||
]
|
||||
299
aivanov_project/vanna/src/vanna/core/audit/base.py
Normal file
299
aivanov_project/vanna/src/vanna/core/audit/base.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Base audit logger interface.
|
||||
|
||||
Audit loggers enable tracking user actions, tool invocations, and access control
|
||||
decisions for security, compliance, and debugging.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from .models import (
|
||||
AiResponseEvent,
|
||||
AuditEvent,
|
||||
ToolAccessCheckEvent,
|
||||
ToolInvocationEvent,
|
||||
ToolResultEvent,
|
||||
UiFeatureAccessCheckEvent,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..tool.models import ToolCall, ToolContext, ToolResult
|
||||
from ..user.models import User
|
||||
|
||||
|
||||
class AuditLogger(ABC):
|
||||
"""Abstract base class for audit logging implementations.
|
||||
|
||||
Implementations can:
|
||||
- Write to files (JSON, CSV, etc.)
|
||||
- Send to databases (Postgres, MongoDB, etc.)
|
||||
- Stream to cloud services (CloudWatch, Datadog, etc.)
|
||||
- Send to SIEM systems (Splunk, Elastic, etc.)
|
||||
|
||||
Example:
|
||||
class PostgresAuditLogger(AuditLogger):
|
||||
async def log_event(self, event: AuditEvent) -> None:
|
||||
await self.db.execute(
|
||||
"INSERT INTO audit_log (...) VALUES (...)",
|
||||
event.model_dump()
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
llm_service=...,
|
||||
audit_logger=PostgresAuditLogger(db_pool)
|
||||
)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def log_event(self, event: AuditEvent) -> None:
|
||||
"""Log a single audit event.
|
||||
|
||||
Args:
|
||||
event: The audit event to log
|
||||
|
||||
Raises:
|
||||
Exception: If logging fails critically
|
||||
"""
|
||||
pass
|
||||
|
||||
async def log_tool_access_check(
|
||||
self,
|
||||
user: "User",
|
||||
tool_name: str,
|
||||
access_granted: bool,
|
||||
required_groups: List[str],
|
||||
context: "ToolContext",
|
||||
reason: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Convenience method for logging tool access checks.
|
||||
|
||||
Args:
|
||||
user: User attempting to access the tool
|
||||
tool_name: Name of the tool being accessed
|
||||
access_granted: Whether access was granted
|
||||
required_groups: Groups required to access the tool
|
||||
context: Tool execution context
|
||||
reason: Optional reason for denial
|
||||
"""
|
||||
event = ToolAccessCheckEvent(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
user_email=user.email,
|
||||
user_groups=user.group_memberships,
|
||||
conversation_id=context.conversation_id,
|
||||
request_id=context.request_id,
|
||||
tool_name=tool_name,
|
||||
access_granted=access_granted,
|
||||
required_groups=required_groups,
|
||||
reason=reason,
|
||||
)
|
||||
await self.log_event(event)
|
||||
|
||||
async def log_tool_invocation(
|
||||
self,
|
||||
user: "User",
|
||||
tool_call: "ToolCall",
|
||||
ui_features: List[str],
|
||||
context: "ToolContext",
|
||||
sanitize_parameters: bool = True,
|
||||
) -> None:
|
||||
"""Convenience method for logging tool invocations.
|
||||
|
||||
Args:
|
||||
user: User invoking the tool
|
||||
tool_call: Tool call information
|
||||
ui_features: List of UI features available to the user
|
||||
context: Tool execution context
|
||||
sanitize_parameters: Whether to sanitize sensitive parameters
|
||||
"""
|
||||
parameters = tool_call.arguments.copy()
|
||||
sanitized = False
|
||||
|
||||
if sanitize_parameters:
|
||||
parameters, sanitized = self._sanitize_parameters(parameters)
|
||||
|
||||
event = ToolInvocationEvent(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
user_email=user.email,
|
||||
user_groups=user.group_memberships,
|
||||
conversation_id=context.conversation_id,
|
||||
request_id=context.request_id,
|
||||
tool_call_id=tool_call.id,
|
||||
tool_name=tool_call.name,
|
||||
parameters=parameters,
|
||||
parameters_sanitized=sanitized,
|
||||
ui_features_available=ui_features,
|
||||
)
|
||||
await self.log_event(event)
|
||||
|
||||
async def log_tool_result(
|
||||
self,
|
||||
user: "User",
|
||||
tool_call: "ToolCall",
|
||||
result: "ToolResult",
|
||||
context: "ToolContext",
|
||||
) -> None:
|
||||
"""Convenience method for logging tool results.
|
||||
|
||||
Args:
|
||||
user: User who invoked the tool
|
||||
tool_call: Tool call information
|
||||
result: Tool execution result
|
||||
context: Tool execution context
|
||||
"""
|
||||
event = ToolResultEvent(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
user_email=user.email,
|
||||
user_groups=user.group_memberships,
|
||||
conversation_id=context.conversation_id,
|
||||
request_id=context.request_id,
|
||||
tool_call_id=tool_call.id,
|
||||
tool_name=tool_call.name,
|
||||
success=result.success,
|
||||
error=result.error,
|
||||
execution_time_ms=result.metadata.get("execution_time_ms", 0.0),
|
||||
result_size_bytes=(
|
||||
len(result.result_for_llm.encode("utf-8"))
|
||||
if result.result_for_llm
|
||||
else 0
|
||||
),
|
||||
ui_component_type=(
|
||||
result.ui_component.__class__.__name__ if result.ui_component else None
|
||||
),
|
||||
)
|
||||
await self.log_event(event)
|
||||
|
||||
async def log_ui_feature_access(
|
||||
self,
|
||||
user: "User",
|
||||
feature_name: str,
|
||||
access_granted: bool,
|
||||
required_groups: List[str],
|
||||
conversation_id: str,
|
||||
request_id: str,
|
||||
) -> None:
|
||||
"""Convenience method for logging UI feature access checks.
|
||||
|
||||
Args:
|
||||
user: User attempting to access the feature
|
||||
feature_name: Name of the UI feature
|
||||
access_granted: Whether access was granted
|
||||
required_groups: Groups required to access the feature
|
||||
conversation_id: Conversation identifier
|
||||
request_id: Request identifier
|
||||
"""
|
||||
event = UiFeatureAccessCheckEvent(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
user_email=user.email,
|
||||
user_groups=user.group_memberships,
|
||||
conversation_id=conversation_id,
|
||||
request_id=request_id,
|
||||
feature_name=feature_name,
|
||||
access_granted=access_granted,
|
||||
required_groups=required_groups,
|
||||
)
|
||||
await self.log_event(event)
|
||||
|
||||
async def log_ai_response(
|
||||
self,
|
||||
user: "User",
|
||||
conversation_id: str,
|
||||
request_id: str,
|
||||
response_text: str,
|
||||
tool_calls: List["ToolCall"],
|
||||
model_info: Optional[Dict[str, Any]] = None,
|
||||
include_full_text: bool = False,
|
||||
) -> None:
|
||||
"""Convenience method for logging AI responses.
|
||||
|
||||
Args:
|
||||
user: User receiving the response
|
||||
conversation_id: Conversation identifier
|
||||
request_id: Request identifier
|
||||
response_text: The AI-generated response text
|
||||
tool_calls: List of tool calls in the response
|
||||
model_info: Optional model configuration info
|
||||
include_full_text: Whether to include full response text
|
||||
"""
|
||||
response_hash = hashlib.sha256(response_text.encode("utf-8")).hexdigest()
|
||||
|
||||
event = AiResponseEvent(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
user_email=user.email,
|
||||
user_groups=user.group_memberships,
|
||||
conversation_id=conversation_id,
|
||||
request_id=request_id,
|
||||
response_length_chars=len(response_text),
|
||||
response_text=response_text if include_full_text else None,
|
||||
response_hash=response_hash,
|
||||
model_name=model_info.get("model") if model_info else None,
|
||||
temperature=model_info.get("temperature") if model_info else None,
|
||||
tool_calls_count=len(tool_calls),
|
||||
tool_names=[tc.name for tc in tool_calls],
|
||||
)
|
||||
await self.log_event(event)
|
||||
|
||||
async def query_events(
|
||||
self,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
limit: int = 100,
|
||||
) -> List[AuditEvent]:
|
||||
"""Query audit events (optional, for implementations that support it).
|
||||
|
||||
Args:
|
||||
filters: Filter criteria (user_id, event_type, etc.)
|
||||
start_time: Filter events after this time
|
||||
end_time: Filter events before this time
|
||||
limit: Maximum number of events to return
|
||||
|
||||
Returns:
|
||||
List of matching audit events
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If query not supported by implementation
|
||||
"""
|
||||
raise NotImplementedError("Query not supported by this implementation")
|
||||
|
||||
def _sanitize_parameters(
|
||||
self, parameters: Dict[str, Any]
|
||||
) -> tuple[Dict[str, Any], bool]:
|
||||
"""Sanitize sensitive data from parameters.
|
||||
|
||||
Args:
|
||||
parameters: Raw parameters dict
|
||||
|
||||
Returns:
|
||||
Tuple of (sanitized_parameters, was_sanitized)
|
||||
"""
|
||||
sanitized = parameters.copy()
|
||||
was_sanitized = False
|
||||
|
||||
# Common sensitive field patterns
|
||||
sensitive_patterns = [
|
||||
"password",
|
||||
"secret",
|
||||
"token",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"credential",
|
||||
"auth",
|
||||
"private_key",
|
||||
"access_key",
|
||||
]
|
||||
|
||||
for key in list(sanitized.keys()):
|
||||
key_lower = key.lower()
|
||||
if any(pattern in key_lower for pattern in sensitive_patterns):
|
||||
sanitized[key] = "[REDACTED]"
|
||||
was_sanitized = True
|
||||
|
||||
return sanitized, was_sanitized
|
||||
131
aivanov_project/vanna/src/vanna/core/audit/models.py
Normal file
131
aivanov_project/vanna/src/vanna/core/audit/models.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Audit event models.
|
||||
|
||||
This module contains data models for audit logging events.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .._compat import StrEnum
|
||||
|
||||
|
||||
class AuditEventType(StrEnum):
|
||||
"""Types of audit events."""
|
||||
|
||||
# Access control events
|
||||
TOOL_ACCESS_CHECK = "tool_access_check"
|
||||
UI_FEATURE_ACCESS_CHECK = "ui_feature_access_check"
|
||||
|
||||
# Tool execution events
|
||||
TOOL_INVOCATION = "tool_invocation"
|
||||
TOOL_RESULT = "tool_result"
|
||||
|
||||
# Conversation events
|
||||
MESSAGE_RECEIVED = "message_received"
|
||||
AI_RESPONSE_GENERATED = "ai_response_generated"
|
||||
CONVERSATION_CREATED = "conversation_created"
|
||||
|
||||
# Security events
|
||||
ACCESS_DENIED = "access_denied"
|
||||
AUTHENTICATION_ATTEMPT = "authentication_attempt"
|
||||
|
||||
|
||||
class AuditEvent(BaseModel):
|
||||
"""Base audit event with common fields."""
|
||||
|
||||
event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
event_type: AuditEventType
|
||||
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# User context
|
||||
user_id: str
|
||||
username: Optional[str] = None
|
||||
user_email: Optional[str] = None
|
||||
user_groups: List[str] = Field(default_factory=list)
|
||||
|
||||
# Request context
|
||||
conversation_id: str
|
||||
request_id: str
|
||||
remote_addr: Optional[str] = None
|
||||
|
||||
# Event-specific data
|
||||
details: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
# Privacy/redaction markers
|
||||
contains_pii: bool = False
|
||||
redacted_fields: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ToolAccessCheckEvent(AuditEvent):
|
||||
"""Audit event for tool access permission checks."""
|
||||
|
||||
event_type: AuditEventType = AuditEventType.TOOL_ACCESS_CHECK
|
||||
tool_name: str
|
||||
access_granted: bool
|
||||
required_groups: List[str] = Field(default_factory=list)
|
||||
reason: Optional[str] = None
|
||||
|
||||
|
||||
class ToolInvocationEvent(AuditEvent):
|
||||
"""Audit event for actual tool executions."""
|
||||
|
||||
event_type: AuditEventType = AuditEventType.TOOL_INVOCATION
|
||||
tool_call_id: str
|
||||
tool_name: str
|
||||
|
||||
# Parameters with sanitization support
|
||||
parameters: Dict[str, Any] = Field(default_factory=dict)
|
||||
parameters_sanitized: bool = False
|
||||
|
||||
# UI context at invocation time
|
||||
ui_features_available: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ToolResultEvent(AuditEvent):
|
||||
"""Audit event for tool execution results."""
|
||||
|
||||
event_type: AuditEventType = AuditEventType.TOOL_RESULT
|
||||
tool_call_id: str
|
||||
tool_name: str
|
||||
success: bool
|
||||
error: Optional[str] = None
|
||||
execution_time_ms: float = 0.0
|
||||
|
||||
# Result metadata (without full content for size)
|
||||
result_size_bytes: Optional[int] = None
|
||||
ui_component_type: Optional[str] = None
|
||||
|
||||
|
||||
class UiFeatureAccessCheckEvent(AuditEvent):
|
||||
"""Audit event for UI feature access checks."""
|
||||
|
||||
event_type: AuditEventType = AuditEventType.UI_FEATURE_ACCESS_CHECK
|
||||
feature_name: str
|
||||
access_granted: bool
|
||||
required_groups: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AiResponseEvent(AuditEvent):
|
||||
"""Audit event for AI-generated responses."""
|
||||
|
||||
event_type: AuditEventType = AuditEventType.AI_RESPONSE_GENERATED
|
||||
|
||||
# Response metadata
|
||||
response_length_chars: int
|
||||
response_length_tokens: Optional[int] = None
|
||||
|
||||
# Full text (optional, configurable)
|
||||
response_text: Optional[str] = None
|
||||
response_hash: str # SHA256 for integrity verification
|
||||
|
||||
# Model info
|
||||
model_name: Optional[str] = None
|
||||
temperature: Optional[float] = None
|
||||
|
||||
# Tool calls in response
|
||||
tool_calls_count: int = 0
|
||||
tool_names: List[str] = Field(default_factory=list)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user