Merge pull request #877 from khoj-ai/features/fit-and-finish-new-ux

Fit and finish updates for the new UX
This commit is contained in:
sabaimran
2024-08-04 10:26:33 -07:00
committed by GitHub
33 changed files with 795 additions and 712 deletions

View File

@@ -16,16 +16,17 @@ div.agentPersonality {
overflow: hidden;
}
div.pageLayout {
max-width: 60vw;
margin: auto;
margin-bottom: 2rem;
}
div.sidePanel {
position: fixed;
height: 100%;
}
div.pageLayout {
gap: 1rem;
}
button.infoButton {
border: none;
background-color: transparent !important;
@@ -46,7 +47,7 @@ div.agentList {
}
@media only screen and (max-width: 700px) {
@media only screen and (max-width: 768px) {
div.agentList {
width: 100%;
padding: 0;
@@ -54,4 +55,13 @@ div.agentList {
margin-left: auto;
grid-template-columns: 1fr;
}
div.pageLayout {
max-width: 90vw;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View File

@@ -254,9 +254,6 @@ export default function Agents() {
if (!data) {
return (
<main className={styles.main}>
<div className={`${styles.titleBar} text-5xl`}>
Agents
</div>
<div className={styles.agentList}>
<InlineLoading /> booting up your agents
</div>
@@ -265,17 +262,8 @@ export default function Agents() {
}
return (
<main className={`${styles.main} w-full mx-auto`}>
<div className="float-right w-fit h-fit">
<NavMenu selected="Agents" />
</div>
{
showLoginPrompt &&
<LoginPrompt
loginRedirectMessage="Sign in to start chatting with a specialized agent"
onOpenChange={setShowLoginPrompt} />
}
<div className={`${styles.pageLayout} w-full mx-auto`}>
<main className={`w-full mx-auto`}>
<div className={`grid w-full mx-auto`}>
<div className={`${styles.sidePanel} top-0`}>
<SidePanel
conversationId={null}
@@ -283,9 +271,9 @@ export default function Agents() {
isMobileWidth={isMobileWidth}
/>
</div>
<div className={`mx-auto ${isMobileWidth ? "w-11/12" : "w-1/2"} pt-4`}>
<div className="pt-8 flex justify-between align-middle w-full">
<h1 className="text-3xl">Agents</h1>
<div className={`${styles.pageLayout} w-full`}>
<div className={`pt-6 md:pt-8 flex justify-between`}>
<h1 className="text-3xl flex items-center">Agents</h1>
<div className="ml-auto float-right border p-2 pt-3 rounded-xl font-bold hover:bg-stone-100 dark:hover:bg-neutral-900">
<TooltipProvider>
<Tooltip>
@@ -302,6 +290,12 @@ export default function Agents() {
</TooltipProvider>
</div>
</div>
{
showLoginPrompt &&
<LoginPrompt
loginRedirectMessage="Sign in to start chatting with a specialized agent"
onOpenChange={setShowLoginPrompt} />
}
<Alert className='bg-secondary border-none my-4'>
<AlertDescription>
<Lightning weight={'fill'} className='h-4 w-4 text-purple-400 inline' />

View File

@@ -28,4 +28,9 @@ div.sidePanel {
div.pageLayout {
max-width: 90vw;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View File

@@ -1,10 +1,9 @@
'use client'
import useSWR from 'swr';
import Loading, { InlineLoading } from '../components/loading/loading';
import { InlineLoading } from '../components/loading/loading';
import {
Card,
CardDescription,
CardContent,
CardFooter,
CardHeader,
@@ -54,7 +53,7 @@ import { useToast } from '@/components/ui/use-toast';
import { ToastAction } from '@/components/ui/toast';
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import NavMenu from '../components/navMenu/navMenu';
import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
const automationsFetcher = () => window.fetch('/api/automations').then(res => res.json()).catch(err => console.log(err));
@@ -219,6 +218,7 @@ function sendAPreview(automationId: string, setToastMessage: (toastMessage: stri
interface AutomationsCardProps {
automation: AutomationsData;
isMobileWidth: boolean;
locationData?: LocationData | null;
suggestedCard?: boolean;
setNewAutomationData?: (data: AutomationsData) => void;
@@ -260,7 +260,7 @@ function AutomationsCard(props: AutomationsCardProps) {
const dayOfMonth = getDayOfMonthFromCron(automationData.crontime);
setIntervalString(`Monthly on the ${dayOfMonth}`);
}
}, [updatedAutomationData, props.automation]);
}, [updatedAutomationData, automation]);
useEffect(() => {
@@ -275,7 +275,7 @@ function AutomationsCard(props: AutomationsCardProps) {
})
setToastMessage('');
}
}, [toastMessage]);
}, [toastMessage, updatedAutomationData, automation, toast]);
if (isDeleted) {
return null;
@@ -291,53 +291,18 @@ function AutomationsCard(props: AutomationsCardProps) {
<Button className='bg-background' variant={'ghost'}><DotsThreeVertical className='h-4 w-4' /></Button>
</PopoverTrigger>
<PopoverContent className='w-auto grid gap-2 text-left bg-secondary'>
<Button variant={'destructive'}
className='justify-start'
onClick={() => {
if (props.suggestedCard) {
setIsDeleted(true);
return;
}
deleteAutomation(automation.id.toString(), setIsDeleted);
}}>
<Trash className='h-4 w-4 mr-2' />Delete
</Button>
{
!props.suggestedCard && (
<Dialog
open={isEditing}
onOpenChange={(open) => {
setIsEditing(open);
}}
>
<DialogTrigger asChild>
<Button variant="outline" className="justify-start">
<Pencil className='h-4 w-4 mr-2' />Edit
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Edit Automation</DialogTitle>
<EditCard
authenticatedData={props.authenticatedData}
automation={automation}
setIsEditing={setIsEditing}
isLoggedIn={props.isLoggedIn}
setShowLoginPrompt={props.setShowLoginPrompt}
setUpdatedAutomationData={setUpdatedAutomationData}
locationData={props.locationData} />
</DialogContent>
</Dialog>
)
}
{
!props.suggestedCard && (
<Button variant={'outline'}
className="justify-start"
onClick={() => {
sendAPreview(automation.id.toString(), setToastMessage);
}}>
<Play className='h-4 w-4 mr-2' />Run Now
</Button>
(!props.suggestedCard && props.locationData) && (
<AutomationComponentWrapper
isMobileWidth={props.isMobileWidth}
callToAction='Edit'
createNew={false}
setIsCreating={setIsEditing}
setShowLoginPrompt={props.setShowLoginPrompt}
setNewAutomationData={setUpdatedAutomationData}
authenticatedData={props.authenticatedData}
isCreating={isEditing}
ipLocationData={props.locationData} />
)
}
<ShareLink
@@ -350,12 +315,35 @@ function AutomationsCard(props: AutomationsCardProps) {
url={createShareLink(automation)}
onShare={() => {
navigator.clipboard.writeText(createShareLink(automation));
}} />
}}
/>
{
!props.suggestedCard && (
<Button variant={'outline'}
className="justify-start"
onClick={() => {
sendAPreview(automation.id.toString(), setToastMessage);
}}>
<Play className='h-4 w-4 mr-2' />Run Now
</Button>
)
}
<Button variant={'destructive'}
className='justify-start'
onClick={() => {
if (props.suggestedCard) {
setIsDeleted(true);
return;
}
deleteAutomation(automation.id.toString(), setIsDeleted);
}}>
<Trash className='h-4 w-4 mr-2' />Delete
</Button>
</PopoverContent>
</Popover>
</CardTitle>
</CardHeader>
<CardContent className='text-secondary-foreground'>
<CardContent className='text-secondary-foreground break-all'>
{updatedAutomationData?.query_to_run || automation.query_to_run}
</CardContent>
<CardFooter className="flex flex-col items-start md:flex-row md:justify-between md:items-center gap-2">
@@ -375,31 +363,17 @@ function AutomationsCard(props: AutomationsCardProps) {
</div>
{
props.suggestedCard && props.setNewAutomationData && (
<Dialog
open={isEditing}
onOpenChange={(open) => {
setIsEditing(open);
}}
>
<DialogTrigger asChild>
<Button variant="outline">
<Plus className='h-4 w-4 mr-2' />
Add
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Add Automation</DialogTitle>
<EditCard
authenticatedData={props.authenticatedData}
createNew={true}
automation={automation}
setIsEditing={setIsEditing}
isLoggedIn={props.isLoggedIn}
setShowLoginPrompt={props.setShowLoginPrompt}
setUpdatedAutomationData={props.setNewAutomationData}
locationData={props.locationData} />
</DialogContent>
</Dialog>
<AutomationComponentWrapper
isMobileWidth={props.isMobileWidth}
callToAction='Add'
createNew={true}
setIsCreating={setIsEditing}
setShowLoginPrompt={props.setShowLoginPrompt}
setNewAutomationData={props.setNewAutomationData}
authenticatedData={props.authenticatedData}
isCreating={isEditing}
automation={automation}
ipLocationData={props.locationData} />
)
}
</CardFooter>
@@ -413,6 +387,7 @@ interface SharedAutomationCardProps {
isLoggedIn: boolean;
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
authenticatedData: UserProfile | null;
isMobileWidth: boolean;
}
function SharedAutomationCard(props: SharedAutomationCardProps) {
@@ -438,30 +413,19 @@ function SharedAutomationCard(props: SharedAutomationCardProps) {
}
return (
<Dialog
open={isCreating}
onOpenChange={(open) => {
setIsCreating(open);
}}
>
<DialogTrigger>
</DialogTrigger>
<DialogContent>
<DialogTitle>
<Plus className='h-4 w-4 mr-2' />
Create Automation
</DialogTitle>
<EditCard
authenticatedData={props.authenticatedData}
createNew={true}
setIsEditing={setIsCreating}
setUpdatedAutomationData={props.setNewAutomationData}
isLoggedIn={props.isLoggedIn}
setShowLoginPrompt={props.setShowLoginPrompt}
automation={automation}
locationData={props.locationData} />
</DialogContent>
</Dialog>
isCreating ?
<AutomationComponentWrapper
isMobileWidth={props.isMobileWidth}
callToAction='Shared'
createNew={true}
setIsCreating={setIsCreating}
setShowLoginPrompt={props.setShowLoginPrompt}
setNewAutomationData={props.setNewAutomationData}
authenticatedData={props.authenticatedData}
isCreating={isCreating}
automation={automation}
ipLocationData={props.locationData} />
: null
)
}
@@ -596,8 +560,6 @@ function AutomationModificationForm(props: AutomationModificationFormProps) {
const [isSaving, setIsSaving] = useState(false);
const { errors } = props.form.formState;
console.log(errors);
function recommendationPill(recommendationText: string, onChange: (value: any, event: React.MouseEvent<HTMLButtonElement>) => void) {
return (
<Button
@@ -867,10 +829,10 @@ function AutomationModificationForm(props: AutomationModificationFormProps) {
function metadataMap(ipLocationData: LocationData, authenticatedData: UserProfile | null) {
return (
<div className='flex flex-wrap gap-2 items-center md:justify-start justify-end'>
<div className='flex flex-wrap gap-2 items-center justify-start md:justify-end'>
{
authenticatedData ? (
<span className='rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm' ><Envelope className='h-4 w-4 mr-2 inline text-orange-500' shadow-s />{authenticatedData.email}</span>
<span className='rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm' ><Envelope className='h-4 w-4 mr-2 inline text-orange-500 shadow-sm' />{authenticatedData.email}</span>
)
: null
}
@@ -889,6 +851,82 @@ function metadataMap(ipLocationData: LocationData, authenticatedData: UserProfil
)
}
interface AutomationComponentWrapperProps {
isMobileWidth: boolean;
callToAction: string;
createNew: boolean;
setIsCreating: (completed: boolean) => void;
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
setNewAutomationData: (data: AutomationsData) => void;
authenticatedData: UserProfile | null;
isCreating: boolean;
ipLocationData: LocationData | null | undefined;
automation?: AutomationsData;
}
function AutomationComponentWrapper(props: AutomationComponentWrapperProps) {
return (
props.isMobileWidth ? (
<Drawer
open={props.isCreating}
onOpenChange={(open) => {
props.setIsCreating(open);
}}>
<DrawerTrigger asChild>
<Button
className='shadow-sm justify-start'
variant="outline">
<Plus className='h-4 w-4 mr-2' />
{props.callToAction}
</Button>
</DrawerTrigger>
<DrawerContent className='p-2'>
<DrawerTitle>Automation</DrawerTitle>
<EditCard
createNew={props.createNew}
automation={props.automation}
setIsEditing={props.setIsCreating}
isLoggedIn={props.authenticatedData ? true : false}
authenticatedData={props.authenticatedData}
setShowLoginPrompt={props.setShowLoginPrompt}
setUpdatedAutomationData={props.setNewAutomationData}
locationData={props.ipLocationData} />
</DrawerContent>
</Drawer>
) :
(
<Dialog
open={props.isCreating}
onOpenChange={(open) => {
props.setIsCreating(open);
}}
>
<DialogTrigger asChild>
<Button
className='shadow-sm justify-start'
variant="outline">
<Plus className='h-4 w-4 mr-2' />
{props.callToAction}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Automation</DialogTitle>
<EditCard
automation={props.automation}
createNew={props.createNew}
setIsEditing={props.setIsCreating}
isLoggedIn={props.authenticatedData ? true : false}
authenticatedData={props.authenticatedData}
setShowLoginPrompt={props.setShowLoginPrompt}
setUpdatedAutomationData={props.setNewAutomationData}
locationData={props.ipLocationData} />
</DialogContent>
</Dialog>
)
);
}
export default function Automations() {
const authenticatedData = useAuthenticatedData();
@@ -918,7 +956,7 @@ export default function Automations() {
setAllNewAutomations([...allNewAutomations, newAutomationData]);
setNewAutomationData(null);
}
}, [newAutomationData]);
}, [newAutomationData, allNewAutomations]);
useEffect(() => {
@@ -932,14 +970,11 @@ export default function Automations() {
}
}, [personalAutomations, allNewAutomations]);
if (error) return <div>Failed to load</div>;
if (error) return <InlineLoading message='Oops, something went wrong. Please refresh the page.' />;
return (
<main className={`${styles.main} w-full ml-auto mr-auto`}>
<div className="float-right w-fit h-fit">
<NavMenu selected="Automations" />
</div>
<div className={`grid w-full ml-auto mr-auto`}>
<main className={`w-full mx-auto`}>
<div className={`grid w-full mx-auto`}>
<div className={`${styles.sidePanel} top-0`}>
<SidePanel
conversationId={null}
@@ -948,14 +983,13 @@ export default function Automations() {
/>
</div>
<div className={`${styles.pageLayout} w-full`}>
<div className='py-4 sm:flex sm:justify-between grid gap-1'>
<h1 className="text-3xl">Automations</h1>
<div className='flex flex-wrap gap-2 items-center md:justify-start justify-end'>
<div className='pt-6 md:pt-8 grid gap-1 md:flex md:justify-between'>
<h1 className="text-3xl flex items-center">Automations</h1>
<div className='flex flex-wrap gap-2 items-center justify-start'>
{
authenticatedData ? (
<span className='rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm' ><Envelope className='h-4 w-4 mr-2 inline text-orange-500' shadow-s />{authenticatedData.email}</span>
)
: null
<span className='rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm' ><Envelope className='h-4 w-4 mr-2 inline text-orange-500 shadow-sm' />{authenticatedData.email}</span>
) : null
}
{
ipLocationData && (
@@ -965,7 +999,6 @@ export default function Automations() {
{
ipLocationData && (
<span className='rounded-lg text-sm border-secondary border p-1 flex items-center shadow-sm' ><Clock className='h-4 w-4 mr-2 inline text-green-500' />{ipLocationData ? `${ipLocationData.timezone}` : 'Unknown'}</span>
)
}
</div>
@@ -973,50 +1006,34 @@ export default function Automations() {
{
showLoginPrompt && (
<LoginPrompt
onOpenChange={setShowLoginPrompt}
loginRedirectMessage={"Create an account to make your own automation"} />
loginRedirectMessage={"Create an account to make your own automation"}
onOpenChange={setShowLoginPrompt} />
)
}
<Alert className='bg-secondary border-none'>
<Alert className='bg-secondary border-none my-4'>
<AlertDescription>
<Lightning weight={'fill'} className='h-4 w-4 text-purple-400 inline' />
<span className='font-bold'>How it works</span> Automations help you structure your time by automating tasks you do regularly. Build your own, or try out our presets. Get results straight to your inbox.
</AlertDescription>
</Alert>
<div className='flex justify-between py-4'>
<div className='flex justify-between items-center py-4'>
<h3
className="text-xl">
Your Creations
</h3>
{
authenticatedData ? (
<Dialog
open={isCreating}
onOpenChange={(open) => {
setIsCreating(open);
}}
>
<DialogTrigger asChild>
<Button
className='shadow-sm'
variant="outline">
<Plus className='h-4 w-4 mr-2' />
Create Automation
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create Automation</DialogTitle>
<EditCard
createNew={true}
setIsEditing={setIsCreating}
isLoggedIn={authenticatedData ? true : false}
authenticatedData={authenticatedData}
setShowLoginPrompt={setShowLoginPrompt}
setUpdatedAutomationData={setNewAutomationData}
locationData={ipLocationData} />
</DialogContent>
</Dialog>
)
authenticatedData ?
<AutomationComponentWrapper
isMobileWidth={isMobileWidth}
callToAction="Create Automation"
createNew={true}
setIsCreating={setIsCreating}
setShowLoginPrompt={setShowLoginPrompt}
setNewAutomationData={setNewAutomationData}
authenticatedData={authenticatedData}
isCreating={isCreating}
ipLocationData={ipLocationData}
/>
: (
<Button
className='shadow-sm'
@@ -1030,6 +1047,7 @@ export default function Automations() {
</div>
<Suspense>
<SharedAutomationCard
isMobileWidth={isMobileWidth}
authenticatedData={authenticatedData}
locationData={ipLocationData}
isLoggedIn={authenticatedData ? true : false}
@@ -1038,33 +1056,22 @@ export default function Automations() {
</Suspense>
{
((!personalAutomations || personalAutomations.length === 0) && (allNewAutomations.length == 0) && !isLoading) && (
<div>
<div className="px-4">
So empty! Create your own automation to get started.
<div className='mt-4'>
{
authenticatedData ? (
<Dialog
open={isCreating}
onOpenChange={(open) => {
setIsCreating(open);
}}
>
<DialogTrigger asChild>
<Button variant="default">Design</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create Automation</DialogTitle>
<EditCard
authenticatedData={authenticatedData}
createNew={true}
isLoggedIn={authenticatedData ? true : false}
setShowLoginPrompt={setShowLoginPrompt}
setIsEditing={setIsCreating}
setUpdatedAutomationData={setNewAutomationData}
locationData={ipLocationData} />
</DialogContent>
</Dialog>
)
authenticatedData ?
<AutomationComponentWrapper
isMobileWidth={isMobileWidth}
callToAction="Design Automation"
createNew={true}
setIsCreating={setIsCreating}
setShowLoginPrompt={setShowLoginPrompt}
setNewAutomationData={setNewAutomationData}
authenticatedData={authenticatedData}
isCreating={isCreating}
ipLocationData={ipLocationData}
/>
: (
<Button
onClick={() => setShowLoginPrompt(true)}
@@ -1087,6 +1094,7 @@ export default function Automations() {
{
personalAutomations && personalAutomations.map((automation) => (
<AutomationsCard
isMobileWidth={isMobileWidth}
key={automation.id}
authenticatedData={authenticatedData}
automation={automation}
@@ -1097,6 +1105,7 @@ export default function Automations() {
{
allNewAutomations.map((automation) => (
<AutomationsCard
isMobileWidth={isMobileWidth}
key={automation.id}
authenticatedData={authenticatedData}
automation={automation}
@@ -1115,6 +1124,7 @@ export default function Automations() {
{
suggestedAutomations.map((automation) => (
<AutomationsCard
isMobileWidth={isMobileWidth}
setNewAutomationData={setNewAutomationData}
key={automation.id}
authenticatedData={authenticatedData}

View File

@@ -1,5 +1,5 @@
div.main {
height: 100vh;
height: 100dvh;
color: hsla(var(--foreground));
}
@@ -16,11 +16,8 @@ div.main {
div.inputBox {
border: 1px solid var(--border-color);
border-radius: 16px;
margin-bottom: 20px;
gap: 12px;
padding-left: 20px;
padding-right: 20px;
align-content: center;
}
@@ -98,16 +95,6 @@ div.agentIndicator {
}
@media (max-width: 768px) {
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBox {
padding: 0;
}
}
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
@@ -117,13 +104,17 @@ div.agentIndicator {
width: 100%;
}
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBox {
padding: 0;
height: 100%;
}
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
}
}

View File

@@ -5,7 +5,6 @@ import React, { Suspense, useEffect, useState } from 'react';
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import ChatHistory from '../components/chatHistory/chatHistory';
import NavMenu from '../components/navMenu/navMenu';
import { useSearchParams } from 'next/navigation'
import Loading from '../components/loading/loading';
@@ -37,26 +36,29 @@ function ChatBodyData(props: ChatBodyDataProps) {
const [processingMessage, setProcessingMessage] = useState(false);
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
const setQueryToProcess = props.setQueryToProcess;
const onConversationIdChange = props.onConversationIdChange;
useEffect(() => {
const storedMessage = localStorage.getItem("message");
if (storedMessage) {
setProcessingMessage(true);
props.setQueryToProcess(storedMessage);
setQueryToProcess(storedMessage);
}
}, []);
}, [setQueryToProcess]);
useEffect(() => {
if (message) {
setProcessingMessage(true);
props.setQueryToProcess(message);
setQueryToProcess(message);
}
}, [message]);
}, [message, setQueryToProcess]);
useEffect(() => {
if (conversationId) {
props.onConversationIdChange?.(conversationId);
onConversationIdChange?.(conversationId);
}
}, [conversationId]);
}, [conversationId, onConversationIdChange]);
useEffect(() => {
if (props.streamedMessages &&
@@ -84,7 +86,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
incomingMessages={props.streamedMessages}
/>
</div>
<div className={`${styles.inputBox} shadow-md bg-background align-middle items-center justify-center px-3 dark:bg-neutral-700 dark:border-0 dark:shadow-sm`}>
<div className={`${styles.inputBox} 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-t-2xl rounded-b-none md:rounded-xl`}>
<ChatInputArea
agentColor={agentMetadata?.color}
isLoggedIn={props.isLoggedIn}
@@ -100,9 +102,10 @@ function ChatBodyData(props: ChatBodyDataProps) {
}
export default function Chat() {
const defaultTitle = 'Khoj AI - Chat';
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState('Khoj AI - Chat');
const [title, setTitle] = useState(defaultTitle);
const [conversationId, setConversationID] = useState<string | null>(null);
const [messages, setMessages] = useState<StreamMessage[]>([]);
const [queryToProcess, setQueryToProcess] = useState<string>('');
@@ -196,7 +199,7 @@ export default function Chat() {
}
// Track context used for chat response. References are rendered at the end of the chat
({context, onlineContext} = processMessageChunk(event, currentMessage, context, onlineContext));
({ context, onlineContext } = processMessageChunk(event, currentMessage, context, onlineContext));
setMessages([...messages]);
}
@@ -227,9 +230,9 @@ export default function Chat() {
if (isLoading) return <Loading />;
return (
<div className={styles.main + " " + styles.chatLayout}>
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>
{title}
{`${defaultTitle}${(!!title && title !== defaultTitle)? `: ${title}` : ''}`}
</title>
<div>
<SidePanel
@@ -239,8 +242,13 @@ export default function Chat() {
/>
</div>
<div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} />
<div className={styles.chatBoxBody}>
{
!isMobileWidth &&
<div className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}>
{title && <h2 className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}>{title}</h2>}
</div>
}
<Suspense fallback={<Loading />}>
<ChatBodyData
isLoggedIn={authenticatedData !== null}

View File

@@ -184,6 +184,20 @@ export function modifyFileFilterForConversation(
});
}
export async function createNewConversation(slug: string) {
try {
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, { method: "POST" });
if (!response.ok) throw new Error(`Failed to fetch chat sessions with status: ${response.status}`);
const data = await response.json();
const conversationID = data.conversation_id;
if (!conversationID) throw new Error("Conversation ID not found in response");
return conversationID;
} catch (error) {
console.error("Error creating new conversation:", error);
throw error;
}
}
export function uploadDataForIndexing(
files: FileList,
setWarning: (warning: string) => void,

View File

@@ -33,7 +33,7 @@ export function convertToBGGradientClass(color: string) {
export function convertToBGClass(color: string) {
if (tailwindColors.includes(color)) {
return `bg-${color}-500 dark:bg-${color}-900`;
return `bg-${color}-500 dark:bg-${color}-900 hover:bg-${color}-400 dark:hover:bg-${color}-800`;
}
return `bg-background`;
}

View File

@@ -92,7 +92,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
scrollToBottomAfterDataLoad();
}
}, [chatHistoryRef.current, data]);
}, [data, currentPage]);
useEffect(() => {
if (!hasMoreMessages || fetchingData) return;
@@ -111,7 +111,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
}
return () => observer.disconnect();
}, [sentinelRef.current, hasMoreMessages, currentPage, fetchingData]);
}, [hasMoreMessages, currentPage, fetchingData]);
useEffect(() => {
setHasMoreMessages(true);
@@ -160,7 +160,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
return () => observer.disconnect();
}, []);
const fetchMoreMessages = (currentPage: number) => {
function fetchMoreMessages(currentPage: number) {
if (!hasMoreMessages || fetchingData) return;
const nextPage = currentPage + 1;

View File

@@ -7,8 +7,8 @@ import { Progress } from "@/components/ui/progress"
import 'katex/dist/katex.min.css';
import {
ArrowCircleUp,
ArrowRight,
ArrowUp,
Browser,
ChatsTeardrop,
GlobeSimple,
@@ -252,7 +252,7 @@ export default function ChatInputArea(props: ChatInputProps) {
startRecordingAndTranscribe();
}
}, [recording]);
}, [recording, mediaRecorder]);
const chatInputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
@@ -359,7 +359,7 @@ export default function ChatInputArea(props: ChatInputProps) {
</Popover>
</div>
}
<div className={`${styles.actualInputArea} flex items-center justify-between dark:bg-neutral-700`}>
<div className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700`}>
<input
type="file"
multiple={true}
@@ -369,10 +369,10 @@ export default function ChatInputArea(props: ChatInputProps) {
/>
<Button
variant={'ghost'}
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
className="!bg-none p-0 m-2 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
disabled={props.sendDisabled}
onClick={handleFileButtonClick}>
<Paperclip className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
<Paperclip className='w-8 h-8' />
</Button>
<div className="grid w-full gap-1.5 relative">
<Textarea
@@ -380,6 +380,7 @@ export default function ChatInputArea(props: ChatInputProps) {
className={`border-none w-full h-16 min-h-16 max-h-[128px] md:py-4 rounded-lg resize-none dark:bg-neutral-700 ${props.isMobileWidth ? 'text-md' : 'text-lg'}`}
placeholder="Type / to see a list of commands"
id="message"
autoFocus={true}
value={message}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -396,14 +397,14 @@ export default function ChatInputArea(props: ChatInputProps) {
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={'ghost'}
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
variant='default'
className={`${!recording && 'hidden'} ${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={() => {
setRecording(!recording);
}}
disabled={props.sendDisabled}
>
<Stop weight='fill' className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
<Stop weight='fill' className='w-6 h-6' />
</Button>
</TooltipTrigger>
<TooltipContent>
@@ -420,15 +421,15 @@ export default function ChatInputArea(props: ChatInputProps) {
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={'ghost'}
className="!bg-none p-1 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
variant='default'
className={`${(!message || recording) || 'hidden'} ${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={() => {
setMessage("Listening...");
setRecording(!recording);
}}
disabled={props.sendDisabled}
>
<Microphone weight='fill' className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
<Microphone weight='fill' className='w-6 h-6' />
</Button>
</TooltipTrigger>
<TooltipContent>
@@ -439,10 +440,10 @@ export default function ChatInputArea(props: ChatInputProps) {
)
}
<Button
className={`${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-0 h-auto text-3xl transition transform hover:-translate-y-1`}
className={`${(!message || recording) && 'hidden'} ${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
onClick={onSendMessage}
disabled={props.sendDisabled}>
<ArrowCircleUp className={`${props.isMobileWidth ? 'w-6 h-6' : 'w-8 h-8'}`} />
<ArrowUp className='w-6 h-6' weight='bold' />
</Button>
</div >
</>

View File

@@ -246,7 +246,7 @@ export default function ChatMessage(props: ChatMessageProps) {
// Sanitize and set the rendered markdown
setMarkdownRendered(DOMPurify.sanitize(markdownRendered));
}, [props.chatMessage.message]);
}, [props.chatMessage.message, props.chatMessage.intent]);
useEffect(() => {
if (copySuccess) {

View File

@@ -1,6 +1,6 @@
export function KhojLogoType() {
return (
<svg width="52" height="auto" viewBox="0 0 442 198" className="fill-zinc-950 dark:fill-zinc-300" xmlns="http://www.w3.org/2000/svg">
<svg width="70" height="auto" viewBox="0 0 442 198" className="fill-zinc-950 dark:fill-zinc-300" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_45_75)">
<path d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z" fill="#FAE80B" />
<path d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z" fill="#FFCC09" />

View File

@@ -74,11 +74,13 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
let userData = useAuthenticatedData();
const setModelUsed = props.setModelUsed;
useEffect(() => {
if (props.setModelUsed && selectedModel) {
props.setModelUsed(selectedModel);
if (setModelUsed && selectedModel) {
setModelUsed(selectedModel);
}
}, [selectedModel]);
}, [selectedModel, setModelUsed]);
if (!models) {
return <div>Loading...</div>;

View File

@@ -17,10 +17,6 @@ a.selected {
div.titleBar {
display: flex;
padding-left: 12px;
padding-right: 12px;
padding-top: 16px;
padding-bottom: 16px;
justify-content: space-between;
align-content: space-evenly;
align-items: start;

View File

@@ -22,36 +22,17 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Moon, Sun, UserCircle, User, Robot, MagnifyingGlass, Question, GearFine, ArrowRight } from '@phosphor-icons/react';
import { KhojLogoType } from '../logo/khogLogo';
import { Moon, Sun, UserCircle, User, Robot, MagnifyingGlass, Question, GearFine, ArrowRight, UsersFour } from '@phosphor-icons/react';
interface NavMenuProps {
selected: string;
showLogo?: boolean;
title?: string;
}
export default function NavMenu(props: NavMenuProps) {
export default function NavMenu() {
const userData = useAuthenticatedData();
const [displayTitle, setDisplayTitle] = useState<string | undefined>(props.title);
const [isMobileWidth, setIsMobileWidth] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [initialLoadDone, setInitialLoadDone] = useState(false);
useEffect(() => {
setIsMobileWidth(window.innerWidth < 768);
if (props.title) {
setDisplayTitle(props.title);
} else if (!props.title) {
setDisplayTitle(undefined);
}
}, [props.title]);
useEffect(() => {
window.addEventListener('resize', () => {
setIsMobileWidth(window.innerWidth < 768);
@@ -81,7 +62,7 @@ export default function NavMenu(props: NavMenuProps) {
useEffect(() => {
if (!initialLoadDone) return;
toggleDarkMode(darkMode);
}, [darkMode]);
}, [darkMode, initialLoadDone]);
function toggleDarkMode(darkMode: boolean) {
if (darkMode) {
@@ -94,72 +75,62 @@ export default function NavMenu(props: NavMenuProps) {
return (
<div className={styles.titleBar}>
<div className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}>
{displayTitle && <h2 className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden`} >{displayTitle}</h2>}
{
!displayTitle && props.showLogo &&
<Link href='/'>
<KhojLogoType />
</Link>
}
</div>
{
isMobileWidth ?
<DropdownMenu>
<DropdownMenuTrigger>
{
userData ?
<Avatar className="h-8 w-8">
<Avatar className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}>
<AvatarImage src={userData?.photo} alt="user profile" />
<AvatarFallback>
{userData?.username[0]}
<AvatarFallback className="bg-transparent hover:bg-muted">
{userData?.username[0].toUpperCase()}
</AvatarFallback>
</Avatar>
:
<UserCircle className="w-6 h-6" />
<UserCircle className="h-10 w-10" />
}
</DropdownMenuTrigger>
<DropdownMenuContent className='gap-2'>
<DropdownMenuItem>
<DropdownMenuItem onClick={() => setDarkMode(!darkMode)} className='w-full cursor-pointer'>
<div
onClick={() => {
setDarkMode(!darkMode)
}
}
className="flex flex-rows">
{darkMode ? <Sun className="w-6 h-6" /> : <Moon className="w-6 h-6" />}
<p className="ml-3 font-semibold">{darkMode ? 'Light Mode' : 'Dark Mode'}</p>
</div>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/agents" className="no-underline">
<Link href="/agents" className="no-underline w-full">
<div className="flex flex-rows">
<User className="w-6 h-6" />
<UsersFour className="w-6 h-6" />
<p className="ml-3 font-semibold">Agents</p>
</div>
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/automations" className="no-underline">
<Link href="/automations" className="no-underline w-full">
<div className="flex flex-rows">
<Robot className="w-6 h-6" />
<p className="ml-3 font-semibold">Automations</p>
</div>
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/search" className="no-underline">
<div className="flex flex-rows">
<MagnifyingGlass className="w-6 h-6" />
<p className="ml-3 font-semibold">Search</p>
</div>
</Link>
</DropdownMenuItem>
{
userData &&
<DropdownMenuItem>
<Link href="/search" className="no-underline w-full">
<div className="flex flex-rows">
<MagnifyingGlass className="w-6 h-6" />
<p className="ml-3 font-semibold">Search</p>
</div>
</Link>
</DropdownMenuItem>
}
<>
<DropdownMenuSeparator />
{userData &&
<DropdownMenuItem>
<Link href="/settings" className="no-underline">
<Link href="/settings" className="no-underline w-full">
<div className="flex flex-rows">
<GearFine className="w-6 h-6" />
<p className="ml-3 font-semibold">Settings</p>
@@ -168,7 +139,7 @@ export default function NavMenu(props: NavMenuProps) {
</DropdownMenuItem>
}
<DropdownMenuItem>
<Link href="https://docs.khoj.dev" className="no-underline">
<Link href="https://docs.khoj.dev" className="no-underline w-full">
<div className="flex flex-rows">
<Question className="w-6 h-6" />
<p className="ml-3 font-semibold">Help</p>
@@ -178,7 +149,7 @@ export default function NavMenu(props: NavMenuProps) {
{
userData ?
<DropdownMenuItem>
<Link href="/auth/logout" className="no-underline">
<Link href="/auth/logout" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Logout</p>
@@ -187,7 +158,7 @@ export default function NavMenu(props: NavMenuProps) {
</DropdownMenuItem>
:
<DropdownMenuItem>
<Link href="/auth/login" className="no-underline">
<Link href="/auth/login" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Login</p>
@@ -199,61 +170,60 @@ export default function NavMenu(props: NavMenuProps) {
</DropdownMenuContent>
</DropdownMenu>
:
<Menubar className='border-none hover:bg-stone-100 dark:hover:bg-neutral-900 bg-none'>
<Menubar className='border-none'>
<MenubarMenu>
<MenubarTrigger>
{
userData ?
<Avatar className="h-8 w-8">
<Avatar className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}>
<AvatarImage src={userData?.photo} alt="user profile" />
<AvatarFallback>
{userData?.username[0]}
<AvatarFallback className="bg-transparent hover:bg-muted">
{userData?.username[0].toUpperCase()}
</AvatarFallback>
</Avatar>
:
<UserCircle className="w-8 h-8" />
<UserCircle className="w-10 h-10" />
}
</MenubarTrigger>
<MenubarContent align="end" className="rounded-xl gap-2">
<MenubarItem>
<MenubarItem onClick={() => setDarkMode(!darkMode)} className="w-full hover:cursor-pointer">
<div
onClick={() => {
setDarkMode(!darkMode)
}
}
className="flex flex-rows">
{darkMode ? <Sun className="w-6 h-6" /> : <Moon className="w-6 h-6" />}
<p className="ml-3 font-semibold">{darkMode ? 'Light Mode' : 'Dark Mode'}</p>
</div>
</MenubarItem>
<MenubarItem>
<Link href="/agents" className="no-underline">
<Link href="/agents" className="no-underline w-full">
<div className="flex flex-rows">
<User className="w-6 h-6" />
<UsersFour className="w-6 h-6" />
<p className="ml-3 font-semibold">Agents</p>
</div>
</Link>
</MenubarItem>
<MenubarItem>
<Link href="/automations" className="no-underline">
<Link href="/automations" className="no-underline w-full">
<div className="flex flex-rows">
<Robot className="w-6 h-6" />
<p className="ml-3 font-semibold">Automations</p>
</div>
</Link>
</MenubarItem>
<MenubarItem>
<Link href="/search" className="no-underline">
<div className="flex flex-rows">
<MagnifyingGlass className="w-6 h-6" />
<p className="ml-3 font-semibold">Search</p>
</div>
</Link>
</MenubarItem>
{
userData &&
<MenubarItem>
<Link href="/search" className="no-underline w-full">
<div className="flex flex-rows">
<MagnifyingGlass className="w-6 h-6" />
<p className="ml-3 font-semibold">Search</p>
</div>
</Link>
</MenubarItem>
}
<>
<MenubarSeparator className="dark:bg-white height-[2px] bg-black" />
<MenubarItem>
<Link href="https://docs.khoj.dev" className="no-underline">
<Link href="https://docs.khoj.dev" className="no-underline w-full">
<div className="flex flex-rows">
<Question className="w-6 h-6" />
<p className="ml-3 font-semibold">Help</p>
@@ -263,7 +233,7 @@ export default function NavMenu(props: NavMenuProps) {
{
userData &&
<MenubarItem>
<Link href="/settings" className="no-underline">
<Link href="/settings" className="no-underline w-full">
<div className="flex flex-rows">
<GearFine className="w-6 h-6" />
<p className="ml-3 font-semibold">Settings</p>
@@ -274,7 +244,7 @@ export default function NavMenu(props: NavMenuProps) {
{
userData ?
<MenubarItem>
<Link href="/auth/logout" className="no-underline">
<Link href="/auth/logout" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Logout</p>
@@ -283,7 +253,7 @@ export default function NavMenu(props: NavMenuProps) {
</MenubarItem>
:
<MenubarItem>
<Link href="/auth/login" className="no-underline">
<Link href="/auth/login" className="no-underline w-full">
<div className="flex flex-rows">
<ArrowRight className="w-6 h-6" />
<p className="ml-3 font-semibold">Login</p>

View File

@@ -76,6 +76,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
import { modifyFileFilterForConversation } from "@/app/common/chatFunctions";
import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area";
import { KhojLogo, KhojLogoType } from "@/app/components/logo/khogLogo";
import NavMenu from "../navMenu/navMenu";
// Define a fetcher function
const fetcher = (url: string) => fetch(url).then((res) => res.json());
@@ -334,7 +335,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
<ScrollArea>
<ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5 border-l border-l-transparent p-[1px]" />
<div className={styles.sessionsList}>
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).map((timeGrouping) => (
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).filter(tg => tg !== "All Time").map((timeGrouping) => (
<div key={timeGrouping} className={`my-4`}>
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem]`}>
{timeGrouping}
@@ -368,6 +369,7 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
interface ChatSessionActionMenuProps {
conversationId: string;
setTitle: (title: string) => void;
}
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
@@ -386,7 +388,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
setShowShareUrl(true);
setIsSharing(false);
}
}, [isSharing]);
}, [isSharing, props.conversationId]);
if (isRenaming) {
return (
@@ -408,6 +410,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
<Button
onClick={() => {
renameConversation(props.conversationId, renamedTitle);
props.setTitle(renamedTitle);
setIsRenaming(false);
}}
type="submit">Rename</Button>
@@ -528,6 +531,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
function ChatSession(props: ChatHistory) {
const [isHovered, setIsHovered] = useState(false);
const [title, setTitle] = useState(props.slug || "New Conversation 🌱");
var currConversationId = parseInt(new URLSearchParams(window.location.search).get('conversationId') || "-1");
return (
<div
@@ -536,9 +540,9 @@ function ChatSession(props: ChatHistory) {
key={props.conversation_id}
className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''} ${currConversationId === parseInt(props.conversation_id) && currConversationId != -1 ? "dark:bg-neutral-800 bg-white" : ""}`}>
<Link href={`/chat?conversationId=${props.conversation_id}`} onClick={() => props.showSidePanel(false)}>
<p className={styles.session}>{props.slug || "New Conversation 🌱"}</p>
<p className={styles.session}>{title}</p>
</Link>
<ChatSessionActionMenu conversationId={props.conversation_id} />
<ChatSessionActionMenu conversationId={props.conversation_id} setTitle={setTitle} />
</div>
);
}
@@ -660,36 +664,42 @@ export default function SidePanel(props: SidePanelProps) {
}, [chatSessions]);
return (
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed} mt-1`}>
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed} ${props.isMobileWidth ? 'mt-0' : 'mt-1'}`}>
<div className={`flex justify-between flex-row`}>
<Link href='/'>
{props.isMobileWidth && <KhojLogo /> || <KhojLogoType />}
</Link>
{
authenticatedData && props.isMobileWidth ?
props.isMobileWidth ?
<Drawer open={enabled} onOpenChange={(open) => {
if (!enabled) setEnabled(false);
setEnabled(open);
}
}>
<DrawerTrigger><Sidebar className="h-4 w-4 mx-2" weight="thin" /></DrawerTrigger>
<DrawerTrigger><Sidebar className="h-8 w-8 mx-2" weight="thin" /></DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Sessions and Files</DrawerTitle>
<DrawerDescription>View all conversation sessions and manage conversation file filters</DrawerDescription>
</DrawerHeader>
<div className={`${styles.panelWrapper}`}>
<SessionsAndFiles
setEnabled={setEnabled}
subsetOrganizedData={subsetOrganizedData}
organizedData={organizedData}
data={data}
uploadedFiles={props.uploadedFiles}
userProfile={authenticatedData}
conversationId={props.conversationId}
isMobileWidth={props.isMobileWidth}
/>
</div>
{
authenticatedData ?
<div className={`${styles.panelWrapper}`}>
<SessionsAndFiles
setEnabled={setEnabled}
subsetOrganizedData={subsetOrganizedData}
organizedData={organizedData}
data={data}
uploadedFiles={props.uploadedFiles}
userProfile={authenticatedData}
conversationId={props.conversationId}
isMobileWidth={props.isMobileWidth}
/>
</div>
:
<div className={`${styles.panelWrapper}`}>
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`} className="text-center"> {/* Redirect to login page */}
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1" />Sign Up</Button>
</Link>
</div>
}
<DrawerFooter>
<DrawerClose>
<Button variant="outline">Done</Button>
@@ -698,15 +708,33 @@ export default function SidePanel(props: SidePanelProps) {
</DrawerContent>
</Drawer>
:
<div className={`${enabled ? 'flex items-center flex-row gap-2' : 'flex'}`}>
<Link className={`ml-4 mr-4`} href="/">
{enabled ? <NotePencil className="h-6 w-6" /> : <NotePencil className="h-6 w-6" color="gray" />}
<div className={`grid grid-flow-col gap-4 w-fit`}>
<Link href='/' className="content-center">
<KhojLogoType />
</Link>
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
{enabled ? <Sidebar className="h-6 w-6" /> : <Sidebar className="h-6 w-6" color="gray" />}
</button>
<div className='grid grid-flow-col gap-2 items-center'>
<Link className='mx-4' href="/">
{enabled ? <NotePencil className="h-6 w-6" weight="fill" /> : <NotePencil className="h-6 w-6" color="gray" />}
</Link>
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
{enabled ? <Sidebar className="h-6 w-6" weight="fill" /> : <Sidebar className="h-6 w-6" color="gray" />}
</button>
</div>
<div className="fixed right-0 top-[0.9rem] w-fit h-fit">
<NavMenu />
</div>
</div>
}
{
props.isMobileWidth &&
<Link href='/' className="content-center">
<KhojLogoType />
</Link>
}
{
props.isMobileWidth &&
<NavMenu />
}
</div>
{
authenticatedData && !props.isMobileWidth && enabled &&
@@ -724,12 +752,10 @@ export default function SidePanel(props: SidePanelProps) {
</div>
}
{
!authenticatedData && enabled &&
!authenticatedData && enabled && !props.isMobileWidth &&
<div className={`${styles.panelWrapper}`}>
<Link href="/">
<Link href="/" className="flex flex-col content-start items-start no-underline">
<Button variant="ghost"><House className="h-4 w-4 mr-1" />Home</Button>
</Link>
<Link href="/">
<Button variant="ghost"><StackPlus className="h-4 w-4 mr-1" />New Conversation</Button>
</Link>
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}

View File

@@ -123,9 +123,9 @@ div.modalSessionsList div.session {
@media screen and (max-width: 768px) {
div.panel {
padding: 0.5rem;
position: fixed;
width: fit-content;
padding: 0.25rem;
/* position: fixed; */
width: 100%;
}
div.expanded {
@@ -143,7 +143,9 @@ div.modalSessionsList div.session {
div.session.compressed {
max-width: 100%;
grid-template-columns: minmax(auto, 300px) 1fr;
display: grid;
grid-template-columns: 70vw auto;
justify-content: space-between;
}
div.session {

View File

@@ -38,7 +38,7 @@ interface SuggestionCardProps {
export default function SuggestionCard(data: SuggestionCardProps) {
const bgColors = converColorToBgGradient(data.color);
const cardClassName = `${styles.card} ${bgColors} md:w-full md:h-fit sm:w-full sm:h-fit lg:w-[200px] lg:h-[200px]`;
const cardClassName = `${styles.card} ${bgColors} md:w-full md:h-fit sm:w-full sm:h-fit lg:w-[200px] lg:h-[200px] cursor-pointer`;
const titleClassName = `${styles.title} pt-2 dark:text-white dark:font-bold`;
const descriptionClassName = `${styles.text} dark:text-white`;

View File

@@ -13,6 +13,11 @@ input.factVerification {
font-size: large;
}
div.factCheckerContainer {
width: 75vw;
margin: auto;
}
input.factVerification:focus {
outline: none;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
@@ -123,6 +128,12 @@ div.dot2 {
animation-delay: -1.0s;
}
@media screen and (max-width: 768px) {
div.factCheckerContainer {
width: 95vw;
}
}
@-webkit-keyframes sk-rotate {
100% {
-webkit-transform: rotate(360deg)

View File

@@ -1,10 +0,0 @@
.factCheckerLayout {
max-width: 70vw;
margin: auto;
}
@media screen and (max-width: 700px) {
.factCheckerLayout {
max-width: 90vw;
}
}

View File

@@ -1,7 +1,4 @@
import type { Metadata } from "next";
import NavMenu from '../components/navMenu/navMenu';
import styles from './factCheckerLayout.module.css';
export const metadata: Metadata = {
title: "Khoj AI - Fact Checker",
@@ -17,8 +14,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<div className={styles.factCheckerLayout}>
<NavMenu selected="none" />
<div>
{children}
</div>
);

View File

@@ -19,6 +19,7 @@ import {
CardTitle,
} from "@/components/ui/card"
import Link from 'next/link';
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
@@ -76,41 +77,41 @@ async function verifyStatement(
setIsLoading: (loading: boolean) => void,
setInitialResponse: (response: string) => void,
setInitialReferences: (references: ResponseWithReferences) => void) {
setIsLoading(true);
// Send a message to the chat server to verify the fact
let verificationMessage = `${verificationPrecursor} ${message}`;
const apiURL = `${chatURL}?q=${encodeURIComponent(verificationMessage)}&client=web&stream=true&conversation_id=${conversationId}`;
try {
const response = await fetch(apiURL);
if (!response.body) throw new Error("No response body found");
setIsLoading(true);
// Send a message to the chat server to verify the fact
let verificationMessage = `${verificationPrecursor} ${message}`;
const apiURL = `${chatURL}?q=${encodeURIComponent(verificationMessage)}&client=web&stream=true&conversation_id=${conversationId}`;
try {
const response = await fetch(apiURL);
if (!response.body) throw new Error("No response body found");
const reader = response.body?.getReader();
let decoder = new TextDecoder();
let result = "";
const reader = response.body?.getReader();
let decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
while (true) {
const { done, value } = await reader.read();
if (done) break;
let chunk = decoder.decode(value, { stream: true });
let chunk = decoder.decode(value, { stream: true });
if (chunk.includes("### compiled references:")) {
const references = handleCompiledReferences(chunk, result);
if (references.response) {
result = references.response;
setInitialResponse(references.response);
setInitialReferences(references);
}
} else {
result += chunk;
setInitialResponse(result);
if (chunk.includes("### compiled references:")) {
const references = handleCompiledReferences(chunk, result);
if (references.response) {
result = references.response;
setInitialResponse(references.response);
setInitialReferences(references);
}
} else {
result += chunk;
setInitialResponse(result);
}
} catch (error) {
console.error("Error verifying statement: ", error);
} finally {
setIsLoading(false);
}
} catch (error) {
console.error("Error verifying statement: ", error);
} finally {
setIsLoading(false);
}
}
@@ -145,7 +146,7 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
setInitialResponse(props.prefilledResponse);
setIsLoading(false);
} else {
verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => {});
verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => { });
}
setIsMobileWidth(window.innerWidth < 768);
@@ -454,7 +455,7 @@ export default function FactChecker() {
additionalLink={additionalLink}
setChildReferencesCallback={setChildReferencesCallback} />
);
}).filter(Boolean);
}).filter(Boolean);
};
const renderSupplementalReferences = (references: SupplementReferences[]) => {
@@ -489,102 +490,111 @@ export default function FactChecker() {
}
return (
<div className={styles.factCheckerContainer}>
<h1 className={`${styles.response} font-large outline-slate-800 dark:outline-slate-200`}>
AI Fact Checker
</h1>
<footer className={`${styles.footer} mt-4`}>
This is an experimental AI tool. It may make mistakes.
</footer>
{
initialResponse && initialReferences && childReferences
?
<div className={styles.reportActions}>
<Button asChild variant='secondary'>
<Link href="/factchecker" target="_blank" rel="noopener noreferrer">
Try Another
</Link>
</Button>
<ShareLink
buttonTitle='Share report'
title="AI Fact Checking Report"
description="Share this fact checking report with others. Anyone who has this link will be able to view the report."
url={constructShareUrl()}
onShare={loadedFromStorage ? () => {} : storeData} />
</div>
: <div className={styles.newReportActions}>
<div className={`${styles.inputFields} mt-4`}>
<Input
type="text"
maxLength={200}
placeholder="Enter a falsifiable statement to verify"
disabled={isLoading}
onChange={(e) => setFactToVerify(e.target.value)}
value={factToVerify}
onKeyDown={(e) => {
if (e.key === "Enter") {
onClickVerify();
}
}}
onFocus={(e) => e.target.placeholder = ""}
onBlur={(e) => e.target.placeholder = "Enter a falsifiable statement to verify"} />
<Button disabled={clickedVerify} onClick={() => onClickVerify()}>Verify</Button>
</div>
<h3 className={`mt-4 mb-4`}>
Try with a particular model. You must be <a href="/settings" className="font-medium text-blue-600 dark:text-blue-500 hover:underline">subscribed</a> to configure the model.
</h3>
</div>
}
<ModelPicker disabled={isLoading || loadedFromStorage} setModelUsed={setModelUsed} initialModel={initialModel} />
{isLoading && <div className={styles.loading}>
<>
<div className='relative md:fixed h-full'>
<SidePanel
conversationId={null}
uploadedFiles={[]}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={styles.factCheckerContainer}>
<h1 className={`${styles.response} pt-8 md:pt-4 font-large outline-slate-800 dark:outline-slate-200`}>
AI Fact Checker
</h1>
<footer className={`${styles.footer} mt-4`}>
This is an experimental AI tool. It may make mistakes.
</footer>
{
initialResponse && initialReferences && childReferences
?
<div className={styles.reportActions}>
<Button asChild variant='secondary'>
<Link href="/factchecker" target="_blank" rel="noopener noreferrer">
Try Another
</Link>
</Button>
<ShareLink
buttonTitle='Share report'
title="AI Fact Checking Report"
description="Share this fact checking report with others. Anyone who has this link will be able to view the report."
url={constructShareUrl()}
onShare={loadedFromStorage ? () => { } : storeData} />
</div>
: <div className={styles.newReportActions}>
<div className={`${styles.inputFields} mt-4`}>
<Input
type="text"
maxLength={200}
placeholder="Enter a falsifiable statement to verify"
disabled={isLoading}
onChange={(e) => setFactToVerify(e.target.value)}
value={factToVerify}
onKeyDown={(e) => {
if (e.key === "Enter") {
onClickVerify();
}
}}
onFocus={(e) => e.target.placeholder = ""}
onBlur={(e) => e.target.placeholder = "Enter a falsifiable statement to verify"} />
<Button disabled={clickedVerify} onClick={() => onClickVerify()}>Verify</Button>
</div>
<h3 className={`mt-4 mb-4`}>
Try with a particular model. You must be <a href="/settings" className="font-medium text-blue-600 dark:text-blue-500 hover:underline">subscribed</a> to configure the model.
</h3>
</div>
}
<ModelPicker disabled={isLoading || loadedFromStorage} setModelUsed={setModelUsed} initialModel={initialModel} />
{isLoading && <div className={styles.loading}>
<LoadingSpinner />
</div>}
{
initialResponse &&
<Card className={`mt-4`}>
<CardHeader>
<CardTitle>{officialFactToVerify}</CardTitle>
</CardHeader>
<CardContent>
<div className={styles.responseText}>
<ChatMessage chatMessage={
{
automationId: "",
by: "AI",
message: initialResponse,
context: [],
created: (new Date()).toISOString(),
onlineContext: {}
}
} isMobileWidth={isMobileWidth} />
</div>
</CardContent>
<CardFooter>
{
initialReferences && initialReferences.online && Object.keys(initialReferences.online).length > 0 && (
<div className={styles.subLinks}>
{
initialResponse &&
<Card className={`mt-4`}>
<CardHeader>
<CardTitle>{officialFactToVerify}</CardTitle>
</CardHeader>
<CardContent>
<div className={styles.responseText}>
<ChatMessage chatMessage={
{
Object.entries(initialReferences.online).map(([key, onlineData], index) => {
const webpages = onlineData?.webpages || [];
return renderWebpages(webpages);
})
automationId: "",
by: "AI",
message: initialResponse,
context: [],
created: (new Date()).toISOString(),
onlineContext: {}
}
</div>
)}
</CardFooter>
</Card>
}
{
initialReferences &&
<div className={styles.referenceContainer}>
<h2 className="mt-4 mb-4">Supplements</h2>
<div className={styles.references}>
{ initialReferences.online !== undefined &&
renderReferences(conversationID, initialReferences, officialFactToVerify, loadedFromStorage, childReferences)}
} isMobileWidth={isMobileWidth} />
</div>
</CardContent>
<CardFooter>
{
initialReferences && initialReferences.online && Object.keys(initialReferences.online).length > 0 && (
<div className={styles.subLinks}>
{
Object.entries(initialReferences.online).map(([key, onlineData], index) => {
const webpages = onlineData?.webpages || [];
return renderWebpages(webpages);
})
}
</div>
)}
</CardFooter>
</Card>
}
{
initialReferences &&
<div className={styles.referenceContainer}>
<h2 className="mt-4 mb-4">Supplements</h2>
<div className={styles.references}>
{initialReferences.online !== undefined &&
renderReferences(conversationID, initialReferences, officialFactToVerify, loadedFromStorage, childReferences)}
</div>
</div>
</div>
}
</div>
}
</div>
</>
)
}

View File

@@ -10,6 +10,7 @@ export const metadata: Metadata = {
icons: {
icon: '/static/favicon.ico',
},
manifest: '/static/khoj.webmanifest',
};
export default function RootLayout({

View File

@@ -1,5 +1,5 @@
div.main {
height: 100vh;
height: 100dvh;
color: hsla(var(--foreground));
}
@@ -13,9 +13,7 @@ div.suggestions {
div.inputBox {
border: 1px solid var(--border-color);
border-radius: 16px;
margin-bottom: 20px;
gap: 12px;
align-content: center;
}
@@ -66,7 +64,6 @@ div.chatLayout {
div.chatBox {
display: grid;
height: 100%;
}
div.titleBar {
@@ -78,43 +75,50 @@ div.chatBoxBody {
display: grid;
height: 100%;
margin: auto;
grid-template-rows: auto 1fr;
}
div.homeGreetings {
display: grid;
height: 100%;
margin: auto;
grid-template-rows: 1fr 2fr;
}
div.sidePanel {
position: fixed;
height: 100%;
}
@media (max-width: 768px) {
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBox {
padding: 0;
}
}
@media screen and (max-width: 768px) {
div.inputBox {
margin-bottom: 0px;
}
div.chatBody {
grid-template-columns: 0fr 1fr;
}
div.chatBoxBody {
width: 100%;
grid-template-rows: auto;
}
div.sidePanel {
position: relative;
}
div.chatBox {
padding: 0;
height: 100%;
}
div.chatLayout {
gap: 0;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
div.homeGreetings {
grid-template-rows: auto 1fr;
}
}

View File

@@ -3,24 +3,24 @@ import './globals.css';
import styles from './page.module.css';
import 'katex/dist/katex.min.css';
import React, { use, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import useSWR from 'swr';
import Image from 'next/image';
import { ClockCounterClockwise } from '@phosphor-icons/react';
import { ArrowCounterClockwise, ClockCounterClockwise } from '@phosphor-icons/react';
import { Card, CardTitle } from '@/components/ui/card';
import SuggestionCard from '@/app/components/suggestions/suggestionCard';
import SidePanel from '@/app/components/sidePanel/chatHistorySidePanel';
import NavMenu from '@/app/components/navMenu/navMenu';
import Loading from '@/app/components/loading/loading';
import ChatInputArea, { ChatOptions } from '@/app/components/chatInputArea/chatInputArea';
import { Suggestion, suggestionsData } from '@/app/components/suggestions/suggestionsData';
import LoginPrompt from '@/app/components/loginPrompt/loginPrompt';
import { useAuthenticatedData, UserConfig, useUserConfig } from '@/app/common/auth';
import { convertColorToBorderClass } from '@/app/common/colorUtils';
import { getIconFromIconName } from '@/app/common/iconUtils';
import { AgentData } from '@/app/agents/page';
import { createNewConversation } from './common/chatFunctions';
interface ChatBodyDataProps {
@@ -33,20 +33,6 @@ interface ChatBodyDataProps {
isLoadingUserConfig: boolean;
}
async function createNewConvo(slug: string) {
try {
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, { method: "POST" });
if (!response.ok) throw new Error(`Failed to fetch chat sessions with status: ${response.status}`);
const data = await response.json();
const conversationID = data.conversation_id;
if (!conversationID) throw new Error("Conversation ID not found in response");
return conversationID;
} catch (error) {
console.error("Error creating new conversation:", error);
throw error;
}
}
function ChatBodyData(props: ChatBodyDataProps) {
const [message, setMessage] = useState('');
const [processingMessage, setProcessingMessage] = useState(false);
@@ -55,6 +41,9 @@ function ChatBodyData(props: ChatBodyDataProps) {
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
const [agentIcons, setAgentIcons] = useState<JSX.Element[]>([]);
const [agents, setAgents] = useState<AgentData[]>([]);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
const onConversationIdChange = props.onConversationIdChange;
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
const { data: agentsData, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
@@ -65,16 +54,12 @@ function ChatBodyData(props: ChatBodyDataProps) {
}
useEffect(() => {
console.log(`Loading user config: ${props.isLoadingUserConfig}`);
if (props.isLoadingUserConfig) return;
// Set user config
console.log(`Logged In: ${props.isLoggedIn}\nUserConfig: ${props.userConfig}`);
// Get today's day
const today = new Date();
const day = today.getDay();
const timeOfDay = today.getHours() > 4 && today.getHours() < 12 ? 'morning' : today.getHours() < 17 ? 'afternoon' : 'evening';
const timeOfDay = today.getHours() >= 17 || today.getHours() < 4 ? 'evening' : today.getHours() >= 12 ? 'afternoon' : 'morning';
const nameSuffix = props.userConfig?.given_name ? `, ${props.userConfig?.given_name}` : "";
const greetings = [
`What would you like to get done${nameSuffix}?`,
@@ -94,7 +79,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
}, [props.chatOptionsData]);
useEffect(() => {
const nSlice = props.isMobileWidth ? 3 : 4;
const nSlice = props.isMobileWidth ? 2 : 4;
const shuffledAgents = agentsData ? [...agentsData].sort(() => 0.5 - Math.random()) : [];
const agents = agentsData ? [agentsData[0]] : []; // Always add the first/default agent.
@@ -111,7 +96,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
agent => getIconFromIconName(agent.icon, agent.color) || <Image key={agent.name} src={agent.avatar} alt={agent.name} width={50} height={50} />
);
setAgentIcons(agentIcons);
}, [agentsData]);
}, [agentsData, props.isMobileWidth]);
function shuffleSuggestionsCards() {
shuffleAndSetOptions();
@@ -122,8 +107,8 @@ function ChatBodyData(props: ChatBodyDataProps) {
if (message && !processingMessage) {
setProcessingMessage(true);
try {
const newConversationId = await createNewConvo(selectedAgent || "khoj");
props.onConversationIdChange?.(newConversationId);
const newConversationId = await createNewConversation(selectedAgent || "khoj");
onConversationIdChange?.(newConversationId);
window.location.href = `/chat?conversationId=${newConversationId}`;
localStorage.setItem('message', message);
}
@@ -138,7 +123,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
if (message) {
setProcessingMessage(true);
};
}, [selectedAgent, message]);
}, [selectedAgent, message, processingMessage, onConversationIdChange]);
function fillArea(link: string, type: string, prompt: string) {
if (!link) {
@@ -148,7 +133,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
if (type === "Online Search") {
message_str = "/online " + prompt;
} else if (type === "Paint") {
message_str = "/paint " + prompt;
message_str = "/image " + prompt;
} else {
message_str = prompt;
}
@@ -164,10 +149,17 @@ function ChatBodyData(props: ChatBodyDataProps) {
}
return (
<div className={`${styles.chatBoxBody}`}>
<div className="w-full text-center">
<div className={`${styles.homeGreetings} w-full md:w-auto`}>
{
showLoginPrompt && (
<LoginPrompt
onOpenChange={setShowLoginPrompt}
loginRedirectMessage={"Login to start extending your second brain"} />
)
}
<div className={`w-full text-center justify-end content-end`}>
<div className="items-center">
<h1 className="text-center w-fit pb-6 px-4 mx-auto">{greeting}</h1>
<h1 className="text-2xl md:text-3xl text-center w-fit pb-6 px-4 mx-auto">{greeting}</h1>
</div>
{
!props.isMobileWidth &&
@@ -192,10 +184,10 @@ function ChatBodyData(props: ChatBodyDataProps) {
</div>
}
</div>
<div className={`ml-auto mr-auto ${props.isMobileWidth ? 'w-full' : 'w-fit'}`}>
<div className={`mx-auto ${props.isMobileWidth ? 'w-full' : 'w-fit'}`}>
{
!props.isMobileWidth &&
<div className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none`}>
<div className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`}>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
@@ -210,7 +202,15 @@ function ChatBodyData(props: ChatBodyDataProps) {
{shuffledOptions.map((suggestion, index) => (
<div
key={`${suggestion.type} ${suggestion.description}`}
onClick={() => fillArea(suggestion.link, suggestion.type, suggestion.description)}>
onClick={(event) => {
if (props.isLoggedIn) {
fillArea(suggestion.link, suggestion.type, suggestion.description);
} else {
event.preventDefault();
event.stopPropagation();
setShowLoginPrompt(true);
}
}}>
<SuggestionCard
key={suggestion.type + Math.random()}
title={suggestion.type}
@@ -225,40 +225,42 @@ function ChatBodyData(props: ChatBodyDataProps) {
<button
onClick={shuffleSuggestionsCards}
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700">
More Examples <ClockCounterClockwise className='h-4 w-4 inline' />
More Ideas <ArrowCounterClockwise className='h-4 w-4 inline' />
</button>
</div>
</div>
{
props.isMobileWidth &&
<div className={`${styles.inputBox} shadow-md dark:bg-neutral-700 bg-background dark: align-middle items-center justify-center py-3 px-1`}>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={null}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles} />
<div className="flex gap-2 items-center justify-left pt-4">
{agentIcons.map((icon, index) => (
<Card
key={`${index}-${agents[index].slug}`}
className={
`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : 'border-muted text-muted-foreground'} hover:cursor-pointer`
}>
<CardTitle
className='text-center text-xs font-medium flex justify-center items-center px-1.5 py-2'
onClick={() => setSelectedAgent(agents[index].slug)}>
{icon} {agents[index].name}
</CardTitle>
<>
<div className={`${styles.inputBox} pt-1 shadow-[0_-20px_25px_-5px_rgba(0,0,0,0.1)] dark:bg-neutral-700 bg-background align-middle items-center justify-center pb-3 mx-1 rounded-t-2xl rounded-b-none`}>
<div className="flex gap-2 items-center justify-left pt-1 pb-2 px-12">
{agentIcons.map((icon, index) => (
<Card
key={`${index}-${agents[index].slug}`}
className={
`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : 'border-muted text-muted-foreground'} hover:cursor-pointer`
}>
<CardTitle
className='text-center text-xs font-medium flex justify-center items-center px-1.5 py-1'
onClick={() => setSelectedAgent(agents[index].slug)}>
{icon} {agents[index].name}
</CardTitle>
</Card>
))}
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
<CardTitle className={`text-center ${props.isMobileWidth ? 'text-xs' : 'text-md'} font-normal flex justify-center items-center px-1.5 py-2`}>See All </CardTitle>
</Card>
))}
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
<CardTitle className={`text-center ${props.isMobileWidth ? 'text-xs' : 'text-md'} font-normal flex justify-center items-center px-1.5 py-2`}>See All </CardTitle>
</Card>
</div>
<ChatInputArea
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendDisabled={processingMessage}
chatOptionsData={props.chatOptionsData}
conversationId={null}
isMobileWidth={props.isMobileWidth}
setUploadedFiles={props.setUploadedFiles} />
</div>
</div>
</>
}
</div>
);
@@ -267,7 +269,6 @@ function ChatBodyData(props: ChatBodyDataProps) {
export default function Home() {
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
const [isLoading, setLoading] = useState(true);
const [title, setTitle] = useState('');
const [conversationId, setConversationID] = useState<string | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [isMobileWidth, setIsMobileWidth] = useState(false);
@@ -314,7 +315,7 @@ export default function Home() {
return (
<div className={`${styles.main} ${styles.chatLayout}`}>
<title>
{title}
Khoj AI - Your Second Brain
</title>
<div className={`${styles.sidePanel}`}>
<SidePanel
@@ -324,7 +325,6 @@ export default function Home() {
/>
</div>
<div className={`${styles.chatBox}`}>
<NavMenu selected="Chat" title={title}></NavMenu>
<div className={`${styles.chatBoxBody}`}>
<ChatBodyData
isLoggedIn={authenticatedData !== null}

View File

@@ -5,7 +5,6 @@ import { Input } from '@/components/ui/input';
import { useAuthenticatedData } from '../common/auth';
import { useEffect, useRef, useState } from 'react';
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
import NavMenu from '../components/navMenu/navMenu';
import styles from './search.module.css';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
@@ -160,13 +159,7 @@ export default function Search() {
}, [isMobileWidth]);
function search() {
if (searchResultsLoading) {
return;
}
if (!searchQuery.trim()) {
return;
}
if (searchResultsLoading || !searchQuery.trim()) return;
const apiUrl = `/api/search?q=${encodeURIComponent(searchQuery)}&client=web`;
fetch(apiUrl, {
@@ -208,101 +201,97 @@ export default function Search() {
}, [searchQuery]);
console.log('searchResults', searchResults);
return (
<div className={`${styles.searchLayout}`}>
<div className='h-full'>
<div>
<div className={`h-full ${styles.sidePanel}`}>
<SidePanel
conversationId={null}
uploadedFiles={[]}
isMobileWidth={isMobileWidth}
/>
</div>
<div className="md:w-3/4 sm:w-full mr-auto ml-auto">
<NavMenu title={title} selected='Chat' />
<div className='p-4 md:w-3/4 sm:w-full mr-auto ml-auto'>
{
isMobileWidth && <div className='font-bold'>Search</div>
}
<div className='flex justify-between items-center border-2 border-muted p-2 gap-4 rounded-lg'>
<MagnifyingGlass className='inline m-2' />
<Input
autoFocus
className='border-none'
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && search()}
type="search"
placeholder="Search Documents" />
<button className='px-4 rounded' onClick={() => search()}>
Find
</button>
</div>
{
focusSearchResult &&
<div className='mt-4'>
<Button onClick={() => setFocusSearchResult(null)} className='mb-4' variant={'outline'}>
<ArrowLeft className='inline mr-2' />
Back
</Button>
{focusNote(focusSearchResult)}
<div className={`${styles.searchLayout}`}>
<div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8">
<div className='p-4 md:w-3/4 sm:w-full mx-auto'>
<div className='flex justify-between items-center border-2 border-muted p-2 gap-4 rounded-lg'>
<MagnifyingGlass className='inline m-2 h-4 w-4' />
<Input
autoFocus={true}
className='border-none'
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && search()}
type="search"
placeholder="Search Documents" />
<button className='px-4 rounded' onClick={() => search()}>
Find
</button>
</div>
}
{
!focusSearchResult && searchResults && searchResults.length > 0 &&
<div className='mt-4'>
<ScrollArea className="h-[80vh]">
{
searchResults.map((result, index) => {
return (
<Note key={result["corpus-id"]}
note={result}
setFocusSearchResult={setFocusSearchResult} />
);
})
}
</ScrollArea>
</div>
}
{
searchResults == null &&
<Card className='flex flex-col items-center justify-center border-none shadow-none'>
<CardHeader className='flex flex-col items-center justify-center'>
<CardDescription className='border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4'>
<FileMagnifyingGlass weight='fill' className='text-muted-foreground h-10 w-10' />
</CardDescription>
<CardTitle className='text-center'>
Search across your documents
</CardTitle>
</CardHeader>
<CardContent className='text-muted-foreground items-center justify-center text-center flex'>
<Lightbulb className='inline mr-2' /> {exampleQuery}
</CardContent>
</Card>
}
{
searchResults && searchResults.length === 0 &&
<Card className='flex flex-col items-center justify-center border-none shadow-none'>
<CardHeader className='flex flex-col items-center justify-center'>
<CardDescription className='border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4'>
<FileDashed weight='fill' className='text-muted-foreground h-10 w-10' />
</CardDescription>
<CardTitle className='text-center'>
No documents found
</CardTitle>
</CardHeader>
<CardContent>
<div className='text-muted-foreground items-center justify-center text-center flex'>
To use search, upload your docs to your account.
</div>
<Link href="https://docs.khoj.dev/data-sources/share_your_data" className='no-underline'>
<div className='mt-4 text-center text-secondary-foreground bg-secondary w-fit m-auto p-2 rounded-lg'>
Learn More
{
focusSearchResult &&
<div className='mt-4'>
<Button onClick={() => setFocusSearchResult(null)} className='mb-4' variant={'outline'}>
<ArrowLeft className='inline mr-2' />
Back
</Button>
{focusNote(focusSearchResult)}
</div>
}
{
!focusSearchResult && searchResults && searchResults.length > 0 &&
<div className='mt-4 max-w-[92vw] break-all'>
<ScrollArea className="h-[80vh]">
{
searchResults.map((result, index) => {
return (
<Note key={result["corpus-id"]}
note={result}
setFocusSearchResult={setFocusSearchResult} />
);
})
}
</ScrollArea>
</div>
}
{
searchResults == null &&
<Card className='flex flex-col items-center justify-center border-none shadow-none'>
<CardHeader className='flex flex-col items-center justify-center'>
<CardDescription className='border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4'>
<FileMagnifyingGlass weight='fill' className='text-muted-foreground h-10 w-10' />
</CardDescription>
<CardTitle className='text-center'>
Search across your documents
</CardTitle>
</CardHeader>
<CardContent className='text-muted-foreground items-center justify-center text-center flex'>
<Lightbulb className='inline mr-2' /> {exampleQuery}
</CardContent>
</Card>
}
{
searchResults && searchResults.length === 0 &&
<Card className='flex flex-col items-center justify-center border-none shadow-none'>
<CardHeader className='flex flex-col items-center justify-center'>
<CardDescription className='border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4'>
<FileDashed weight='fill' className='text-muted-foreground h-10 w-10' />
</CardDescription>
<CardTitle className='text-center'>
No documents found
</CardTitle>
</CardHeader>
<CardContent>
<div className='text-muted-foreground items-center justify-center text-center flex'>
To use search, upload your docs to your account.
</div>
</Link>
</CardContent>
</Card>
}
<Link href="https://docs.khoj.dev/data-sources/share_your_data" className='no-underline'>
<div className='mt-4 text-center text-secondary-foreground bg-secondary w-fit m-auto p-2 rounded-lg'>
Learn More
</div>
</Link>
</CardContent>
</Card>
}
</div>
</div>
</div>
</div>

View File

@@ -1,12 +1,22 @@
div.searchLayout {
display: grid;
grid-template-columns: auto 1fr;
grid-template-columns: 1fr;
gap: 1rem;
height: 100vh;
}
div.sidePanel {
position: fixed;
height: 100%;
}
@media screen and (max-width: 768px) {
div.searchLayout {
gap: 0;
}
div.sidePanel {
position: relative;
height: 100%;
}
}

View File

@@ -69,9 +69,10 @@ import {
ArrowCircleDown,
ArrowsClockwise,
Check,
CaretDown,
Waveform,
} from "@phosphor-icons/react";
import NavMenu from "../components/navMenu/navMenu";
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import Loading from "../components/loading/loading";
@@ -242,11 +243,11 @@ const DropdownComponent: React.FC<DropdownComponentProps> = ({ items, selected,
const [position, setPosition] = useState(selected?.toString() ?? "0");
return !!selected && (
<div className="overflow-hidden">
<div className="overflow-hidden shadow-md rounded-lg">
<DropdownMenu>
<DropdownMenuTrigger asChild className="w-full">
<Button variant="outline" className="justify-start py-6">
{items.find(item => item.id.toString() === position)?.name}
<DropdownMenuTrigger asChild className="w-full rounded-lg">
<Button variant="outline" className="justify-start py-6 rounded-lg">
{items.find(item => item.id.toString() === position)?.name} <CaretDown className="h-4 w-4 ml-auto text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
@@ -347,7 +348,7 @@ enum PhoneNumberValidationState {
export default function SettingsView() {
const [title, setTitle] = useState("Settings");
const [isMobileWidth, setIsMobileWidth] = useState(false);
const { apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey } = useApiKeys();
const {apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey} = useApiKeys();
const {userConfig: initialUserConfig} = useUserConfig(true);
const [userConfig, setUserConfig] = useState<UserConfig | null>(null);
const [name, setName] = useState<string | undefined>(undefined);
@@ -357,7 +358,7 @@ export default function SettingsView() {
const [numberValidationState, setNumberValidationState] = useState<PhoneNumberValidationState>(PhoneNumberValidationState.Verified);
const [isManageFilesModalOpen, setIsManageFilesModalOpen] = useState(false);
const { toast } = useToast();
const cardClassName = "w-full lg:w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg";
const cardClassName = "w-full lg:w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg bg-gradient-to-b from-background to-gray-50 dark:to-gray-950";
useEffect(() => {
setUserConfig(initialUserConfig);
@@ -643,7 +644,7 @@ export default function SettingsView() {
if (!userConfig) return <Loading />;
return (
<div id="page" className={styles.page}>
<div className={styles.page}>
<title>
{title}
</title>
@@ -655,8 +656,7 @@ export default function SettingsView() {
/>
</div>
<div className={styles.content}>
<NavMenu selected="Settings" title="Settings" showLogo={true} />
<div className={styles.contentBody}>
<div className={`${styles.contentBody} mx-10 my-2`}>
<Suspense fallback={<Loading />}>
<div id="content" className="grid grid-flow-column sm:grid-flow-row gap-16 m-8">
<div className="section grid gap-8">
@@ -752,7 +752,7 @@ export default function SettingsView() {
<div className="text-2xl">Content</div>
<div className="cards flex flex-wrap gap-16">
<Card className={cardClassName}>
<CardHeader className="text-xl flex flex-row text-2xl"><Laptop className="h-8 w-8 mr-2" />Files</CardHeader>
<CardHeader className="flex flex-row text-2xl"><Laptop className="h-8 w-8 mr-2" />Files</CardHeader>
<CardContent className="overflow-hidden pb-12 text-gray-400">
Manage your synced files
</CardContent>
@@ -773,7 +773,7 @@ export default function SettingsView() {
</CardFooter>
</Card>
<Card className={`${cardClassName} hidden`}>
<CardHeader className="text-xl flex flex-row text-2xl"><GithubLogo className="h-8 w-8 mr-2" />Github</CardHeader>
<CardHeader className="flex flex-row text-2xl"><GithubLogo className="h-8 w-8 mr-2" />Github</CardHeader>
<CardContent className="overflow-hidden pb-12 text-gray-400">
Set Github repositories to index
</CardContent>
@@ -898,7 +898,7 @@ export default function SettingsView() {
)}
{userConfig.voice_model_options.length > 0 && (
<Card className={cardClassName}>
<CardHeader className="text-xl flex flex-row"><SpeakerHigh className="h-7 w-7 mr-2"/>Voice</CardHeader>
<CardHeader className="text-xl flex flex-row"><Waveform className="h-7 w-7 mr-2"/>Voice</CardHeader>
<CardContent className="overflow-hidden pb-12 grid gap-8">
<p className="text-gray-400">Pick the voice model to generate speech responses</p>
<DropdownComponent
@@ -919,7 +919,8 @@ export default function SettingsView() {
<div className="section grid gap-8">
<div className="text-2xl">Clients</div>
<div className="cards flex flex-wrap gap-8">
<Card className="grid grid-flow-column border border-gray-300 shadow-md rounded-lg">
{!userConfig.anonymous_mode && (
<Card className="grid grid-flow-column border border-gray-300 shadow-md rounded-lg bg-gradient-to-b from-background to-gray-50 dark:to-gray-950">
<CardHeader className="text-xl grid grid-flow-col grid-cols-[1fr_auto] pb-0">
<span className="flex flex-wrap">
<Key className="h-7 w-7 mr-2" />API Keys
@@ -937,13 +938,27 @@ export default function SettingsView() {
{apiKeys.map((key) => (
<TableRow key={key.token}>
<TableCell className="pl-0 py-3">{key.name}</TableCell>
<TableCell className="grid grid-flow-col grid-cols-[1fr_auto] bg-secondary rounded-xl p-3">
<TableCell className="grid grid-flow-col grid-cols-[1fr_auto] bg-secondary rounded-xl p-3 m-1">
<span>
{`${key.token.slice(0, 6)}...${key.token.slice(-4)}`}
</span>
<div className="grid grid-flow-col">
<Copy weight="bold" className="h-4 w-4 mr-2 hover:bg-primary/40" onClick={() => copyAPIKey(key.token)}/>
<Trash weight="bold" className='h-4 w-4 mr-2 md:ml-4 text-red-400 hover:bg-primary/40' onClick={() => deleteAPIKey(key.token)}/>
<Copy
weight="bold"
className="h-4 w-4 mr-2 hover:bg-primary/40"
onClick={() => {
toast({title: `🔑 Copied API Key: ${key.name}`, description: `Set this API key in the Khoj apps you want to connect to this Khoj account`});
copyAPIKey(key.token);
}}
/>
<Trash
weight="bold"
className='h-4 w-4 mr-2 md:ml-4 text-red-400 hover:bg-primary/40'
onClick={() => {
toast({title: `🔑 Deleted API Key: ${key.name}`, description: `Apps using this API key will no longer connect to this Khoj account`});
deleteAPIKey(key.token);
}}
/>
</div>
</TableCell>
</TableRow>
@@ -954,6 +969,7 @@ export default function SettingsView() {
<CardFooter className="flex flex-wrap gap-4">
</CardFooter>
</Card>
)}
<Card className={cardClassName}>
<CardHeader className="text-xl flex flex-row">
<WhatsappLogo className="h-7 w-7 mr-2"/>

View File

@@ -1,6 +1,6 @@
div.page {
display: grid;
grid-template-columns: auto 1fr;
grid-template-columns: 1fr;
gap: 1rem;
height: 100vh;
color: hsla(var(--foreground));
@@ -8,15 +8,35 @@ div.page {
div.contentBody {
display: grid;
margin: auto;
margin-left: 20vw;
margin-top: 1rem;
}
div.phoneInput {
padding: 0rem;
}
div.sidePanel {
position: fixed;
height: 100%;
}
div.phoneInput input {
width: 100%;
padding: 0.5rem;
border: 1px solid hsla(var(--border));
border-radius: 0.25rem;
}
@media screen and (max-width: 768px) {
div.sidePanel {
position: relative;
height: 100%;
}
div.contentBody {
margin-left: 0;
margin-top: 0;
}
}

View File

@@ -37,24 +37,26 @@ function ChatBodyData(props: ChatBodyDataProps) {
const [processingMessage, setProcessingMessage] = useState(false);
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
const setQueryToProcess = props.setQueryToProcess
const streamedMessages = props.streamedMessages;
useEffect(() => {
if (message) {
setProcessingMessage(true);
props.setQueryToProcess(message);
setQueryToProcess(message);
}
}, [message]);
}, [message, setQueryToProcess]);
useEffect(() => {
console.log("Streamed messages", props.streamedMessages);
if (props.streamedMessages &&
props.streamedMessages.length > 0 &&
props.streamedMessages[props.streamedMessages.length - 1].completed) {
if (streamedMessages &&
streamedMessages.length > 0 &&
streamedMessages[streamedMessages.length - 1].completed) {
setProcessingMessage(false);
} else {
setMessage('');
}
}, [props.streamedMessages]);
}, [streamedMessages]);
if (!props.publicConversationSlug && !props.conversationId) {
return (
@@ -167,7 +169,7 @@ export default function SharedChat() {
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
setProcessQuerySignal(true);
}
}, [queryToProcess]);
}, [queryToProcess, conversationId, paramSlug]);
useEffect(() => {
if (processQuerySignal) {
@@ -251,7 +253,7 @@ export default function SharedChat() {
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
}
})();
}, [conversationId]);
}, [conversationId, queryToProcess]);
if (isLoading) {
return <Loading />;
@@ -280,7 +282,6 @@ export default function SharedChat() {
</div>
<div className={styles.chatBox}>
<NavMenu selected="Chat" title={title} />
<div className={styles.chatBoxBody}>
<Suspense fallback={<Loading />}>
<ChatBodyData

View File

@@ -102,6 +102,7 @@ div.agentIndicator {
div.chatBox {
padding: 0;
height: min-content;
}
}
@@ -116,6 +117,7 @@ div.agentIndicator {
div.chatBox {
padding: 0;
height: min-content;
}
div.chatLayout {

View File

@@ -14,6 +14,10 @@ const nextConfig = {
source: '/auth/:path*',
destination: 'http://localhost:42110/auth/:path*',
},
{
source: '/static/:path*',
destination: 'http://localhost:42110/static/:path*',
},
];
},
trailingSlash: true,

View File

@@ -323,7 +323,7 @@ async def aget_relevant_output_modes(query: str, conversation_history: dict, is_
response = await send_message_to_model_wrapper(relevant_mode_prompt)
try:
response = response.strip()
response = response.strip().strip('"')
if is_none_or_empty(response):
return ConversationCommand.Text