Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/interface/web/app/chat/chat.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
div.main {
height: 100%;
overflow: hidden;
color: hsla(var(--foreground));
margin-left: auto;
margin-right: auto;
Expand Down Expand Up @@ -127,6 +128,7 @@ div.chatTitleWrapper {

/* Print-specific styles for chat layout */
@media print {

/* Chat container adjustments */
div.main {
height: auto !important;
Expand Down
6 changes: 3 additions & 3 deletions src/interface/web/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
/>
</div>
<div
className={`${styles.inputBox} print-hidden p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto mt-auto`}
className={`${styles.inputBox} print-hidden p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-2xl md:rounded-xl h-fit ${chatHistoryCustomClassName} mr-auto ml-auto mt-auto mb-6 sticky bottom-3 z-50`}
>
<ChatInputArea
agentColor={agentMetadata?.color}
Expand Down Expand Up @@ -575,7 +575,7 @@ export default function Chat() {
<div className="print-hidden">
<AppSidebar conversationId={conversationId || ""} />
</div>
<SidebarInset>
<SidebarInset className="h-screen flex flex-col">
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 print-hidden">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
Expand Down Expand Up @@ -616,7 +616,7 @@ export default function Chat() {
</Button>
</div>
</header>
<div className={`${styles.main} ${styles.chatLayout}`}>
<div className={`${styles.main} ${styles.chatLayout} overflow-hidden flex-1`}>
<title>
{`${defaultTitle}${!!title && title !== defaultTitle ? `: ${title}` : ""}`}
</title>
Expand Down
43 changes: 30 additions & 13 deletions src/interface/web/app/components/appSidebar/appSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,36 @@ export function AppSidebar(props: AppSidebarProps) {
</SidebarMenuButton>
</SidebarMenuItem>
}
{items.map((item) => (
<SidebarMenuItem key={item.title} className="p-0 list-none m-0">
<SidebarMenuButton asChild>
<a
href={item.url}
className="flex items-center gap-2 no-underline"
>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
{items.map((item) => {
// Map title to shortcut label
const labelMap: Record<string, string> = {
Home: "Alt+H",
Agents: "Alt+A",
Automations: "Alt+U",
Search: "Alt+K",
Settings: "Alt+,",
};
const shortcut = labelMap[item.title];

return (
<SidebarMenuItem key={item.title} className="p-0 list-none m-0">
<SidebarMenuButton asChild>
<a
href={item.url}
className="flex items-center gap-2 no-underline justify-between w-full"
>
<span className="flex items-center gap-2">
<item.icon />
<span>{item.title}</span>
</span>
{shortcut && (
<kbd className="ml-2 rounded px-2 py-0.5 border text-xs font-mono bg-surface">{shortcut}</kbd>
)}
</a>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
<AllConversations
Expand Down
14 changes: 7 additions & 7 deletions src/interface/web/app/components/chatHistory/chatHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
instant ||
(scrollAreaEl &&
scrollAreaEl.scrollHeight - (scrollAreaEl.scrollTop + scrollAreaEl.clientHeight) <
5)
5)
) {
setIsNearBottom(true);
}
Expand Down Expand Up @@ -576,7 +576,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
<hr />
</div>

<div className={`${styles.chatHistory} ${props.customClassName}`}>
<div className={`${styles.chatHistory} h-full ${props.customClassName}`}>
<div ref={sentinelRef} style={{ height: "1px" }}>
{fetchingData && <InlineLoading className="opacity-50" />}
</div>
Expand All @@ -601,12 +601,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
index === data.chat.length - 2
? latestUserMessageRef
: // attach ref to the newest fetched message to handle scroll on fetch
// note: stabilize index selection against last page having less messages than fetchMessageCount
index ===
// note: stabilize index selection against last page having less messages than fetchMessageCount
index ===
data.chat.length -
(currentPage - 1) * fetchMessageCount
? latestFetchedMessageRef
: null
(currentPage - 1) * fetchMessageCount
? latestFetchedMessageRef
: null
}
isMobileWidth={isMobileWidth}
chatMessage={chatMessage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ div.actualInputArea {
display: grid;
grid-template-columns: auto 1fr auto auto;
}

/* Highlight when dragging files over the input area */
.dragOver {
outline: 2px dashed rgba(59, 130, 246, 0.6);
/* blue-500 with transparency */
border-radius: 8px;
}
67 changes: 63 additions & 4 deletions src/interface/web/app/components/chatInputArea/chatInputArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,15 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr

const [progressValue, setProgressValue] = useState(0);
const [isDragAndDropping, setIsDragAndDropping] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);

const [showCommandList, setShowCommandList] = useState(false);
const [useResearchMode, setUseResearchMode] = useState<boolean>(
props.isResearchModeEnabled || false,
);

const chatInputRef = ref as React.MutableRefObject<HTMLTextAreaElement>;
const commandRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!uploading) {
setProgressValue(0);
Expand Down Expand Up @@ -236,12 +238,23 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
function handleDragAndDropFiles(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
setIsDragAndDropping(false);
setIsDraggingOver(false);

if (!event.dataTransfer.files) return;

uploadFiles(event.dataTransfer.files);
}

function handlePaste(event: React.ClipboardEvent<HTMLDivElement>) {
if (!event.clipboardData) return;
const files = event.clipboardData.files;
if (files && files.length > 0) {
// Prevent default only when handling files so simple text paste remains intact
event.preventDefault();
uploadFiles(files);
}
}

function uploadFiles(files: FileList) {
if (!props.isLoggedIn) {
setLoginRedirectMessage("Please login to chat with your files");
Expand Down Expand Up @@ -321,8 +334,8 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
} catch (error) {
setError(
"Error converting files. " +
error +
". Please try again, or contact [email protected] if the issue persists.",
error +
". Please try again, or contact [email protected] if the issue persists.",
);
console.error("Error converting files:", error);
return [];
Expand Down Expand Up @@ -421,11 +434,18 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
function handleDragOver(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
setIsDragAndDropping(true);
setIsDraggingOver(true);
}

function handleDragEnter(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
setIsDraggingOver(true);
}

function handleDragLeave(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
setIsDragAndDropping(false);
setIsDraggingOver(false);
}

function removeImageUpload(index: number) {
Expand Down Expand Up @@ -514,7 +534,7 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
sideOffset={props.conversationId ? 0 : 80}
alignOffset={0}
>
<Command className="max-w-full">
<Command ref={commandRef} className="max-w-full">
<CommandInput
placeholder="Type a command or search..."
value={message}
Expand Down Expand Up @@ -643,10 +663,12 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
))}
</div>
<div
className={`${styles.actualInputArea} justify-between dark:bg-neutral-700 relative ${isDragAndDropping && "animate-pulse"}`}
className={`${styles.actualInputArea} justify-between dark:bg-neutral-700 relative ${isDragAndDropping && "animate-pulse"} ${isDraggingOver ? styles.dragOver : ""}`}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDragAndDropFiles}
onPaste={handlePaste}
>
<input
type="file"
Expand Down Expand Up @@ -689,6 +711,43 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
autoFocus={true}
value={message}
onKeyDown={(e) => {
// When the slash command menu is visible, forward ArrowUp/ArrowDown
// key events to the Command component so it can handle selection.
// We dispatch a cloned KeyboardEvent on the Command DOM node and
// then prevent the textarea from moving the caret.
if (showCommandList && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
if (commandRef.current) {
const forwardedEvent = new window.KeyboardEvent(e.type, {
key: e.key,
code: (e as any).code,
bubbles: true,
cancelable: true,
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
metaKey: e.metaKey,
});
// Dispatch the cloned event on the Command element so the
// Command component's internal handler receives it.
commandRef.current.dispatchEvent(forwardedEvent);
}
// Prevent the textarea caret from moving after dispatching.
e.preventDefault();
}

// If the slash command menu is open, Enter should select the highlighted command
if (showCommandList && e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
const selectedEl = commandRef.current?.querySelector(
'[data-selected="true"]',
) as HTMLElement | null;
if (selectedEl) {
selectedEl.click();
}
setShowCommandList(false);
return;
}

if (
e.key === "Enter" &&
!e.shiftKey &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ div.khoj div.imagesContainer {
overflow-x: hidden;
}

div.chatMessageContainer > img {
div.chatMessageContainer>img {
width: auto;
height: auto;
max-width: 100%;
Expand Down Expand Up @@ -233,6 +233,45 @@ div.trainOfThoughtElement {
align-items: start;
}

/* Ensure selection highlight is visible inside chat messages */
.chatMessage *::selection,
.chatMessage ::selection,
.chatMessage *::-moz-selection,
.chatMessage ::-moz-selection {
background: rgba(59, 130, 246, 0.35) !important;
color: inherit !important;
-webkit-text-fill-color: unset !important;
}

.dark .chatMessage *::selection,
.dark .chatMessage ::selection,
.dark .chatMessage *::-moz-selection,
.dark .chatMessage ::-moz-selection {
background: rgba(99, 102, 241, 0.45) !important;
color: inherit !important;
-webkit-text-fill-color: unset !important;
}

/* Footer controls visibility toggle to avoid DOM mount/unmount while preserving selection */
.chatFooterInner {
display: flex;
align-items: center;
justify-content: space-between;
transition: opacity 120ms ease, transform 120ms ease;
}

.hiddenControls {
opacity: 0;
pointer-events: none;
transform: translateY(4px);
}

.visibleControls {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}

div.trainOfThoughtElement ol,
div.trainOfThoughtElement ul {
margin: auto;
Expand Down
Loading