Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a7d793a
feat(ui): professional Hallmark redesign of design system + app shell
chattermate Jun 21, 2026
53cb585
Merge remote-tracking branch 'origin/main' into claude/amazing-bhabha…
chattermate Jun 21, 2026
9f53071
feat(ui): complete premium redesign across all pages
chattermate Jun 25, 2026
15e3aff
feat(ui): redesign agent list + add topbar page titles
chattermate Jun 25, 2026
5e9c569
feat(ui): fix widget-apps route title + monospace table headers
chattermate Jun 25, 2026
c7947c6
feat(ui): remove duplicate page titles, upgrade analytics + login sty…
chattermate Jun 25, 2026
302d43a
chore: update enterprise submodule pointer (subscription dark mode)
chattermate Jun 25, 2026
7842a7d
feat(ui): remove duplicate headings from Groups and Roles tabs
chattermate Jun 25, 2026
838ed41
feat(ui): stage all remaining redesign frontend changes from previous…
chattermate Jun 25, 2026
a4c4d83
feat(ui): fix all lime-button text color and legacy CSS vars across a…
chattermate Jun 25, 2026
1ef223d
feat(ui): purge all remaining orange brand colors from admin UI
chattermate Jun 25, 2026
9c3c730
feat(ui): fix white text on lime buttons + white backgrounds across a…
chattermate Jun 25, 2026
832dfdc
feat(ui): extend theme tokens — contrast inks, mode pills, tints, bub…
chattermate Jun 25, 2026
f01c2fa
feat(ui): app shell to design — collapsed sidebar, topbar, notificati…
chattermate Jun 25, 2026
3b204ff
feat(ui): agent editor header + Agent tab to design
chattermate Jun 25, 2026
4681690
fix(ui): inbox 3-way message bubbles to match design
chattermate Jun 25, 2026
f6e355f
refactor(ui): de-hardcode 31 admin components to theme tokens
chattermate Jun 25, 2026
fa5cf47
refactor(ui): de-hardcode LoginView auth page to theme tokens
chattermate Jun 25, 2026
819db6c
fix(ui): sidebar logo no longer truncates + even topbar icon spacing
chattermate Jun 25, 2026
e90a5a2
feat(ui): sidebar nav icons match design (inline stroke SVGs)
chattermate Jun 25, 2026
9e19679
feat(ui): match all agent-editor tabs to design pixel-by-pixel
chattermate Jun 25, 2026
7d7e642
fix(ui): agent header row + Knowledge 3-col table + Integrations coll…
chattermate Jun 25, 2026
03c06ae
feat(human-agents): redesign to People/Teams/Roles dashboard + team-o…
chattermate Jun 25, 2026
e32a7c7
feat(ui): match all settings pages to design (Org, User, AI Config, I…
chattermate Jun 25, 2026
a99bf32
feat(ui): org stats KPI strip + lime send button
chattermate Jun 26, 2026
3bea5e6
fix(ui): widget modal, AI config width, password align, collapsed sid…
chattermate Jun 26, 2026
25d1c48
fix(ui): organization full-width cards, danger zone, drop logo upload
chattermate Jun 26, 2026
55cef90
fix(ui): modal sizing — clean 520px, aligned content, no double padding
chattermate Jun 26, 2026
f1b9849
fix(ui): org footer sticks + remove logo; dropdown menu-item button r…
chattermate Jun 26, 2026
44894db
fix(ui): AI Agents footer sticks + compact Create Role modal
chattermate Jun 26, 2026
13788f9
fix(ui): scroll the content area, not the document — sidebar/topbar s…
chattermate Jun 26, 2026
ea3a8c5
fix(org): consistent admin count + resilient business-hours
chattermate Jun 26, 2026
e194caf
feat: onboarding wizard, system theme, light-mode accent + agent-edit…
chattermate Jun 26, 2026
7550b20
fix(ui): agent editor polish — flat panel, aligned content, preview w…
chattermate Jun 26, 2026
f0ae86f
feat(chat): customizable contact collection + premium chat designs
mickyarun Jun 29, 2026
3778dbb
chore: bump enterprise submodule (collect_email toggle + redesign views)
mickyarun Jun 29, 2026
39c93c0
chore(citations): default show_citations to false for now
mickyarun Jun 29, 2026
fa8f1ef
chore: gitignore .hallmark/ (local design-skill logs)
mickyarun Jun 29, 2026
a597daa
fix(security): use absolute-URL check instead of host substring match
chattermate Jun 29, 2026
f6a6743
fix(security): replace remaining amazonaws.com host-substring checks
chattermate Jun 29, 2026
1db53fa
Merge remote-tracking branch 'origin/main' into feature/ui-profession…
chattermate Jun 29, 2026
fe40a15
test: update view specs to redesigned markup
chattermate Jun 29, 2026
8fa65ec
chore(enterprise): bump backend submodule for zero-agent onboarding s…
chattermate Jun 29, 2026
fe33c6c
test(crawl4ai): skip live network/browser tests in CI
chattermate Jun 29, 2026
286aebc
test(crawl4ai): cover real threaded event-loop path with mocks
chattermate Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@ chattermate-cli/templates/backend/scripts/preload_models.py
chattermate-cli/templates/backend/scripts/start.sh
chattermate-cli/templates/frontend/nginx.conf
.claude/settings.local.json

# Hallmark design skill local logs
.hallmark/
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""add premium chat styles and show_citations to agent_customizations

Adds the new GLASS/TERMINAL/PLAYFUL/CALM_MINT values to the `chatstyle` enum and a
`show_citations` boolean column.

Revision ID: c1d2e3f4a5b6
Revises: add_sentiment_analysis_001
Create Date: 2026-06-26 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'c1d2e3f4a5b6'
down_revision: Union[str, None] = 'add_sentiment_analysis_001'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


NEW_CHAT_STYLES = ("GLASS", "TERMINAL", "PLAYFUL", "CALM_MINT")


def upgrade() -> None:
bind = op.get_bind()

# Add new values to the native Postgres `chatstyle` enum. ALTER TYPE ... ADD VALUE
# cannot run inside a transaction block, so use an autocommit block.
if bind.dialect.name == "postgresql":
with op.get_context().autocommit_block():
for value in NEW_CHAT_STYLES:
op.execute(f"ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS '{value}'")

# Add show_citations column (default OFF for now — citations need more work).
# Use IF NOT EXISTS on Postgres so the migration is safe to run on a DB where the
# column was already added out-of-band.
if bind.dialect.name == "postgresql":
op.execute(
"ALTER TABLE agent_customizations "
"ADD COLUMN IF NOT EXISTS show_citations boolean NOT NULL DEFAULT false"
)
else:
op.add_column(
'agent_customizations',
sa.Column('show_citations', sa.Boolean(), server_default=sa.false(), nullable=False),
)


def downgrade() -> None:
op.drop_column('agent_customizations', 'show_citations')
# Postgres cannot cleanly drop individual enum values; the added chatstyle
# values are intentionally left in place.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""add contact-collection columns (collect_email + handoff_collect_email/name)

Adds:
- agent_customizations.collect_email (default false) -> optional pre-chat email gate
- agents.handoff_collect_email (default true) -> ask email on human handoff
- agents.handoff_collect_name (default true) -> ask name on human handoff

Revision ID: d2e3f4a5b6c7
Revises: c1d2e3f4a5b6
Create Date: 2026-06-29 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'd2e3f4a5b6c7'
down_revision: Union[str, None] = 'c1d2e3f4a5b6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
bind = op.get_bind()
if bind.dialect.name == "postgresql":
# IF NOT EXISTS so the migration is safe on a DB where columns were added out-of-band
op.execute(
"ALTER TABLE agent_customizations "
"ADD COLUMN IF NOT EXISTS collect_email boolean NOT NULL DEFAULT false"
)
op.execute(
"ALTER TABLE agents "
"ADD COLUMN IF NOT EXISTS handoff_collect_email boolean NOT NULL DEFAULT true"
)
op.execute(
"ALTER TABLE agents "
"ADD COLUMN IF NOT EXISTS handoff_collect_name boolean NOT NULL DEFAULT true"
)
else:
op.add_column('agent_customizations', sa.Column('collect_email', sa.Boolean(), server_default=sa.false(), nullable=False))
op.add_column('agents', sa.Column('handoff_collect_email', sa.Boolean(), server_default=sa.true(), nullable=False))
op.add_column('agents', sa.Column('handoff_collect_name', sa.Boolean(), server_default=sa.true(), nullable=False))


def downgrade() -> None:
op.drop_column('agents', 'handoff_collect_name')
op.drop_column('agents', 'handoff_collect_email')
op.drop_column('agent_customizations', 'collect_email')
38 changes: 38 additions & 0 deletions backend/alembic/versions/e3f4a5b6c7d8_add_aurora_chat_style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""add AURORA chat style to agent_customizations

Adds the new AURORA value to the `chatstyle` enum (a dark "ask-me-anything"
design with a gradient orb avatar). The orb-vs-photo avatar choice is stored in
the existing `customization_metadata` JSON column, so no schema change is needed
for that.

Revision ID: e3f4a5b6c7d8
Revises: d2e3f4a5b6c7
Create Date: 2026-06-29 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op


# revision identifiers, used by Alembic.
revision: str = 'e3f4a5b6c7d8'
down_revision: Union[str, None] = 'd2e3f4a5b6c7'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
bind = op.get_bind()

# Add the new value to the native Postgres `chatstyle` enum. ALTER TYPE ...
# ADD VALUE cannot run inside a transaction block, so use an autocommit block.
if bind.dialect.name == "postgresql":
with op.get_context().autocommit_block():
op.execute("ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS 'AURORA'")


def downgrade() -> None:
# Postgres cannot cleanly drop individual enum values; the added chatstyle
# value is intentionally left in place.
pass
36 changes: 32 additions & 4 deletions backend/app/agents/chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,13 @@ def __init__(self, api_key: str, model_name: str = "gpt-4o-mini", model_type: st
logger.debug(f"Initializing chat agent for agent_id: {agent_id} and org_id: {org_id} and source: {source}")
tools = []
knowledge_tool_prompt = "" # Initialize to empty string

self.knowledge_tool = None # Holds the KnowledgeSearchByAgent for citation collection

if org_id and agent_id:
logger.debug(f"Initializing knowledge search tool for agent_id: {agent_id} and org_id: {org_id} and source: {source}")
knowledge_tool = KnowledgeSearchByAgent(
agent_id=agent_id, org_id=org_id, source=source)
self.knowledge_tool = knowledge_tool
tools.append(knowledge_tool)

# Base knowledge tool prompt
Expand Down Expand Up @@ -649,7 +651,11 @@ async def _handle_transfer(self, response_content: ChatResponse, session_id: str
create_ticket=False,
shopify_output=None
)


# Signal the widget to collect the visitor's contact details (handoff happened,
# whether or not a live agent was available).
updated_response.request_contact = True

# Prepare message attributes
attributes = {
"transfer_to_human": updated_response.transfer_to_human,
Expand Down Expand Up @@ -741,7 +747,11 @@ async def get_response(self, message: str, session_id: str = None, org_id: str =
"attributes": {}
})



# Reset citation collection for this turn
if self.knowledge_tool is not None:
self.knowledge_tool.collected_sources = []

# Get AI response
response = await self.agent.arun(
message=message,
Expand All @@ -752,6 +762,20 @@ async def get_response(self, message: str, session_id: str = None, org_id: str =
# Use the utility function to parse the response
response_content = parse_response_content(response)

# Attach knowledge-base citations gathered during this turn (overrides any
# value the LLM may have produced — this field is system-managed).
if self.knowledge_tool is not None and self.knowledge_tool.collected_sources:
from app.models.schemas.chat import SourceRef
response_content.sources = [
SourceRef(**s) for s in self.knowledge_tool.collected_sources
]
else:
response_content.sources = None

# request_contact is system-managed (set only by the transfer handler);
# clear anything the LLM may have produced.
response_content.request_contact = False

logger.debug(f"Response content: {response_content}")

# Enrich Shopify response with full product data from Redis
Expand Down Expand Up @@ -792,7 +816,11 @@ async def get_response(self, message: str, session_id: str = None, org_id: str =
"request_rating": response_content.request_rating,
"shopify_output": response_content.shopify_output
}


# Persist citations so reloaded history can render them too
if response_content.sources:
attributes["sources"] = [s.model_dump() for s in response_content.sources]

# Add ticket attributes if present
if response_content.create_ticket:
attributes.update({
Expand Down
15 changes: 10 additions & 5 deletions backend/app/agents/transfer_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,12 @@ async def get_transfer_response(
f"1. If within business hours ({is_business_hours}) and agents available ({available_agents} online), "
f"explain that you need to transfer to a human agent who can better assist them.\n"
f"2. If outside business hours or no agents available, if jira tool is available, "
f"create a ticket and inform that the team will contact them"
f"{' at ' + customer_email if customer_email else ''}.\n"
f"create a ticket so the team can follow up. "
f"{('Tell them the team will follow up at ' + customer_email + '. ') if customer_email else 'No email is on file. Simply reassure them that the team will follow up. Do NOT ask for an email; do NOT mention, reference, or link to any form; and never write a URL, a bracketed placeholder, or any email address. '}"
f"\n"
f"3. Keep the response professional and empathetic.\n"
f"4. Make it clear whether they should expect immediate help (transfer) "
f"or a follow-up email.\n"
f"4. Never show a placeholder or fake email address. Make it clear whether they should expect "
f"immediate help (transfer) or a follow-up.\n"
f"Generate a natural-sounding response:"
)

Expand Down Expand Up @@ -194,10 +195,14 @@ async def get_agent_availability_response(
"transfer_to_human": False
}

# Get customer email if customer_id provided
# Get customer email if customer_id provided. Treat the auto-generated anonymous
# placeholder (…@noemail.com) as "no email" so the bot never promises to follow up
# at a fake address — the handoff contact form collects a real one instead.
customer_email = None
if customer_id:
customer_email = customer_repo.get_customer_email(customer_id)
if CustomerRepository.is_placeholder_email(customer_email):
customer_email = None

# Get available users with proper session handling
available_users = []
Expand Down
8 changes: 8 additions & 0 deletions backend/app/api/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ async def create_agent(
customization=None,
transfer_to_human=agent.transfer_to_human or False,
ask_for_rating=agent.ask_for_rating or False,
handoff_collect_email=agent.handoff_collect_email,
handoff_collect_name=agent.handoff_collect_name,
enable_rate_limiting=agent.enable_rate_limiting or False,
overall_limit_per_ip=agent.overall_limit_per_ip or 100,
requests_per_sec=agent.requests_per_sec or 1.0,
Expand Down Expand Up @@ -316,6 +318,8 @@ async def update_agent(
customization=customization,
transfer_to_human=agent.transfer_to_human,
ask_for_rating=agent.ask_for_rating,
handoff_collect_email=agent.handoff_collect_email,
handoff_collect_name=agent.handoff_collect_name,
enable_rate_limiting=agent.enable_rate_limiting,
overall_limit_per_ip=agent.overall_limit_per_ip,
requests_per_sec=agent.requests_per_sec,
Expand Down Expand Up @@ -374,6 +378,8 @@ async def get_organization_agents(
organization_id=agent.organization_id,
transfer_to_human=agent.transfer_to_human or False,
ask_for_rating=agent.ask_for_rating or False,
handoff_collect_email=agent.handoff_collect_email,
handoff_collect_name=agent.handoff_collect_name,
enable_rate_limiting=agent.enable_rate_limiting or False,
overall_limit_per_ip=agent.overall_limit_per_ip or 100,
requests_per_sec=agent.requests_per_sec or 1.0,
Expand Down Expand Up @@ -568,6 +574,8 @@ async def update_agent_groups(
customization=customization,
transfer_to_human=agent.transfer_to_human,
ask_for_rating=agent.ask_for_rating,
handoff_collect_email=agent.handoff_collect_email,
handoff_collect_name=agent.handoff_collect_name,
enable_rate_limiting=agent.enable_rate_limiting,
overall_limit_per_ip=agent.overall_limit_per_ip,
requests_per_sec=agent.requests_per_sec,
Expand Down
Loading
Loading