Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 26 additions & 4 deletions src/client/components/ChatOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const ChatOverlay = ({ onClose }) => {
const [isNewsEnabled, setNewsEnabled] = useState(false)
const [isMapsEnabled, setMapsEnabled] = useState(false)
const [isShoppingEnabled, setShoppingEnabled] = useState(false)
const [stepEvents, setStepEvents] = useState([]) // NEW: for backend step streaming
const textareaRef = useRef(null)
const chatEndRef = useRef(null)
const abortControllerRef = useRef(null) // To abort fetch requests
Expand Down Expand Up @@ -86,6 +87,7 @@ const ChatOverlay = ({ onClose }) => {
// Add new message and create history for API call
const updatedMessages = [...messages, newUserMessage]
setMessages(updatedMessages)
setStepEvents([]) // NEW: clear step log for new session

setInput("")
if (textareaRef.current) textareaRef.current.style.height = "auto"
Expand Down Expand Up @@ -152,6 +154,10 @@ const ChatOverlay = ({ onClose }) => {
toast.error(`An error occurred: ${parsed.message}`)
continue
}
if (parsed.type === "step") {
setStepEvents((prev) => [...prev, parsed])
continue
}
if (parsed.type === "assistantStream") {
const token = parsed.token || parsed.message || ""
assistantMessageId = parsed.messageId
Expand All @@ -163,10 +169,10 @@ const ChatOverlay = ({ onClose }) => {
return prev.map((msg, index) =>
index === existingMsgIndex
? {
...msg,
content: msg.content + token
}
: msg
...msg,
content: msg.content + token
}
: msg
)
} else {
return [
Expand Down Expand Up @@ -243,6 +249,22 @@ const ChatOverlay = ({ onClose }) => {
</button>
</header>

{/* NEW: Step progress log */}
{stepEvents.length > 0 && (
<div className="p-4 border-b border-[var(--color-primary-surface-elevated)] bg-[var(--color-primary-surface)]/50">
<h4 className="text-sm font-semibold text-[var(--color-accent-blue)] mb-2">Backend Process Steps</h4>
<ul className="space-y-1 text-xs">
{stepEvents.map((step, idx) => (
<li key={idx} className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${step.status === 'done' ? 'bg-green-400' : step.status === 'in_progress' ? 'bg-yellow-400 animate-pulse' : 'bg-red-400'}`}></span>
<span className="font-medium text-[var(--color-text-primary)]">{step.step}</span>
<span className="text-[var(--color-text-secondary)]">[{new Date(step.timestamp).toLocaleTimeString()}]</span>
</li>
))}
</ul>
</div>
)}

<div
ref={scrollContainerRef}
className="flex-1 overflow-y-auto p-4 flex flex-col gap-4 custom-scrollbar"
Expand Down
108 changes: 108 additions & 0 deletions src/client/components/ToolResultCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState } from "react"
import PropTypes from "prop-types"
import { IconCalendarEvent, IconUser, IconChevronDown, IconChevronUp, IconExternalLink } from "@tabler/icons-react"

/**
* ToolResultCard Component - Displays a tool result (e.g., event details) in a styled card.
*
* @param {object} props - Component props.
* @param {string} props.title - Main title of the result (e.g., event name).
* @param {string} [props.subtitle] - Subtitle or secondary info (e.g., date/time).
* @param {object|array} [props.details] - Additional details (object or array of key-value pairs).
* @param {array} [props.attendees] - List of attendees (array of strings or objects).
* @param {string} [props.externalLink] - Optional external link for more info.
* @returns {React.ReactNode}
*/
const ToolResultCard = ({ title, subtitle, details, attendees, externalLink }) => {
const [expanded, setExpanded] = useState(false)

const handleToggle = () => setExpanded((prev) => !prev)

return (
<div className="w-full bg-[var(--color-primary-surface)] rounded-lg p-4 mb-4 border border-[var(--color-accent-blue)]">
{/* Header */}
<div className="flex items-center gap-2 mb-2">
<IconCalendarEvent className="w-6 h-6 text-[var(--color-accent-blue)]" />
<h3 className="text-lg font-semibold text-[var(--color-text-primary)]">{title}</h3>
{externalLink && (
<a
href={externalLink}
target="_blank"
rel="noopener noreferrer"
className="ml-auto text-[var(--color-accent-blue)] hover:underline flex items-center gap-1"
aria-label="Open in external app"
>
<IconExternalLink className="w-4 h-4" />
<span className="text-xs">Open</span>
</a>
)}
</div>
{subtitle && <div className="text-[var(--color-text-secondary)] text-sm mb-2">{subtitle}</div>}
{/* Expand/collapse details */}
<button
onClick={handleToggle}
className="flex items-center gap-1 text-[var(--color-accent-blue)] hover:text-white text-sm mb-2 focus:outline-none"
aria-expanded={expanded}
aria-controls="tool-result-details"
>
{expanded ? <IconChevronUp className="w-4 h-4" /> : <IconChevronDown className="w-4 h-4" />}
<span>{expanded ? "Hide details" : "Show details"}</span>
</button>
{expanded && (
<div id="tool-result-details" className="mt-2 border-t border-[var(--color-primary-surface-elevated)] pt-2">
{/* Render details as key-value pairs if object, or as list if array */}
{details && typeof details === "object" && !Array.isArray(details) && (
<ul className="mb-2">
{Object.entries(details).map(([key, value]) => (
<li key={key} className="text-[var(--color-text-primary)] text-sm flex gap-2">
<span className="font-medium">{key}:</span> <span>{String(value)}</span>
</li>
))}
</ul>
)}
{details && Array.isArray(details) && (
<ul className="mb-2">
{details.map((item, idx) => (
<li key={idx} className="text-[var(--color-text-primary)] text-sm">{String(item)}</li>
))}
</ul>
)}
{/* Attendees */}
{attendees && attendees.length > 0 && (
<div className="mt-2">
<div className="flex items-center gap-2 mb-1">
<IconUser className="w-4 h-4 text-[var(--color-accent-blue)]" />
<span className="font-medium text-[var(--color-text-primary)]">Attendees:</span>
</div>
<ul className="ml-6 list-disc">
{attendees.map((att, idx) => (
<li key={idx} className="text-[var(--color-text-secondary)] text-sm">
{typeof att === "string" ? att : att.name || JSON.stringify(att)}
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
)
}

ToolResultCard.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
details: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array
]),
attendees: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
])
),
externalLink: PropTypes.string
}

export default ToolResultCard
21 changes: 17 additions & 4 deletions src/server/main/chat/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ async def generate_chat_llm_stream(
) -> AsyncGenerator[Dict[str, Any], None]:
assistant_message_id = str(uuid.uuid4())

def step_event(step, status):
return {
"type": "step",
"step": step,
"status": status,
"timestamp": datetime.datetime.utcnow().isoformat() + "Z"
}

try:
yield step_event("Initializing chat stream", "in_progress")
# --- Construct detailed system prompt from user context ---
username = user_context.get("name", "User")
timezone_str = user_context.get("timezone", "UTC")
Expand All @@ -50,6 +59,7 @@ async def generate_chat_llm_stream(
current_user_time = datetime.datetime.now(user_timezone).strftime('%Y-%m-%d %H:%M:%S %Z')

user_profile = await db_manager.get_user_profile(user_id)
yield step_event("Fetched user profile", "done")
supermemory_user_id = user_profile.get("userData", {}).get("supermemory_user_id") if user_profile else None

active_mcp_servers = {}
Expand All @@ -60,13 +70,12 @@ async def generate_chat_llm_stream(
"transport": "sse",
"url": full_supermemory_mcp_url
}
yield step_event("Configured MCP servers", "done")

active_mcp_servers["chat_tools"] = {"url": INTEGRATIONS_CONFIG["chat_tools"]["mcp_server_config"]["url"], "headers": {"X-User-ID": user_id}}

# ADDED: Journal Server
active_mcp_servers["journal_server"] = {"url": INTEGRATIONS_CONFIG["journal"]["mcp_server_config"]["url"], "headers": {"X-User-ID": user_id}}


tool_flags = {
"internet_search": enable_internet,
"accuweather": enable_weather,
Expand All @@ -81,8 +90,9 @@ async def generate_chat_llm_stream(
if config and "mcp_server_config" in config:
mcp_config = config["mcp_server_config"]
active_mcp_servers[mcp_config["name"]] = {"url": mcp_config["url"], "headers": {"X-User-ID": user_id}}

yield step_event("Prepared tool flags and servers", "done")
tools = [{"mcpServers": active_mcp_servers}]
yield step_event("System prompt and tools ready", "done")

except Exception as e:
logger.error(f"Failed during initial setup for chat stream for user {user_id}: {e}", exc_info=True)
Expand All @@ -94,6 +104,7 @@ async def generate_chat_llm_stream(

stream_interrupted = False
try:
yield step_event("Starting LLM worker thread", "in_progress")
def worker():
try:
system_prompt = ( # noqa
Expand All @@ -103,7 +114,7 @@ def worker():
f"2. **Continuously Learn:** If you learn a new, permanent fact about the user (their preferences, relationships, personal details, goals, etc.), you MUST use the `supermemory-addToSupermemory` tool to remember it for the future. For example, if the user says 'my wife's name is Jane', you must call the tool to save this fact.\n"
f"3. **Delegate Complex Tasks:** For requests that require multiple steps, research, or actions over time (e.g., 'plan my trip', 'summarize this topic'), use the `create_task_from_description` tool. Do not try to perform complex tasks yourself.\n"
f"4. **Use Your Journal:** For daily notes, simple reminders, or retrieving information from a specific day, use the journal tools (`search_journal`, `summarize_day`, `add_journal_entry`).\n"
f"5. **Final Answer Format:** When you have a complete, final answer for the user that is not a tool call, you MUST wrap it in `<answer>` tags. For example: `<answer>The weather in London is 15°C and cloudy.</answer>`. Any self-correction or thought process should be inside `<think>` tags, which can precede the final answer.\n\n"
f"5. **Final Answer Format:** When you have a complete, final answer for the user that is not a tool call, you MUST wrap it in <answer> tags. For example: <answer>The weather in London is 15°C and cloudy.</answer>. Any self-correction or thought process should be inside <think> tags, which can precede the final answer.\n\n"
f"**User Context (for your reference):**\n"
f"- **User's Name:** {username}\n"
f"- **User's Location:** {location}\n"
Expand All @@ -124,6 +135,7 @@ def worker():

thread = threading.Thread(target=worker, daemon=True)
thread.start()
yield step_event("LLM worker thread started", "done")

last_yielded_content_str, final_history_state = "", None
while True:
Expand All @@ -143,6 +155,7 @@ def worker():
except asyncio.CancelledError:
stream_interrupted = True; raise
finally:
yield step_event("Chat stream complete", "done")
yield {"type": "assistantStream", "token": "", "done": True, "messageId": assistant_message_id}
await asyncio.to_thread(thread.join)

Expand Down
Loading