diff --git a/chatbot-api/twenty_crm.py b/chatbot-api/twenty_crm.py
index 4cc3852..3b1edfb 100644
--- a/chatbot-api/twenty_crm.py
+++ b/chatbot-api/twenty_crm.py
@@ -178,6 +178,33 @@ async def create_note(title: str, person_id: str | None = None) -> str | None:
return None
+async def create_opportunity(name: str, company_id: str | None = None, point_of_contact_id: str | None = None) -> str | None:
+ """Create an opportunity in NEW stage."""
+ if not settings.twenty_crm_api_key:
+ return None
+ try:
+ body: dict = {"name": name, "stage": "NEW"}
+ if company_id:
+ body["companyId"] = company_id
+ if point_of_contact_id:
+ body["pointOfContactId"] = point_of_contact_id
+ async with httpx.AsyncClient() as http:
+ resp = await http.post(
+ f"{BASE_URL}/opportunities",
+ headers=HEADERS,
+ json=body,
+ timeout=10,
+ )
+ if resp.status_code == 201:
+ opp_id = resp.json()["data"]["createOpportunity"]["id"]
+ logger.info(f"Twenty opportunity created: {opp_id} ({name})")
+ return opp_id
+ logger.error(f"Twenty create_opportunity: {resp.status_code} {resp.text}")
+ except Exception as e:
+ logger.error(f"Twenty create_opportunity error: {e}")
+ return None
+
+
async def create_lead_in_crm(
name: str,
email: str,
@@ -228,6 +255,11 @@ async def create_lead_in_crm(
f"Chatbot lead: {need} (page: {page_context})", person_id
)
+ # Create opportunity in NEW stage
+ if person_id:
+ opp_name = f"{name} — {need[:50]}" if need else name
+ await create_opportunity(opp_name, company_id=company_id, point_of_contact_id=person_id)
+
return person_id
diff --git a/email-api/index.mjs b/email-api/index.mjs
index 099358d..bff6b05 100644
--- a/email-api/index.mjs
+++ b/email-api/index.mjs
@@ -62,6 +62,13 @@ async function createLeadInCRM({ fullName, workEmail, companyName, jobTitle, pho
}
}
console.log(`CRM lead created: ${personId} (${fullName})`);
+ // Create opportunity in NEW stage
+ const oppBody = { name: `${fullName} — ${(need || 'Contact form').substring(0, 50)}`, stage: 'NEW' };
+ if (companyId) oppBody.companyId = companyId;
+ if (personId) oppBody.pointOfContactId = personId;
+ await fetch(`${TWENTY_CRM_URL}/rest/opportunities`, {
+ method: 'POST', headers, body: JSON.stringify(oppBody),
+ });
}
} catch (err) {
console.error('CRM error:', err.message);
diff --git a/src/components/ChatMessage.css b/src/components/ChatMessage.css
index d15eae0..f221c05 100644
--- a/src/components/ChatMessage.css
+++ b/src/components/ChatMessage.css
@@ -50,3 +50,17 @@
border-bottom-left-radius: 4px;
border-left: 2px solid var(--yellow-100);
}
+
+.chat-message__bubble a {
+ color: var(--yellow-100);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.chat-message__bubble--user a {
+ color: #fff;
+}
+
+.chat-message__bubble a:hover {
+ opacity: 0.85;
+}
diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx
index 1bea2a2..607dbb4 100644
--- a/src/components/ChatMessage.tsx
+++ b/src/components/ChatMessage.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
import type { ChatMessage as ChatMessageType } from '../types/chat';
import './ChatMessage.css';
@@ -7,10 +7,36 @@ interface ChatMessageProps {
message: ChatMessageType;
}
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function renderMarkdown(text: string): string {
+ return escapeHtml(text)
+ // Bold: **text** or __text__
+ .replace(/\*\*(.+?)\*\*/g, '$1')
+ .replace(/__(.+?)__/g, '$1')
+ // Italic: *text* or _text_ (but not inside words with underscores)
+ .replace(/(?$1')
+ .replace(/(?$1')
+ // Links: [text](url)
+ .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1')
+ // Bare URLs
+ .replace(/(?$1')
+ // Line breaks
+ .replace(/\n/g, '
');
+}
+
const ChatMessage: React.FC = ({ message }) => {
const isUser = message.sender === 'user';
const isHuman = message.sender === 'human';
+ const html = useMemo(() => renderMarkdown(message.text), [message.text]);
+
return (
= ({ message }) => {
{isHuman && message.agent && (
{message.agent}
)}
-
- {message.text}
-
+
);
};