Skip to content
3 changes: 3 additions & 0 deletions apps/chatbot/app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getLanguageModel, getMemWalModel } from "@/lib/ai/providers";
import { createDocument } from "@/lib/ai/tools/create-document";
import { getWeather } from "@/lib/ai/tools/get-weather";
import { requestSuggestions } from "@/lib/ai/tools/request-suggestions";
import { saveMemory } from "@/lib/ai/tools/save-memory";
import { updateDocument } from "@/lib/ai/tools/update-document";
import { isProductionEnvironment } from "@/lib/constants";
import {
Expand Down Expand Up @@ -170,6 +171,7 @@ export async function POST(request: Request) {
"createDocument",
"updateDocument",
"requestSuggestions",
"saveMemory",
],
providerOptions: isReasoningModel
? {
Expand All @@ -183,6 +185,7 @@ export async function POST(request: Request) {
createDocument: createDocument({ session, dataStream }),
updateDocument: updateDocument({ session, dataStream }),
requestSuggestions: requestSuggestions({ session, dataStream }),
saveMemory: saveMemory({ memwalKey, memwalAccountId }),
},
experimental_telemetry: {
isEnabled: isProductionEnvironment,
Expand Down
22 changes: 21 additions & 1 deletion apps/chatbot/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ const PurePreviewMessage = ({
>
{message.role === "assistant" && (
<div className="-mt-1 flex size-8 shrink-0 items-center justify-center rounded-full bg-background ring-1 ring-border">
<SparklesIcon size={14} />
<div className={isLoading && !message.parts?.some((p) => (p.type === "text" && p.text?.trim()) || ["tool-getWeather", "tool-createDocument", "tool-updateDocument", "tool-requestSuggestions"].includes(p.type)) ? "animate-pulse" : ""}>
<SparklesIcon size={14} />
</div>
</div>
)}

Expand Down Expand Up @@ -104,6 +106,24 @@ const PurePreviewMessage = ({
</div>
)}

{/* Show "Thinking..." when assistant message is loading with no content */}
{message.role === "assistant" &&
isLoading &&
!message.parts?.some(
(p) =>
(p.type === "text" && p.text?.trim()) ||
["tool-getWeather", "tool-createDocument", "tool-updateDocument", "tool-requestSuggestions"].includes(p.type)
) && (
<div className="flex items-center gap-1 p-0 text-muted-foreground text-sm">
<span className="animate-pulse">Thinking</span>
<span className="inline-flex">
<span className="animate-bounce [animation-delay:0ms]">.</span>
<span className="animate-bounce [animation-delay:150ms]">.</span>
<span className="animate-bounce [animation-delay:300ms]">.</span>
</span>
</div>
)}

{message.parts?.map((part, index) => {
const { type } = part;
const key = `message-${message.id}-part-${index}`;
Expand Down
15 changes: 9 additions & 6 deletions apps/chatbot/components/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,15 @@ function PureMessages({
/>
))}

{status === "submitted" &&
!messages.some((msg) =>
msg.parts?.some(
(part) => "state" in part && part.state === "approval-responded"
)
) && <ThinkingMessage />}
{(status === "submitted" || status === "streaming") &&
(() => {
const lastMsg = messages[messages.length - 1];
// Show ThinkingMessage when:
// 1. Status is "submitted" (request sent, no response yet) AND last message is from user
// 2. Status is "streaming" but no assistant message exists yet
const lastIsUser = !lastMsg || lastMsg.role === "user";
return lastIsUser;
})() && <ThinkingMessage />}

<div
className="min-h-[24px] min-w-[24px] shrink-0"
Expand Down
4 changes: 2 additions & 2 deletions apps/chatbot/lib/ai/entitlements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ export const entitlementsByUserType: Record<UserType, Entitlements> = {
* For users without an account
*/
guest: {
maxMessagesPerHour: 10,
maxMessagesPerHour: 30,
},

/*
* For users with an account
*/
regular: {
maxMessagesPerHour: 10,
maxMessagesPerHour: 30,
},

/*
Expand Down
7 changes: 6 additions & 1 deletion apps/chatbot/lib/ai/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ Do not update document right after creating it. Wait for user feedback or reques

export const regularPrompt = `You are a friendly assistant! Keep your responses concise and helpful.

When asked to write, create, or help with something, just do it directly. Don't ask clarifying questions unless absolutely necessary - make reasonable assumptions and proceed with the task.`;
When asked to write, create, or help with something, just do it directly. Don't ask clarifying questions unless absolutely necessary - make reasonable assumptions and proceed with the task.

You have access to the user's personal memory system powered by MemWal. Memories are automatically recalled and injected as context during conversations.

Memory Tool:
- saveMemory({text}) - Save information to the user's personal memory on the blockchain. ONLY call this when the user EXPLICITLY asks to save or remember something. Do NOT call it proactively.`;

export type RequestHints = {
latitude: Geo["latitude"];
Expand Down
48 changes: 48 additions & 0 deletions apps/chatbot/lib/ai/tools/save-memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { tool } from "ai";
import { z } from "zod";
import { MemWal } from "@mysten-incubation/memwal";

export const saveMemory = ({
memwalKey,
memwalAccountId,
}: {
memwalKey?: string;
memwalAccountId?: string;
}) =>
tool({
description:
"Save information to the user's personal memory on the blockchain. ONLY use this tool when the user EXPLICITLY asks you to save or remember something (e.g., 'remember this', 'save this', 'lưu lại', 'nhớ giùm'). Do NOT use this tool proactively. Save the FULL, DETAILED content — do not summarize or shorten it.",
inputSchema: z.object({
text: z
.string()
.describe(
"The full, detailed text to save to memory. Include all relevant details — do not summarize."
),
}),
execute: async ({ text }) => {
const key = memwalKey || process.env.MEMWAL_KEY;
const accountId = memwalAccountId || process.env.MEMWAL_ACCOUNT_ID;
const serverUrl = process.env.MEMWAL_SERVER_URL || "http://localhost:8000";

if (!key || !accountId) {
return {
saved: false,
text,
error: "MemWal not configured — MEMWAL_KEY or MEMWAL_ACCOUNT_ID missing",
};
}

try {
const memwal = MemWal.create({ key, accountId, serverUrl });
await memwal.remember(text);
return { saved: true, text };
} catch (error) {
console.error("[Tool] saveMemory error:", error);
return {
saved: false,
text,
error: error instanceof Error ? error.message : "Failed to save memory",
};
}
},
});