Skip to content

Commit e5f8b9f

Browse files
committed
cleanup
1 parent d686e2d commit e5f8b9f

22 files changed

+1147
-677
lines changed

apps/api/src/routes/agent.ts

Lines changed: 139 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -5,153 +5,155 @@ import { buildAppContext, triageAgent } from "../ai";
55
import { record, setAttributes } from "../lib/tracing";
66
import { validateWebsite } from "../lib/website-utils";
77

8-
// Accept both content string and parts array formats (like Midday)
98
const AgentRequestSchema = t.Object({
10-
websiteId: t.String(),
11-
message: t.Object({
12-
id: t.Optional(t.String()),
13-
role: t.Union([t.Literal("user"), t.Literal("assistant")]),
14-
content: t.Optional(t.String()),
15-
parts: t.Optional(
16-
t.Array(
17-
t.Object({
18-
type: t.String(),
19-
text: t.Optional(t.String()),
20-
})
21-
)
22-
),
23-
text: t.Optional(t.String()), // SDK sometimes sends text directly
24-
}),
25-
id: t.Optional(t.String()),
26-
timezone: t.Optional(t.String()),
9+
websiteId: t.String(),
10+
message: t.Object({
11+
id: t.Optional(t.String()),
12+
role: t.Union([t.Literal("user"), t.Literal("assistant")]),
13+
content: t.Optional(t.String()),
14+
parts: t.Optional(
15+
t.Array(
16+
t.Object({
17+
type: t.String(),
18+
text: t.Optional(t.String()),
19+
})
20+
)
21+
),
22+
text: t.Optional(t.String()), // SDK sometimes sends text directly
23+
}),
24+
id: t.Optional(t.String()),
25+
timezone: t.Optional(t.String()),
2726
});
2827

2928
interface UIMessage {
30-
id: string;
31-
role: "user" | "assistant";
32-
parts: Array<{ type: "text"; text: string }>;
29+
id: string;
30+
role: "user" | "assistant";
31+
parts: Array<{ type: "text"; text: string }>;
3332
}
3433

3534
interface IncomingMessage {
36-
id?: string;
37-
role: "user" | "assistant";
38-
content?: string;
39-
parts?: Array<{ type: string; text?: string }>;
40-
text?: string;
35+
id?: string;
36+
role: "user" | "assistant";
37+
content?: string;
38+
parts?: Array<{ type: string; text?: string }>;
39+
text?: string;
4140
}
4241

4342
function toUIMessage(msg: IncomingMessage): UIMessage {
44-
// If already has parts, use them
45-
if (msg.parts && msg.parts.length > 0) {
46-
return {
47-
id: msg.id ?? crypto.randomUUID(),
48-
role: msg.role,
49-
parts: msg.parts.map((p) => ({ type: "text", text: p.text ?? "" })),
50-
};
51-
}
52-
53-
// Extract text from content or text field
54-
const text = msg.content ?? msg.text ?? "";
55-
56-
return {
57-
id: msg.id ?? crypto.randomUUID(),
58-
role: msg.role,
59-
parts: [{ type: "text", text }],
60-
};
43+
if (msg.parts && msg.parts.length > 0) {
44+
return {
45+
id: msg.id ?? crypto.randomUUID(),
46+
role: msg.role,
47+
parts: msg.parts.map((p) => ({ type: "text", text: p.text ?? "" })),
48+
};
49+
}
50+
51+
// Extract text from content or text field
52+
const text = msg.content ?? msg.text ?? "";
53+
54+
return {
55+
id: msg.id ?? crypto.randomUUID(),
56+
role: msg.role,
57+
parts: [{ type: "text", text }],
58+
};
6159
}
6260

6361
export const agent = new Elysia({ prefix: "/v1/agent" })
64-
.derive(async ({ request }) => {
65-
const session = await auth.api.getSession({ headers: request.headers });
66-
return { user: session?.user ?? null };
67-
})
68-
.onBeforeHandle(({ user }) => {
69-
if (!user) {
70-
return new Response(
71-
JSON.stringify({
72-
success: false,
73-
error: "Authentication required",
74-
code: "AUTH_REQUIRED",
75-
}),
76-
{ status: 401, headers: { "Content-Type": "application/json" } }
77-
);
78-
}
79-
})
80-
.post(
81-
"/chat",
82-
function agentChat({ body, user, request }) {
83-
return record("agentChat", async () => {
84-
const chatId = body.id ?? crypto.randomUUID();
85-
86-
setAttributes({
87-
"agent.website_id": body.websiteId,
88-
"agent.user_id": user?.id ?? "unknown",
89-
"agent.chat_id": chatId,
90-
});
91-
92-
try {
93-
const websiteValidation = await validateWebsite(body.websiteId);
94-
if (!(websiteValidation.success && websiteValidation.website)) {
95-
return new Response(
96-
JSON.stringify({
97-
error: websiteValidation.error ?? "Website not found",
98-
}),
99-
{ status: 404, headers: { "Content-Type": "application/json" } }
100-
);
101-
}
102-
103-
const { website } = websiteValidation;
104-
105-
let authorized = website.isPublic;
106-
if (!authorized) {
107-
if (website.organizationId) {
108-
const { success } = await websitesApi.hasPermission({
109-
headers: request.headers,
110-
body: { permissions: { website: ["read"] } },
111-
});
112-
authorized = success;
113-
} else {
114-
authorized = website.userId === user?.id;
115-
}
116-
}
117-
118-
if (!authorized) {
119-
return new Response(JSON.stringify({ error: "Unauthorized" }), {
120-
status: 403,
121-
headers: { "Content-Type": "application/json" },
122-
});
123-
}
124-
125-
const appContext = buildAppContext(
126-
user?.id ?? "anonymous",
127-
body.websiteId,
128-
website.domain ?? "unknown",
129-
body.timezone ?? "UTC"
130-
);
131-
132-
const message = toUIMessage(body.message);
133-
134-
return triageAgent.toUIMessageStream({
135-
message,
136-
strategy: "auto",
137-
maxRounds: 5,
138-
maxSteps: 20,
139-
context: appContext,
140-
experimental_transform: smoothStream({
141-
chunking: "word",
142-
}),
143-
sendSources: true,
144-
});
145-
} catch (error) {
146-
console.error("Agent chat error:", error);
147-
return new Response(
148-
JSON.stringify({
149-
error: error instanceof Error ? error.message : "Unknown error",
150-
}),
151-
{ status: 500, headers: { "Content-Type": "application/json" } }
152-
);
153-
}
154-
});
155-
},
156-
{ body: AgentRequestSchema }
157-
);
62+
.derive(async ({ request }) => {
63+
const session = await auth.api.getSession({ headers: request.headers });
64+
return { user: session?.user ?? null };
65+
})
66+
.onBeforeHandle(({ user, set }) => {
67+
if (!user) {
68+
set.status = 401;
69+
return {
70+
success: false,
71+
error: "Authentication required",
72+
code: "AUTH_REQUIRED",
73+
};
74+
}
75+
})
76+
.post(
77+
"/chat",
78+
function agentChat({ body, user, request }) {
79+
return record("agentChat", async () => {
80+
const chatId = body.id ?? crypto.randomUUID();
81+
82+
setAttributes({
83+
"agent.website_id": body.websiteId,
84+
"agent.user_id": user?.id ?? "unknown",
85+
"agent.chat_id": chatId,
86+
});
87+
88+
try {
89+
const websiteValidation = await validateWebsite(body.websiteId);
90+
if (!(websiteValidation.success && websiteValidation.website)) {
91+
return new Response(
92+
JSON.stringify({
93+
error: websiteValidation.error ?? "Website not found",
94+
}),
95+
{ status: 404, headers: { "Content-Type": "application/json" } }
96+
);
97+
}
98+
99+
const { website } = websiteValidation;
100+
101+
let authorized = website.isPublic;
102+
if (!authorized) {
103+
if (website.organizationId) {
104+
const { success } = await websitesApi.hasPermission({
105+
headers: request.headers,
106+
body: { permissions: { website: ["read"] } },
107+
});
108+
authorized = success;
109+
} else {
110+
authorized = website.userId === user?.id;
111+
}
112+
}
113+
114+
if (!authorized) {
115+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
116+
status: 403,
117+
headers: { "Content-Type": "application/json" },
118+
});
119+
}
120+
121+
const appContext = buildAppContext(
122+
user?.id ?? "anonymous",
123+
body.websiteId,
124+
website.domain ?? "unknown",
125+
body.timezone ?? "UTC"
126+
);
127+
128+
const message = toUIMessage(body.message);
129+
130+
// Update context with chatId for memory retrieval
131+
const contextWithChatId = {
132+
...appContext,
133+
chatId,
134+
};
135+
136+
return triageAgent.toUIMessageStream({
137+
message,
138+
strategy: "auto",
139+
maxRounds: 5,
140+
maxSteps: 20,
141+
context: contextWithChatId,
142+
experimental_transform: smoothStream({
143+
chunking: "word",
144+
}),
145+
sendSources: true,
146+
});
147+
} catch (error) {
148+
console.error("Agent chat error:", error);
149+
return new Response(
150+
JSON.stringify({
151+
error: error instanceof Error ? error.message : "Unknown error",
152+
}),
153+
{ status: 500, headers: { "Content-Type": "application/json" } }
154+
);
155+
}
156+
});
157+
},
158+
{ body: AgentRequestSchema }
159+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import { createContext, useContext } from "react";
4+
import { usePathname } from "next/navigation";
5+
6+
interface AgentChatContextValue {
7+
chatId: string;
8+
setChatId: (id: string) => void;
9+
}
10+
11+
const AgentChatContext = createContext<AgentChatContextValue | null>(null);
12+
13+
export function AgentChatProvider({
14+
chatId,
15+
children,
16+
}: {
17+
chatId: string;
18+
children: React.ReactNode;
19+
}) {
20+
const pathname = usePathname();
21+
22+
const setChatId = (id: string) => {
23+
const params = new URLSearchParams(window.location.search);
24+
params.set("chatId", id);
25+
window.history.pushState({}, "", `${pathname}?${params.toString()}`);
26+
};
27+
28+
return (
29+
<AgentChatContext.Provider value={{ chatId, setChatId }}>
30+
{children}
31+
</AgentChatContext.Provider>
32+
);
33+
}
34+
35+
export function useAgentChatId(): string {
36+
const context = useContext(AgentChatContext);
37+
if (!context) {
38+
throw new Error("useAgentChatId must be used within AgentChatProvider");
39+
}
40+
return context.chatId;
41+
}
42+
43+
export function useSetAgentChatId(): (id: string) => void {
44+
const context = useContext(AgentChatContext);
45+
if (!context) {
46+
throw new Error("useSetAgentChatId must be used within AgentChatProvider");
47+
}
48+
return context.setChatId;
49+
}
50+

apps/dashboard/app/(main)/websites/[id]/agent/_components/agent-input.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Input } from "@/components/ui/input";
1212
import { cn } from "@/lib/utils";
1313
import { AgentCommandMenu } from "./agent-command-menu";
1414
import { useAgentChat, useAgentCommands } from "./hooks";
15+
import { useAgentChatId, useSetAgentChatId } from "./agent-chat-context";
1516
import { RecordButton } from "./record-button";
1617

1718
export function AgentInput() {
@@ -20,10 +21,13 @@ export function AgentInput() {
2021
const { sendMessage, stop, isLoading } = useAgentChat();
2122
const { input, handleInputChange, handleKeyDown, showCommands } =
2223
useAgentCommands();
24+
const chatId = useAgentChatId();
25+
const setChatId = useSetAgentChatId();
2326

2427
const handleSubmit = (e?: React.FormEvent) => {
2528
e?.preventDefault();
2629
if (!input.trim() || isLoading) return;
30+
if (chatId) setChatId(chatId);
2731
sendMessage(input.trim());
2832
};
2933

@@ -32,10 +36,7 @@ export function AgentInput() {
3236
};
3337

3438
const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
35-
// Let command menu handle navigation keys
3639
if (handleKeyDown(e)) return;
37-
38-
// Submit on Enter (when not in command mode)
3940
if (e.key === "Enter" && !e.shiftKey && !showCommands) {
4041
e.preventDefault();
4142
handleSubmit();

0 commit comments

Comments
 (0)