-
-
Notifications
You must be signed in to change notification settings - Fork 2
feat(ai): add AI chat app integrated from duyetbot-chatkit #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Reviewer's GuideThis PR folds the standalone duyetbot-chatkit project into the monorepo as a new Next.js 15 AI chat app at apps/ai. It lays out a conventional app router structure, adds core components (ChatKitPanel, sidebars, overlays), implements custom hooks for theme, script loading, session management and conversation history, introduces a registry-based client-tools system, provides a serverless create-session API route, and wires up Turborepo/Vercel deployment through updated config files. Sequence diagram for ChatKit session creation via API routesequenceDiagram
participant U as actor User
participant App as Next.js App
participant API as "create-session API route"
participant OpenAI as "OpenAI ChatKit API"
U->>App: Loads chat page
App->>API: POST /api/create-session
API->>OpenAI: POST /v1/chatkit/sessions (with workflowId, userId)
OpenAI-->>API: Returns client_secret
API-->>App: Returns client_secret
App->>ChatKit: Initializes session with client_secret
ChatKit-->>U: Shows chat interface
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
💡 Enable Vercel Agent with $100 free credit for automated AI reviews |
Summary of ChangesHello @duyet, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a new AI chat application into the monorepo, migrating a standalone project to enhance the ecosystem with an AI-powered conversational interface. The integration ensures consistency with existing development patterns and prepares the application for deployment, offering users a robust and interactive chat experience. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes - here's some feedback:
- The dozens of autogenerated README.md files in each folder will clutter the codebase—consider consolidating these into a single docs directory or generating them outside of your source tree to keep production code lean.
- The useConversationHistory hook exposes an
isReadyflag but the UI renders immediately—gate the conversation sidebar (or show a loader) untilisReadyis true to avoid flashing empty states on initial load. - The create-session API route contains a lot of inline parsing and error-extraction logic; extracting those into shared utility functions would reduce duplication and make the handler easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The dozens of autogenerated README.md files in each folder will clutter the codebase—consider consolidating these into a single docs directory or generating them outside of your source tree to keep production code lean.
- The useConversationHistory hook exposes an `isReady` flag but the UI renders immediately—gate the conversation sidebar (or show a loader) until `isReady` is true to avoid flashing empty states on initial load.
- The create-session API route contains a lot of inline parsing and error-extraction logic; extracting those into shared utility functions would reduce duplication and make the handler easier to maintain.
## Individual Comments
### Comment 1
<location> `apps/ai/hooks/useConversationHistory.ts:159-78` </location>
<code_context>
+ /**
+ * Delete a conversation
+ */
+ const deleteConversation = useCallback((threadId: string) => {
+ setConversations((prev) => {
+ const filtered = prev.filter((c) => c.threadId !== threadId);
+
+ if (isDev) {
+ console.debug(
+ "[useConversationHistory] Deleted conversation:",
+ threadId,
+ );
+ }
+
+ return filtered;
+ });
+ }, []);
+
+ /**
</code_context>
<issue_to_address>
**suggestion (bug_risk):** deleteConversation could clear activeThreadId if the deleted thread is active.
Please update activeThreadId to null when the deleted thread matches the current activeThreadId to prevent UI inconsistencies.
Suggested implementation:
```typescript
/**
* Delete a conversation
*/
const deleteConversation = useCallback((threadId: string) => {
setConversations((prev) => {
const filtered = prev.filter((c) => c.threadId !== threadId);
if (isDev) {
console.debug(
"[useConversationHistory] Deleted conversation:",
threadId,
);
}
// Clear activeThreadId if the deleted thread is active
setActiveThreadId((currentActiveThreadId) =>
currentActiveThreadId === threadId ? null : currentActiveThreadId
);
return filtered;
});
}, []);
```
Make sure that `setActiveThreadId` is defined in the same scope as `deleteConversation`. If it is not, you will need to add a `useState` for `activeThreadId` and `setActiveThreadId` at the top of your hook:
```ts
const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
```
</issue_to_address>
### Comment 2
<location> `apps/ai/lib/conversation-storage.ts:57-67` </location>
<code_context>
+ return { success: true, data: [] };
+ }
+
+ // Validate each conversation object
+ const validated = parsed.filter((item): item is Conversation => {
+ return (
+ typeof item === "object" &&
</code_context>
<issue_to_address>
**suggestion:** Validation does not check for optional fields' types.
Currently, optional fields such as preview and messageCount are not type-checked during validation. Please update the validation logic to verify their types when these fields are present.
```suggestion
// Validate each conversation object
const validated = parsed.filter((item): item is Conversation => {
return (
typeof item === "object" &&
item !== null &&
typeof item.threadId === "string" &&
typeof item.title === "string" &&
typeof item.createdAt === "number" &&
typeof item.lastActivityAt === "number" &&
(
// preview is optional, but if present must be a string
!("preview" in item) || typeof item.preview === "string"
) &&
(
// messageCount is optional, but if present must be a number
!("messageCount" in item) || typeof item.messageCount === "number"
)
);
});
```
</issue_to_address>
### Comment 3
<location> `apps/ai/components/ChatKitPanel.tsx:190-199` </location>
<code_context>
+ * Handles thread changes in ChatKit
+ * Clears processed facts when switching conversations and tracks thread ID
+ */
+ const handleThreadChange = useCallback(
+ ({ threadId }: { threadId: string | null }) => {
+ processedFacts.current.clear();
+
+ // Update active thread ID in conversation history
+ setActiveThreadId(threadId);
+
+ // If this is a new thread (not from sidebar selection), add it to history
+ if (threadId && threadId !== lastThreadIdRef.current) {
+ upsertConversation(threadId, {
+ title: "New Conversation",
+ preview: undefined,
+ messageCount: 0,
+ });
+ }
+
+ lastThreadIdRef.current = threadId;
+
+ if (isDev) {
+ console.debug("[ChatKitPanel] Thread changed:", threadId);
+ }
+ },
+ [setActiveThreadId, upsertConversation],
+ );
+
</code_context>
<issue_to_address>
**suggestion:** Thread change handler may add duplicate conversations if threadId is reused.
To avoid overwriting existing conversation titles when threadId is reused, ensure the default title is only set for genuinely new threads.
Suggested implementation:
```typescript
// If this is a new thread (not from sidebar selection), add it to history
if (
threadId &&
threadId !== lastThreadIdRef.current &&
!conversations?.[threadId] // or use: !getConversation(threadId)
) {
upsertConversation(threadId, {
title: "New Conversation",
preview: undefined,
messageCount: 0,
});
}
```
If you do not have a `conversations` object/map available in this scope, you may need to:
1. Pass it in as a prop or retrieve it from context/state.
2. Alternatively, use a selector or a function like `getConversation(threadId)` to check for existence.
Adjust the existence check accordingly to fit your codebase.
</issue_to_address>
### Comment 4
<location> `apps/ai/components/ChatKitPanel.tsx:91` </location>
<code_context>
+ * />
+ * ```
+ */
+export function ChatKitPanel({
+ theme,
+ onWidgetAction,
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the ChatKitPanel's logic into a custom hook to separate integration details from presentation.
Here’s one way to pull most of the “plumbing” out of the component into a reusable hook so that your panel only deals with JSX and very high-level state. You can fold all of these callbacks + the toolContext + the ChatKit setup into a `useChatKitIntegration` hook:
```ts
// hooks/useChatKitIntegration.ts
import { useCallback, useMemo, useRef } from "react";
import { useChatKit } from "@openai/chatkit-react";
import { executeClientTool, type ClientToolContext } from "@/lib/client-tools";
import { CREATE_SESSION_ENDPOINT, WORKFLOW_ID, MODELS, PLACEHOLDER_INPUT, STARTER_PROMPTS, GREETING } from "@/lib/config";
import { useChatKitSession } from "@/hooks/useChatKitSession";
import { useChatKitScript } from "@/hooks/useChatKitScript";
import { useConversationHistory } from "@/hooks/useConversationHistory";
export function useChatKitIntegration(
theme: ColorScheme,
onWidgetAction: (act: FactAction) => Promise<void>,
onThemeRequest: (scheme: ColorScheme) => void,
onResponseEnd: () => void,
setErrorState: (upd: Partial<ErrorState>) => void,
) {
const processedFacts = useRef(new Set<string>());
const lastThreadIdRef = useRef<string | null>(null);
const { conversations, activeThreadId, setActiveThreadId, upsertConversation, deleteConversation } = useConversationHistory();
const isWorkflowConfigured = Boolean(WORKFLOW_ID && !WORKFLOW_ID.startsWith("wf_replace"));
const { isInitializing: isInitializingSession, getClientSecret } = useChatKitSession({
workflowId: WORKFLOW_ID,
endpoint: CREATE_SESSION_ENDPOINT,
onErrorUpdate: setErrorState,
isWorkflowConfigured,
});
const { isReady: isScriptReady } = useChatKitScript({
onErrorUpdate: setErrorState,
loadTimeout: 5000,
});
// client-tool context + handler
const toolContext = useMemo<ClientToolContext>(
() => ({ onThemeRequest, onWidgetAction, processedFacts: processedFacts.current, isDev: false }),
[onWidgetAction, onThemeRequest]
);
const handleClientTool = useCallback(
(invocation) => executeClientTool(invocation, toolContext),
[toolContext]
);
const handleResponseStart = useCallback(
() => setErrorState({ integration: null, retryable: false }),
[setErrorState]
);
const handleThreadChange = useCallback(
({ threadId }: { threadId: string | null }) => {
processedFacts.current.clear();
setActiveThreadId(threadId);
if (threadId && threadId !== lastThreadIdRef.current) {
upsertConversation(threadId, { title: "New Conversation", preview: undefined, messageCount: 0 });
}
lastThreadIdRef.current = threadId;
},
[setActiveThreadId, upsertConversation]
);
const handleError = useCallback(({ error }) => console.error(error), []);
const chatkit = useChatKit({
api: { getClientSecret },
theme: {
colorScheme: theme,
/* ... other theme props ... */
},
startScreen: { greeting: GREETING, prompts: STARTER_PROMPTS },
composer: { placeholder: PLACEHOLDER_INPUT, models: MODELS },
onClientTool: handleClientTool,
onResponseEnd,
onResponseStart: handleResponseStart,
onThreadChange,
onError: handleError,
});
return {
chatkit,
conversations,
activeThreadId,
deleteConversation,
setThread: chatkit.setThreadId,
isInitializingSession,
isScriptReady,
};
}
```
Then your panel collapses to:
```tsx
export function ChatKitPanel({ theme, onWidgetAction, onResponseEnd, onThemeRequest }: Props) {
const [errors, setErrors] = useState<ErrorState>(createInitialErrors());
const [widgetKey, bump] = useReducer((x) => x + 1, 0);
const setErrorState = useCallback((u: Partial<ErrorState>) => setErrors((e) => ({ ...e, ...u })), []);
const {
chatkit,
conversations,
activeThreadId,
deleteConversation,
setThread,
isInitializingSession,
isScriptReady,
} = useChatKitIntegration(theme, onWidgetAction, onThemeRequest, onResponseEnd, setErrorState);
// compute blockingError / showChatKit...
// JSX only
}
```
This reduces the 350 lines of one component into:
1. a ~120 line hook (`useChatKitIntegration.ts`)
2. a ~50 line presentational component
All feature‐parity is preserved, but the panel now only wires up state+JSX.
</issue_to_address>
### Comment 5
<location> `apps/ai/hooks/useColorScheme.ts:258` </location>
<code_context>
+ * }
+ * ```
+ */
+export function useColorScheme(
+ initialPreference: ColorSchemePreference = "system",
+): UseColorSchemeResult {
</code_context>
<issue_to_address>
**issue (complexity):** Consider splitting the large hook into three smaller, focused hooks for system detection, localStorage management, and DOM updates.
You can dramatically shrink this one‐huge hook by breaking it into three focused pieces—(1) system‐scheme detection via `useSyncExternalStore`, (2) a `useLocalStorage` hook that handles read/write + cross‐tab sync, and (3) a small “apply to DOM” hook. Then your top‐level `useColorScheme` is just wiring those together:
```ts
// hooks/usePrefersColorScheme.ts
import { useSyncExternalStore } from "react";
import { getMediaQuery, subscribeSystem } from "./mediaQueryUtils";
export function usePrefersColorScheme(): ColorScheme {
return useSyncExternalStore(
subscribeSystem,
() => (getMediaQuery()?.matches ? "dark" : "light"),
() => "light"
);
}
```
```ts
// hooks/useLocalStorage.ts
import { useState, useEffect, useCallback } from "react";
export function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, (val: T) => void] {
const [state, setState] = useState<T>(() => {
try {
const raw = window.localStorage.getItem(key);
return raw != null ? JSON.parse(raw) : defaultValue;
} catch {
return defaultValue;
}
});
// persist + cross-tab
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(state));
} catch {}
}, [key, state]);
useEffect(() => {
const onStorage = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
setState(JSON.parse(e.newValue));
}
};
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, [key]);
return [state, useCallback((v: T) => setState(v), [])];
}
```
```ts
// hooks/useApplyColorScheme.ts
import { useEffect } from "react";
import { applyDocumentScheme } from "./domUtils";
export function useApplyColorScheme(scheme: ColorScheme) {
useEffect(() => {
applyDocumentScheme(scheme);
}, [scheme]);
}
```
```ts
// hooks/useColorScheme.ts
import { usePrefersColorScheme } from "./usePrefersColorScheme";
import { useLocalStorage } from "./useLocalStorage";
import { useApplyColorScheme } from "./useApplyColorScheme";
export function useColorScheme(
initialPref: ColorSchemePreference = "system"
): UseColorSchemeResult {
const system = usePrefersColorScheme();
const [pref, setPref] = useLocalStorage<ColorSchemePreference>(
STORAGE_KEY,
initialPref
);
const scheme = pref === "system" ? system : pref;
useApplyColorScheme(scheme);
const setScheme = (s: ColorScheme) => setPref(s);
const resetPref = () => setPref("system");
return { scheme, preference: pref, setScheme, setPreference: setPref, resetPreference: resetPref };
}
```
This keeps 100% of your features, but each file only does one thing and is ~30 lines instead of 300+.
</issue_to_address>
### Comment 6
<location> `apps/ai/app/api/create-session/route.ts:24` </location>
<code_context>
+ return process.env.CHATKIT_API_BASE ?? DEFAULT_CHATKIT_BASE;
+}
+
+export async function POST(request: Request): Promise<Response> {
+ if (request.method !== "POST") {
+ return methodNotAllowedResponse();
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring to use Next.js's built-in request and response APIs to simplify JSON parsing, cookie handling, and response creation.
You can collapse almost all of the manual JSON/cookie/response plumbing by using Next.js’s built-in `NextResponse` and `NextRequest.json()` / `cookies` APIs. Below is an example sketch of how your `POST` handler could shrink by 60–70% without losing any functionality:
```ts
// app/api/create-session/route.ts
import { NextResponse, NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
if (req.method !== "POST") {
return NextResponse.json({ error: "Method Not Allowed" }, { status: 405 });
}
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!openaiApiKey) {
return NextResponse.json(
{ error: "Missing OPENAI_API_KEY environment variable" },
{ status: 500 }
);
}
// 1) parse JSON
let body: CreateSessionRequestBody;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
// 2) resolve or set cookie
const existing = req.cookies.get(SESSION_COOKIE_NAME)?.value;
const userId = existing
? existing
: crypto.randomUUID?.() ?? Math.random().toString(36).slice(2);
const res = NextResponse.next();
if (!existing) {
// 3) set cookie
res.cookies.set({
name: SESSION_COOKIE_NAME,
value: userId,
maxAge: SESSION_COOKIE_MAX_AGE,
path: "/",
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
});
}
// 4) call upstream
const workflowId = body.workflow?.id ?? body.workflowId ?? WORKFLOW_ID;
if (!workflowId) {
return NextResponse.json({ error: "Missing workflow id" }, { status: 400 });
}
const upstream = await fetch(`${getApiBase()}/v1/chatkit/sessions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${openaiApiKey}`,
"OpenAI-Beta": "chatkit_beta=v1",
...(process.env.CLOUDFLARE_AI_GATEWAY_URL && {
"cf-aig-authorization": `Bearer ${openaiApiKey}`,
}),
},
body: JSON.stringify({ workflow: { id: workflowId }, user: userId }),
});
const json = await upstream.json().catch(() => ({}));
if (!upstream.ok) {
const errorMsg = extractUpstreamError(json) ?? upstream.statusText;
return NextResponse.json(
{ error: errorMsg, details: json },
{ status: upstream.status }
);
}
// 5) success
return NextResponse.json(
{
client_secret: json.client_secret,
expires_after: json.expires_after,
},
{ status: 200 }
);
}
```
What this gives you:
- Request.json() instead of your `safeParseJson`
- `req.cookies.get` / `res.cookies.set` instead of manual cookie parsing/serializing
- `NextResponse.json` instead of rolling your own `buildJsonResponse`
- No more manual `new Response(JSON.stringify(…))`, `Headers`, or method-checks
You can safely remove `safeParseJson`, `getCookieValue`, `serializeSessionCookie`, and `buildJsonResponse` once you’ve migrated to `NextResponse`.
</issue_to_address>
### Comment 7
<location> `apps/ai/app/api/create-session/route.ts:251` </location>
<code_context>
const error = payload.error;
</code_context>
<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))
```suggestion
const {error} = payload;
```
<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.
From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>
### Comment 8
<location> `apps/ai/app/api/create-session/route.ts:265` </location>
<code_context>
const details = payload.details;
</code_context>
<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))
```suggestion
const {details} = payload;
```
<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.
From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>
### Comment 9
<location> `apps/ai/lib/errors.ts:87` </location>
<code_context>
const error = payload.error;
</code_context>
<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))
```suggestion
const {error} = payload;
```
<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.
From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>
### Comment 10
<location> `apps/ai/lib/errors.ts:102` </location>
<code_context>
const details = payload.details;
</code_context>
<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))
```suggestion
const {details} = payload;
```
<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.
From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| } | ||
|
|
||
| setIsReady(true); | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): deleteConversation could clear activeThreadId if the deleted thread is active.
Please update activeThreadId to null when the deleted thread matches the current activeThreadId to prevent UI inconsistencies.
Suggested implementation:
/**
* Delete a conversation
*/
const deleteConversation = useCallback((threadId: string) => {
setConversations((prev) => {
const filtered = prev.filter((c) => c.threadId !== threadId);
if (isDev) {
console.debug(
"[useConversationHistory] Deleted conversation:",
threadId,
);
}
// Clear activeThreadId if the deleted thread is active
setActiveThreadId((currentActiveThreadId) =>
currentActiveThreadId === threadId ? null : currentActiveThreadId
);
return filtered;
});
}, []);Make sure that setActiveThreadId is defined in the same scope as deleteConversation. If it is not, you will need to add a useState for activeThreadId and setActiveThreadId at the top of your hook:
const [activeThreadId, setActiveThreadId] = useState<string | null>(null);| // Validate each conversation object | ||
| const validated = parsed.filter((item): item is Conversation => { | ||
| return ( | ||
| typeof item === "object" && | ||
| item !== null && | ||
| typeof item.threadId === "string" && | ||
| typeof item.title === "string" && | ||
| typeof item.createdAt === "number" && | ||
| typeof item.lastActivityAt === "number" | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Validation does not check for optional fields' types.
Currently, optional fields such as preview and messageCount are not type-checked during validation. Please update the validation logic to verify their types when these fields are present.
| // Validate each conversation object | |
| const validated = parsed.filter((item): item is Conversation => { | |
| return ( | |
| typeof item === "object" && | |
| item !== null && | |
| typeof item.threadId === "string" && | |
| typeof item.title === "string" && | |
| typeof item.createdAt === "number" && | |
| typeof item.lastActivityAt === "number" | |
| ); | |
| }); | |
| // Validate each conversation object | |
| const validated = parsed.filter((item): item is Conversation => { | |
| return ( | |
| typeof item === "object" && | |
| item !== null && | |
| typeof item.threadId === "string" && | |
| typeof item.title === "string" && | |
| typeof item.createdAt === "number" && | |
| typeof item.lastActivityAt === "number" && | |
| ( | |
| // preview is optional, but if present must be a string | |
| !("preview" in item) || typeof item.preview === "string" | |
| ) && | |
| ( | |
| // messageCount is optional, but if present must be a number | |
| !("messageCount" in item) || typeof item.messageCount === "number" | |
| ) | |
| ); | |
| }); |
| const handleThreadChange = useCallback( | ||
| ({ threadId }: { threadId: string | null }) => { | ||
| processedFacts.current.clear(); | ||
|
|
||
| // Update active thread ID in conversation history | ||
| setActiveThreadId(threadId); | ||
|
|
||
| // If this is a new thread (not from sidebar selection), add it to history | ||
| if (threadId && threadId !== lastThreadIdRef.current) { | ||
| upsertConversation(threadId, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Thread change handler may add duplicate conversations if threadId is reused.
To avoid overwriting existing conversation titles when threadId is reused, ensure the default title is only set for genuinely new threads.
Suggested implementation:
// If this is a new thread (not from sidebar selection), add it to history
if (
threadId &&
threadId !== lastThreadIdRef.current &&
!conversations?.[threadId] // or use: !getConversation(threadId)
) {
upsertConversation(threadId, {
title: "New Conversation",
preview: undefined,
messageCount: 0,
});
}If you do not have a conversations object/map available in this scope, you may need to:
- Pass it in as a prop or retrieve it from context/state.
- Alternatively, use a selector or a function like
getConversation(threadId)to check for existence.
Adjust the existence check accordingly to fit your codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is an impressive pull request that successfully integrates the duyetbot-chatkit project as a new AI chat application within the monorepo. The code quality is excellent, with a well-thought-out architecture, comprehensive documentation in READMEs, and robust implementation of custom hooks and components. The use of modern React features like useSyncExternalStore for theme management and the extensible client-tool registry are particularly noteworthy. I've only found a couple of minor areas for cleanup in the API route and Next.js configuration, which are detailed in the comments. Overall, this is a fantastic addition to the project.
| if (request.method !== "POST") { | ||
| return methodNotAllowedResponse(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| webpack: (config) => { | ||
| config.resolve.alias = { | ||
| ...(config.resolve.alias ?? {}), | ||
| } | ||
| return config | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The webpack function in your next.config.ts currently performs a no-op by spreading the existing aliases into a new object. If there are no aliases to add, this entire webpack configuration block can be removed to simplify the config file. If you plan to add aliases later, consider adding a // TODO comment explaining why it's there.
Deploying duyet-blog with
|
| Latest commit: |
a8d87b2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://bb62ab04.duyet-blog.pages.dev |
| Branch Preview URL: | https://feat-add-ai-chat-app.duyet-blog.pages.dev |
Deploying duyet-insights with
|
| Latest commit: |
a8d87b2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9e76f843.duyet-insights.pages.dev |
| Branch Preview URL: | https://feat-add-ai-chat-app.duyet-insights.pages.dev |
Deploying duyet-photos with
|
| Latest commit: |
a8d87b2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://684c8d0d.duyet-photos.pages.dev |
| Branch Preview URL: | https://feat-add-ai-chat-app.duyet-photos.pages.dev |
Add new AI chat application to the monorepo as apps/ai, integrated from the standalone duyetbot-chatkit project. This app provides an AI-powered chat interface using OpenAI's ChatKit. Key features: - Next.js 15 application with React 19 - OpenAI ChatKit integration for AI chat functionality - Conversation history management with localStorage - Responsive UI with dark mode support - Client-side tools for conversation management Configuration updates: - Added AI app environment variables to turbo.json - Configured for deployment at ai.duyet.net - Updated yarn.lock with new dependencies Project structure: - app/ - Next.js app router pages and API routes - components/ - React components - hooks/ - Custom React hooks for ChatKit integration - lib/ - Utility functions and configuration Generated with Claude Code Co-Authored-By: Claude <[email protected]>
- Replace .eslintrc.js with eslint.config.mjs to match monorepo pattern - Add CLOUDFLARE_AI_GATEWAY_URL to turbo.json globalEnv - Fixes ESLint linting errors in CI Generated with Claude Code Co-Authored-By: Claude <[email protected]>
Add Cloudflare Workers deployment configuration to AI chat app: - Add wrangler.jsonc for Cloudflare Workers config - Add open-next.config.ts for OpenNext.js adapter - Add .dev.vars.example for local development env vars - Add @opennextjs/cloudflare and wrangler as optional dependencies - Add deployment scripts: preview, deploy, upload, cf-typegen The app now supports both Vercel and Cloudflare Workers deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Cloudflare Pages (cv) and Vercel (home) failures are unrelated to AI app changes. Triggering rerun to verify deployment status. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
a8d87b2 to
a8efa61
Compare
Summary
This PR integrates the standalone duyetbot-chatkit project into the monorepo as a new AI chat application at
apps/ai/. The app provides an AI-powered chat interface using OpenAI's ChatKit and will be deployed to ai.duyet.net.Changes
New Application Structure
/app- App router pages and API routes/components- React components (ChatKitPanel, ConversationList, etc.)/hooks- Custom hooks for ChatKit integration/lib- Utility functions and configuration/types- TypeScript type definitionsKey Features
Configuration Updates
NEXT_PUBLIC_DUYET_AI_URLOPENAI_API_KEYNEXT_PUBLIC_CHATKIT_WORKFLOW_IDCHATKIT_API_BASEIntegration Details
The app follows monorepo patterns:
@duyet/components,@duyet/libs, etc.)Deployment
Testing
The app has been built successfully and includes:
Notes
Generated with Claude Code
Summary by Sourcery
Integrate a new AI chat application into the monorepo under apps/ai, built with Next.js 15 and React 19, featuring an OpenAI ChatKit–powered interface with session API, custom hooks, localStorage–backed conversation history, theme management, client-side tools, and comprehensive error handling. Configure Turborepo and Vercel for deployment at ai.duyet.net.
New Features:
Enhancements:
Build:
Documentation:
Tests: