Fix and Improve Chat sidebar and component setup on Web App (#1157)

- Set chatSidebar prompt, Setting name fields to empty str if value null
- Track if agent modified in chatSidebar to simplify code, fix looping
- Suppress spurious dark mode hydration warnings on the web app
- Set key for chatMessage parent to get UX efficiently updated by react
- Let only root next.js layout handle html, body tags, not child layouts
This commit is contained in:
Debanjum
2025-04-11 16:12:03 +05:30
committed by GitHub
11 changed files with 90 additions and 214 deletions

View File

@@ -1,8 +1,5 @@
import type { Metadata } from "next";
import { noto_sans, noto_sans_arabic } from "@/app/fonts";
import "../globals.css";
import { ContentSecurityPolicy } from "../common/layoutHelper";
import { ThemeProvider } from "../components/providers/themeProvider";
export const metadata: Metadata = {
title: "Khoj AI - Agents",
@@ -34,33 +31,10 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default function ChildLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
`,
}}
/>
</head>
<ContentSecurityPolicy />
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
return <>{children}</>;
}

View File

@@ -2,7 +2,6 @@ import type { Metadata } from "next";
import { Toaster } from "@/components/ui/toaster";
import "../globals.css";
import { ContentSecurityPolicy } from "../common/layoutHelper";
export const metadata: Metadata = {
title: "Khoj AI - Automations",
@@ -34,18 +33,15 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default function ChildLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html>
<ContentSecurityPolicy />
<body>
{children}
<Toaster />
</body>
</html>
<>
{children}
<Toaster />
</>
);
}

View File

@@ -1,8 +1,5 @@
import type { Metadata } from "next";
import { noto_sans, noto_sans_arabic } from "@/app/fonts";
import "../globals.css";
import { ContentSecurityPolicy } from "../common/layoutHelper";
import { ThemeProvider } from "../components/providers/themeProvider";
export const metadata: Metadata = {
title: "Khoj AI - Chat",
@@ -34,38 +31,19 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default function ChildLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
`,
}}
/>
</head>
<ContentSecurityPolicy />
<body>
<ThemeProvider>
{children}
<script
dangerouslySetInnerHTML={{
__html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`,
}}
/>
</ThemeProvider>
</body>
</html>
<>
{children}
<script
dangerouslySetInnerHTML={{
__html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`,
}}
/>
</>
);
}

View File

@@ -28,8 +28,7 @@ import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/h
import { Skeleton } from "@/components/ui/skeleton";
interface ModelSelectorProps extends PopoverProps {
onSelect: (model: ModelOptions, userModification: boolean) => void;
selectedModel?: string;
onSelect: (model: ModelOptions) => void;
disabled?: boolean;
initialModel?: string;
}
@@ -49,9 +48,8 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
setModels(userConfig.chat_model_options);
if (!props.initialModel) {
const selectedChatModelOption = userConfig.chat_model_options.find(model => model.id === userConfig.selected_chat_model_config);
if (!selectedChatModelOption) {
if (!selectedChatModelOption && userConfig.chat_model_options.length > 0) {
setSelectedModel(userConfig.chat_model_options[0]);
return;
} else {
setSelectedModel(selectedChatModelOption);
}
@@ -63,29 +61,10 @@ export function ModelSelector({ ...props }: ModelSelectorProps) {
}, [userConfig, props.initialModel, isLoadingUserConfig]);
useEffect(() => {
if (props.selectedModel && selectedModel && props.selectedModel !== selectedModel.name) {
const model = models.find(model => model.name === props.selectedModel);
setSelectedModel(model);
if (selectedModel && userConfig) {
props.onSelect(selectedModel);
}
else if (props.selectedModel === null && userConfig) {
const selectedChatModelOption = userConfig.chat_model_options.find(model => model.id === userConfig.selected_chat_model_config);
if (!selectedChatModelOption) {
props.onSelect(userConfig.chat_model_options[0], false);
return;
} else {
props.onSelect(selectedChatModelOption, false);
}
}
}, [props.selectedModel, models]);
useEffect(() => {
if (selectedModel) {
const userModification = selectedModel.id !== userConfig?.selected_chat_model_config;
if (props.selectedModel !== selectedModel.name) {
props.onSelect(selectedModel, userModification);
}
}
}, [selectedModel]);
}, [selectedModel, userConfig, props.onSelect]);
if (isLoadingUserConfig) {
return (

View File

@@ -28,10 +28,6 @@ interface ChatResponse {
response: ChatHistoryData;
}
interface ChatHistory {
[key: string]: string;
}
interface ChatHistoryProps {
conversationId: string;
setTitle: (title: string) => void;
@@ -368,7 +364,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
{data &&
data.chat &&
data.chat.map((chatMessage, index) => (
<>
<React.Fragment key={`chatMessage-${index}`}>
{chatMessage.trainOfThought && chatMessage.by === "khoj" && (
<TrainOfThoughtComponent
trainOfThought={chatMessage.trainOfThought?.map(
@@ -403,7 +399,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
onDeleteMessage={handleDeleteMessage}
conversationId={props.conversationId}
/>
</>
</React.Fragment>
))}
{props.incomingMessages &&
props.incomingMessages.map((message, index) => {

View File

@@ -311,12 +311,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
isLoading: authenticationLoading,
} = useAuthenticatedData();
const [customPrompt, setCustomPrompt] = useState<string | undefined>("");
const [customPrompt, setCustomPrompt] = useState<string | undefined>();
const [selectedModel, setSelectedModel] = useState<string | undefined>();
const [inputTools, setInputTools] = useState<string[] | undefined>();
const [outputModes, setOutputModes] = useState<string[] | undefined>();
const [hasModified, setHasModified] = useState<boolean>(false);
const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(agentData?.slug.toLowerCase() === "khoj" ? true : false);
const [isDefaultAgent, setIsDefaultAgent] = useState<boolean>(!agentData || agentData?.slug.toLowerCase() === "khoj");
const [displayInputTools, setDisplayInputTools] = useState<string[] | undefined>();
const [displayOutputModes, setDisplayOutputModes] = useState<string[] | undefined>();
const [isSaving, setIsSaving] = useState<boolean>(false);
@@ -325,12 +327,14 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
function setupAgentData() {
if (agentData) {
setInputTools(agentData.input_tools);
if (agentData.input_tools === undefined || agentData.input_tools.length === 0) {
setInputTools(agentConfigurationOptions?.input_tools ? Object.keys(agentConfigurationOptions.input_tools) : []);
setDisplayInputTools(agentData.input_tools);
if (agentData.input_tools === undefined) {
setDisplayInputTools(agentConfigurationOptions?.input_tools ? Object.keys(agentConfigurationOptions.input_tools) : []);
}
setOutputModes(agentData.output_modes);
if (agentData.output_modes === undefined || agentData.output_modes.length === 0) {
setOutputModes(agentConfigurationOptions?.output_modes ? Object.keys(agentConfigurationOptions.output_modes) : []);
setDisplayOutputModes(agentData.output_modes);
if (agentData.output_modes === undefined) {
setDisplayOutputModes(agentConfigurationOptions?.output_modes ? Object.keys(agentConfigurationOptions.output_modes) : []);
}
if (agentData.name.toLowerCase() === "khoj" || agentData.is_hidden === true) {
@@ -351,16 +355,30 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
useEffect(() => {
setupAgentData();
setHasModified(false);
}, [agentData]);
// Track changes to the model, prompt, input tools and output modes fields
useEffect(() => {
if (!agentData || agentDataLoading) return; // Don't compare until data is loaded
const modelChanged = !!selectedModel && selectedModel !== agentData.chat_model;
const promptChanged = !!customPrompt && customPrompt !== agentData.persona;
// Order independent check to ensure input tools or output modes haven't been changed.
const toolsChanged = JSON.stringify(inputTools?.sort() || []) !== JSON.stringify(agentData.input_tools?.sort());
const modesChanged = JSON.stringify(outputModes?.sort() || []) !== JSON.stringify(agentData.output_modes?.sort());
setHasModified(modelChanged || promptChanged || toolsChanged || modesChanged);
// Add agentDataLoading to dependencies to ensure it runs after loading finishes
}, [selectedModel, customPrompt, inputTools, outputModes, agentData, agentDataLoading]);
function isValueChecked(value: string, existingSelections: string[]): boolean {
return existingSelections.includes(value);
}
function handleCheckToggle(value: string, existingSelections: string[]): string[] {
setHasModified(true);
if (existingSelections.includes(value)) {
return existingSelections.filter((v) => v !== value);
}
@@ -370,7 +388,6 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
function handleCustomPromptChange(value: string) {
setCustomPrompt(value);
setHasModified(true);
}
function handleSave() {
@@ -430,11 +447,8 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
setHasModified(false);
}
function handleModelSelect(model: string, userModification: boolean = true) {
function handleModelSelect(model: string) {
setSelectedModel(model);
if (userModification) {
setHasModified(true);
}
}
return (
@@ -488,7 +502,7 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<SidebarMenuItem className="list-none">
<Textarea
className="w-full h-32 resize-none hover:resize-y"
value={customPrompt}
value={customPrompt || ""}
onChange={(e) => handleCustomPromptChange(e.target.value)}
readOnly={!isEditable}
disabled={!isEditable} />
@@ -514,9 +528,8 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<SidebarMenuItem key={"model"} className="list-none">
<ModelSelector
disabled={!isEditable || !isSubscribed}
onSelect={(model, userModification) => handleModelSelect(model.name, userModification)}
onSelect={(model) => handleModelSelect(model.name)}
initialModel={isDefaultAgent ? undefined : agentData?.chat_model}
selectedModel={selectedModel}
/>
</SidebarMenuItem>
</SidebarMenu>
@@ -551,8 +564,12 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<Checkbox
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, inputTools ?? [])}
onCheckedChange={() => setInputTools(handleCheckToggle(key, inputTools ?? []))}
checked={isValueChecked(key, displayInputTools ?? [])}
onCheckedChange={() => {
let updatedInputTools = handleCheckToggle(key, displayInputTools ?? [])
setInputTools(updatedInputTools);
setDisplayInputTools(updatedInputTools);
}}
disabled={!isEditable}
>
{key}
@@ -584,8 +601,12 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<Checkbox
id={key}
className={`${isEditable ? "cursor-pointer" : ""}`}
checked={isValueChecked(key, outputModes ?? [])}
onCheckedChange={() => setOutputModes(handleCheckToggle(key, outputModes ?? []))}
checked={isValueChecked(key, displayOutputModes ?? [])}
onCheckedChange={() => {
let updatedOutputModes = handleCheckToggle(key, displayOutputModes ?? [])
setOutputModes(updatedOutputModes);
setDisplayOutputModes(updatedOutputModes);
}}
disabled={!isEditable}
>
{key}
@@ -649,8 +670,8 @@ function ChatSidebarInternal({ ...props }: ChatSideBarProps) {
<AgentCreationForm
customPrompt={customPrompt}
selectedModel={selectedModel}
inputTools={inputTools ?? []}
outputModes={outputModes ?? []}
inputTools={displayInputTools ?? []}
outputModes={displayOutputModes ?? []}
/>
</SidebarMenuButton>
</SidebarMenuItem>

View File

@@ -48,7 +48,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}>
<html
lang="en"
className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}
suppressHydrationWarning
>
<head>
<script
dangerouslySetInnerHTML={{
@@ -65,9 +69,7 @@ export default function RootLayout({
</head>
<ContentSecurityPolicy />
<body>
<ThemeProvider>
{children}
</ThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);

View File

@@ -1,9 +1,6 @@
import type { Metadata } from "next";
import "../globals.css";
import { ContentSecurityPolicy } from "../common/layoutHelper";
import { Toaster } from "@/components/ui/toaster";
import { ThemeProvider } from "../components/providers/themeProvider";
export const metadata: Metadata = {
title: "Khoj AI - Search",
@@ -29,33 +26,10 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default function ChildLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
`,
}}
/>
</head>
<ContentSecurityPolicy />
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
return <>{children}</>;
}

View File

@@ -1,10 +1,7 @@
import type { Metadata } from "next";
import { noto_sans, noto_sans_arabic } from "@/app/fonts";
import "../globals.css";
import { Toaster } from "@/components/ui/toaster";
import { ContentSecurityPolicy } from "../common/layoutHelper";
import { ChatwootWidget } from "../components/chatWoot/ChatwootWidget";
import { ThemeProvider } from "../components/providers/themeProvider";
export const metadata: Metadata = {
title: "Khoj AI - Settings",
@@ -34,35 +31,16 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default function ChildLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
`,
}}
/>
</head>
<ContentSecurityPolicy />
<body>
<ThemeProvider>
{children}
<Toaster />
<ChatwootWidget />
</ThemeProvider>
</body>
</html>
<>
{children}
<Toaster />
<ChatwootWidget />
</>
);
}

View File

@@ -746,7 +746,7 @@ export default function SettingsView() {
<Input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
value={name || ""}
className="w-full border border-gray-300 rounded-lg p-4 py-6"
/>
</CardContent>

View File

@@ -1,8 +1,5 @@
import type { Metadata } from "next";
import { noto_sans, noto_sans_arabic } from "@/app/fonts";
import "../../globals.css";
import { ContentSecurityPolicy } from "@/app/common/layoutHelper";
import { ThemeProvider } from "@/app/components/providers/themeProvider";
export const metadata: Metadata = {
title: "Khoj AI - Ask Anything",
@@ -34,38 +31,19 @@ export const metadata: Metadata = {
},
};
export default function RootLayout({
export default function ChildLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
`,
}}
/>
</head>
<ContentSecurityPolicy />
<body>
<ThemeProvider>
{children}
<script
dangerouslySetInnerHTML={{
__html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`,
}}
/>
</ThemeProvider>
</body>
</html>
<>
{children}
<script
dangerouslySetInnerHTML={{
__html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`,
}}
/>
</>
);
}