diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba60a27..6a312cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,5 @@ name: CI - on: [push, pull_request] - jobs: test: runs-on: ubuntu-latest @@ -15,19 +13,13 @@ jobs: ports: - 5432:5432 options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: redis:7 ports: - 6379:6379 options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -37,7 +29,7 @@ jobs: - run: pip install -r requirements.txt - run: pip install ruff pytest pytest-asyncio pytest-cov - name: Lint - run: ruff check api/ tests/ --ignore E501 + run: ruff check api/ tests/ --ignore E501,E712 --per-file-ignores "api/models/*:F821" - name: Type check run: pip install mypy types-redis && mypy api/ --ignore-missing-imports - name: Test diff --git a/README.md b/README.md index ae64b3d..aa68a5b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ ![Next.js](https://img.shields.io/badge/Next.js-16.2-000000) ![Claude](https://img.shields.io/badge/Claude-Sonnet-blueviolet) ![CI](https://github.com/ChunkyTortoise/chatbot-widget/actions/workflows/ci.yml/badge.svg) -[![Dashboard](https://img.shields.io/badge/Dashboard-Live-brightgreen)](https://chatbot-widget-dashboard.vercel.app) # Chatbot Widget SaaS @@ -203,10 +202,9 @@ The dashboard includes a [live playground](/dashboard/app/playground/page.tsx) f *Required for production. API runs without these in development mode. -## Live Demo +## Demo (Self-Host) -- **Dashboard**: [chatbot-widget-dashboard.vercel.app](https://chatbot-widget-dashboard.vercel.app) — "Try Demo" button on the login page (no account needed) -- **Widget demo page**: `GET /demo` — portfolio-quality showcase with live widget, copy-paste embed snippet, and cold-start indicator +Screenshots below are from a local self-hosted run. See [Self-Hosting](#self-hosting) for full setup. ### Dashboard Login (with demo button) ![Dashboard Login](docs/screenshots/dashboard-login.png) @@ -214,7 +212,7 @@ The dashboard includes a [live playground](/dashboard/app/playground/page.tsx) f ### Demo Page ![Demo Page](docs/screenshots/demo-page.png) -Run locally: +Run the widget demo locally: ```bash DEMO_MODE=true uvicorn api.main:app --reload # then open http://localhost:8000/demo diff --git a/api/models/chatbot.py b/api/models/chatbot.py index 6e1d576..b481d64 100644 --- a/api/models/chatbot.py +++ b/api/models/chatbot.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid from sqlalchemy import String, Text, Boolean, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship diff --git a/api/models/conversation.py b/api/models/conversation.py index fe22bfa..b79566b 100644 --- a/api/models/conversation.py +++ b/api/models/conversation.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid from sqlalchemy import String, Integer, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship diff --git a/api/models/document_chunk.py b/api/models/document_chunk.py index f7a342f..e6a3f09 100644 --- a/api/models/document_chunk.py +++ b/api/models/document_chunk.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import uuid -from sqlalchemy import String, Text, Integer, ForeignKey, Index +from sqlalchemy import Text, Integer, ForeignKey, Index from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID from pgvector.sqlalchemy import Vector diff --git a/api/models/knowledge_doc.py b/api/models/knowledge_doc.py index 2d1f0c2..78c7d73 100644 --- a/api/models/knowledge_doc.py +++ b/api/models/knowledge_doc.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid from sqlalchemy import String, Text, Integer, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship diff --git a/api/models/message.py b/api/models/message.py index c51ce7b..4209a3e 100644 --- a/api/models/message.py +++ b/api/models/message.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import uuid from sqlalchemy import String, Text, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship diff --git a/api/models/user.py b/api/models/user.py index cfece3a..cb13cca 100644 --- a/api/models/user.py +++ b/api/models/user.py @@ -1,4 +1,5 @@ -import uuid +from __future__ import annotations + from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base, UUIDMixin, TimestampMixin diff --git a/api/routes/analytics.py b/api/routes/analytics.py index 2fc5a6a..1902e93 100644 --- a/api/routes/analytics.py +++ b/api/routes/analytics.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, func, cast, Date, text +from sqlalchemy import select, func, text from api.dependencies import get_db from api.auth.middleware import get_current_user, CurrentUser from api.models.chatbot import Chatbot diff --git a/api/routes/chatbots.py b/api/routes/chatbots.py index 37905d8..0877b6e 100644 --- a/api/routes/chatbots.py +++ b/api/routes/chatbots.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update from api.dependencies import get_db, get_admin_key, get_admin_or_owner -from api.auth.middleware import get_optional_user, get_current_user, CurrentUser +from api.auth.middleware import get_current_user, CurrentUser from api.models.chatbot import Chatbot from api.schemas.chatbot import ( ChatbotCreate, diff --git a/tests/test_analytics.py b/tests/test_analytics.py index 118100f..e17dcf9 100644 --- a/tests/test_analytics.py +++ b/tests/test_analytics.py @@ -1,6 +1,6 @@ import pytest import uuid -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock from datetime import datetime, timezone, timedelta from jose import jwt from httpx import AsyncClient, ASGITransport diff --git a/tests/test_auth.py b/tests/test_auth.py index b5f4b4c..c341403 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,6 +1,5 @@ import pytest -import uuid -from unittest.mock import patch, AsyncMock +from unittest.mock import AsyncMock from datetime import datetime, timezone, timedelta from jose import jwt from httpx import AsyncClient, ASGITransport diff --git a/tests/test_billing.py b/tests/test_billing.py index 36b8a4b..d79b536 100644 --- a/tests/test_billing.py +++ b/tests/test_billing.py @@ -6,7 +6,6 @@ from api.main import app from api.dependencies import get_db, get_redis from api.billing.quota import ( - get_message_count, increment_message_count, check_quota, get_plan_limit, diff --git a/tests/test_chat.py b/tests/test_chat.py index 642a88e..13c268f 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -223,7 +223,7 @@ async def fake_redis_get(key): mock_redis.expire = AsyncMock() with patch("api.services.chat_service.similarity_search", return_value=[]): - response = await client.post( + await client.post( f"/api/v1/chat/{chatbot_id}", json={"message": "Hello", "session_id": "test-session"}, ) diff --git a/tests/test_chatbot_quota.py b/tests/test_chatbot_quota.py index 4cb77dd..b3a0b15 100644 --- a/tests/test_chatbot_quota.py +++ b/tests/test_chatbot_quota.py @@ -1,6 +1,6 @@ import pytest import uuid -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock from datetime import datetime, timezone, timedelta from jose import jwt from httpx import AsyncClient, ASGITransport diff --git a/tests/test_demo.py b/tests/test_demo.py index 048aa9e..182b42a 100644 --- a/tests/test_demo.py +++ b/tests/test_demo.py @@ -2,7 +2,6 @@ import pytest from pathlib import Path from httpx import AsyncClient -from api.main import app class TestDemoUI: @@ -104,7 +103,7 @@ async def test_chat_with_demo_id_in_demo_mode( ) -> None: """POST /chat/demo should work when DEMO_MODE=true.""" response = await demo_client.post( - f"/api/v1/chat/demo", + "/api/v1/chat/demo", json={"message": "Hello", "session_id": "test-session"}, ) # Note: This may fail if no LLM API key is configured, but the routing should work diff --git a/tests/test_documents.py b/tests/test_documents.py index 75812cb..305cbf4 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,7 +1,7 @@ import pytest import uuid from io import BytesIO -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock from datetime import datetime, timezone, timedelta from jose import jwt diff --git a/tests/test_health.py b/tests/test_health.py index 1dafa1c..7b54293 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -3,7 +3,6 @@ @pytest.mark.asyncio async def test_health_endpoint(client, mock_db, mock_redis): - from sqlalchemy import text mock_db.execute = pytest.importorskip # will be called mock_db.execute = __import__("unittest.mock", fromlist=["AsyncMock"]).AsyncMock() diff --git a/tests/test_kb_quota.py b/tests/test_kb_quota.py index f45e7fb..cc5d5b3 100644 --- a/tests/test_kb_quota.py +++ b/tests/test_kb_quota.py @@ -1,7 +1,7 @@ import pytest import uuid from unittest.mock import AsyncMock, MagicMock -from api.billing.kb_quota import check_knowledge_base_quota, KB_LIMITS, KBQuotaResult +from api.billing.kb_quota import check_knowledge_base_quota, KB_LIMITS @pytest.fixture diff --git a/tests/test_websocket.py b/tests/test_websocket.py index ef4c0ef..a25db39 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -4,7 +4,7 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch from starlette.testclient import TestClient -from fastapi import FastAPI, WebSocket, Depends +from fastapi import FastAPI from api.routes.chat import router from api.dependencies import get_db, get_redis