Skip to content
Merged
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
64 changes: 44 additions & 20 deletions Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,32 @@ graph TB

### Conversation Persistence

**Frontend Message Type:**

The frontend uses LangChain's `HumanMessage` and `AIMessage` classes directly to keep message types consistent with the backend:

```typescript
import type { AIMessage, HumanMessage } from "@langchain/core/messages";

type TChatMessage = HumanMessage | AIMessage;
```

LangChain's `BaseMessage` exposes several accessors for message data:
- `.content` — the raw message content (`string | Array<ContentBlock>`)
- `.text` — a getter that returns `.content` as a `string` (handles content block arrays)
- `.type` — the message role (`"human"` or `"ai"`)
- `.id` — unique message identifier

When serializing messages for the backend API, the hook maps these to the format the backend expects:

```typescript
const serializedMsg = messages.map((msg) => ({
role: msg.type,
content: msg.text,
id: msg.id,
}));
```

**Session Data Structure:**

```typescript
Expand All @@ -225,7 +251,7 @@ interface TenantSessionData {
state: string; // User's state (default: "or")
messages: Array<{
// Complete conversation history
role: "user" | "ai";
role: "human" | "ai";
content: string;
}>;
Comment thread
leekahung marked this conversation as resolved.
}
Expand Down Expand Up @@ -286,60 +312,58 @@ sequenceDiagram

### Frontend Streaming Implementation

**Stream Processing** (`streamHelper.ts:15-80`):
**Stream Processing** (`streamHelper.ts`):

```typescript
async function streamText({
addMessage,
setMessages,
location,
housingLocation,
setIsLoading,
}: IStreamTextOptions) {
}: IStreamTextOptions): Promise<boolean | undefined> {
const botMessageId = (Date.now() + 1).toString();

setIsLoading?.(true);

// Add empty bot message that will be updated
setMessages((prev) => [
...prev,
{
role: "ai",
content: "",
messageId: botMessageId,
},
new AIMessage({ content: "", id: botMessageId }),
]);

try {
const reader = await addMessage({
city: location?.city,
state: location?.state || "",
city: housingLocation?.city,
state: housingLocation?.state || "",
});
if (!reader) return;
if (!reader) {
console.error("Stream reader is unavailable");
return;
}
const decoder = new TextDecoder();
let fullText = "";

while (true) {
const { done, value } = await reader.read();
if (done) break;
if (done) return true;
const chunk = decoder.decode(value);
fullText += chunk;

// Update only the bot's message
const botMessage = new AIMessage({ content: fullText, id: botMessageId });
setMessages((prev) =>
prev.map((msg) =>
msg.messageId === botMessageId ? { ...msg, content: fullText } : msg,
),
prev.map((msg) => (msg.id === botMessageId ? botMessage : msg)),
);
}
} catch (error) {
console.error("Error:", error);
setMessages((prev) =>
prev.map((msg) =>
msg.messageId === botMessageId
? {
...msg,
msg.id === botMessageId
? new AIMessage({
content: "Sorry, I encountered an error. Please try again.",
}
id: botMessageId,
})
: msg,
),
);
Expand Down
Loading
Loading