Compare commits

..

4 commits

Author SHA1 Message Date
sabaimran
7f5bf35806 Disambiguate renewal_date type. Previously, being used as None, False, and Datetime in different places.
Some checks failed
dockerize / Publish Khoj Docker Images (push) Waiting to run
build and deploy github pages for documentation / deploy (push) Waiting to run
pypi / Publish Python Package to PyPI (push) Waiting to run
pre-commit / Setup Application and Lint (push) Has been cancelled
test / Run Tests (push) Has been cancelled
2024-11-22 12:06:20 -08:00
sabaimran
5e8c824ecc Improve the experience for finding past conversation
- add a conversation title search filter, and an agents filter, for finding conversations
- in the chat session api, return relevant agent style data
2024-11-22 12:03:01 -08:00
sabaimran
a761865724 Fix handling of customer.subscription.updated event to process new renewal end date 2024-11-22 12:03:01 -08:00
sabaimran
6a054d884b Add quicker/easier filtering on auth 2024-11-22 12:03:01 -08:00
5 changed files with 209 additions and 17 deletions

View file

@ -2,7 +2,8 @@
import styles from "./sidePanel.module.css"; import styles from "./sidePanel.module.css";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useRef } from "react";
import { mutate } from "swr"; import { mutate } from "swr";
@ -57,12 +58,16 @@ import {
UserCirclePlus, UserCirclePlus,
Sidebar, Sidebar,
NotePencil, NotePencil,
FunnelSimple,
MagnifyingGlass,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
interface ChatHistory { interface ChatHistory {
conversation_id: string; conversation_id: string;
slug: string; slug: string;
agent_name: string; agent_name: string;
agent_icon: string;
agent_color: string;
compressed: boolean; compressed: boolean;
created: string; created: string;
updated: string; updated: string;
@ -71,8 +76,11 @@ interface ChatHistory {
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
@ -96,6 +104,8 @@ import { modifyFileFilterForConversation } from "@/app/common/chatFunctions";
import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area"; import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area";
import { KhojLogoType } from "@/app/components/logo/khojLogo"; import { KhojLogoType } from "@/app/components/logo/khojLogo";
import NavMenu from "@/app/components/navMenu/navMenu"; import NavMenu from "@/app/components/navMenu/navMenu";
import { getIconFromIconName } from "@/app/common/iconUtils";
import AgentProfileCard from "../profileCard/profileCard";
// Define a fetcher function // Define a fetcher function
const fetcher = (url: string) => const fetcher = (url: string) =>
@ -407,6 +417,12 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
return ( return (
<> <>
<div> <div>
{props.data && props.data.length > 5 && (
<ChatSessionsModal
data={props.organizedData}
showSidePanel={props.setEnabled}
/>
)}
<ScrollArea> <ScrollArea>
<ScrollAreaScrollbar <ScrollAreaScrollbar
orientation="vertical" orientation="vertical"
@ -436,6 +452,8 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
} }
slug={chatHistory.slug} slug={chatHistory.slug}
agent_name={chatHistory.agent_name} agent_name={chatHistory.agent_name}
agent_color={chatHistory.agent_color}
agent_icon={chatHistory.agent_icon}
showSidePanel={props.setEnabled} showSidePanel={props.setEnabled}
/> />
), ),
@ -444,12 +462,6 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
))} ))}
</div> </div>
</ScrollArea> </ScrollArea>
{props.data && props.data.length > 5 && (
<ChatSessionsModal
data={props.organizedData}
showSidePanel={props.setEnabled}
/>
)}
</div> </div>
<FilesMenu <FilesMenu
conversationId={props.conversationId} conversationId={props.conversationId}
@ -683,28 +695,150 @@ interface ChatSessionsModalProps {
showSidePanel: (isEnabled: boolean) => void; showSidePanel: (isEnabled: boolean) => void;
} }
interface AgentStyle {
color: string;
icon: string;
}
function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) { function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) {
const [agentsFilter, setAgentsFilter] = useState<string[]>([]);
const [agentOptions, setAgentOptions] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState<string>("");
const [agentNameToStyleMap, setAgentNameToStyleMap] = useState<Record<string, AgentStyle>>({});
useEffect(() => {
if (data) {
const agents: string[] = [];
let agentNameToStyleMapLocal: Record<string, AgentStyle> = {};
Object.keys(data).forEach((timeGrouping) => {
data[timeGrouping].forEach((chatHistory) => {
if (!agents.includes(chatHistory.agent_name) && chatHistory.agent_name) {
agents.push(chatHistory.agent_name);
agentNameToStyleMapLocal = {
...agentNameToStyleMapLocal,
[chatHistory.agent_name]: {
color: chatHistory.agent_color,
icon: chatHistory.agent_icon,
},
};
}
});
});
console.log(agentNameToStyleMapLocal);
setAgentNameToStyleMap(agentNameToStyleMapLocal);
setAgentOptions(agents);
}
}, [data]);
// Memoize the filtered results
const filteredData = useMemo(() => {
if (!data) return null;
// Early return if no filters active
if (agentsFilter.length === 0 && searchQuery.length === 0) {
return data;
}
const filtered: GroupedChatHistory = {};
const agentSet = new Set(agentsFilter);
const searchLower = searchQuery.toLowerCase();
for (const timeGrouping in data) {
const matches = data[timeGrouping].filter((chatHistory) => {
// Early return for agent filter
if (agentsFilter.length > 0 && !agentSet.has(chatHistory.agent_name)) {
return false;
}
// Early return for search query
if (searchQuery && !chatHistory.slug?.toLowerCase().includes(searchLower)) {
return false;
}
return true;
});
if (matches.length > 0) {
filtered[timeGrouping] = matches;
}
}
return filtered;
}, [data, agentsFilter, searchQuery]);
return ( return (
<Dialog> <Dialog>
<DialogTrigger className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-4 text-sm p-[0.5rem]"> <DialogTrigger className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-1 text-sm p-[0.1rem]">
<span className="mr-2"> <span className="flex items-center gap-1">
See All <ArrowRight className="inline h-4 w-4" weight="bold" /> <MagnifyingGlass className="inline h-4 w-4 mr-1" weight="bold" /> Find
Conversation
</span> </span>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>All Conversations</DialogTitle> <DialogTitle>All Conversations</DialogTitle>
<DialogDescription className="p-0"> <DialogDescription className="p-0">
<div className="flex flex-row justify-between mt-2 gap-2">
<Input
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
placeholder="Search conversations"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className={`p-0 px-1 ${agentsFilter.length > 0 ? "bg-muted text-muted-foreground" : "bg-inherit"} `}
>
<FunnelSimple />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{/* <ScrollArea className="h-[200px]"> */}
<DropdownMenuLabel>Agents</DropdownMenuLabel>
<DropdownMenuSeparator />
{agentOptions.map((agent) => (
<DropdownMenuCheckboxItem
key={agent}
onSelect={(e) => e.preventDefault()}
checked={agentsFilter.includes(agent)}
onCheckedChange={(checked) => {
if (checked) {
setAgentsFilter([...agentsFilter, agent]);
} else {
setAgentsFilter(
agentsFilter.filter((a) => a !== agent),
);
}
}}
>
<div className="flex items-center justify-center px-1">
{getIconFromIconName(
agentNameToStyleMap[agent]?.icon,
agentNameToStyleMap[agent]?.color,
)}
<div className="break-words">{agent}</div>
</div>
</DropdownMenuCheckboxItem>
))}
{/* </ScrollArea> */}
</DropdownMenuContent>
</DropdownMenu>
</div>
<ScrollArea className="h-[500px] py-4"> <ScrollArea className="h-[500px] py-4">
{data && {filteredData &&
Object.keys(data).map((timeGrouping) => ( Object.keys(filteredData).map((timeGrouping) => (
<div key={timeGrouping}> <div key={timeGrouping}>
<div <div
className={`text-muted-foreground text-sm font-bold p-[0.5rem] `} className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}
> >
{timeGrouping} {timeGrouping}
</div> </div>
{data[timeGrouping].map((chatHistory) => ( {filteredData[timeGrouping].map((chatHistory) => (
<ChatSession <ChatSession
updated={chatHistory.updated} updated={chatHistory.updated}
created={chatHistory.created} created={chatHistory.created}
@ -713,6 +847,8 @@ function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) {
conversation_id={chatHistory.conversation_id} conversation_id={chatHistory.conversation_id}
slug={chatHistory.slug} slug={chatHistory.slug}
agent_name={chatHistory.agent_name} agent_name={chatHistory.agent_name}
agent_color={chatHistory.agent_color}
agent_icon={chatHistory.agent_icon}
showSidePanel={showSidePanel} showSidePanel={showSidePanel}
/> />
))} ))}

View file

@ -345,7 +345,7 @@ async def set_user_subscription(
user_subscription.type = type user_subscription.type = type
if is_recurring is not None: if is_recurring is not None:
user_subscription.is_recurring = is_recurring user_subscription.is_recurring = is_recurring
if renewal_date is False: if renewal_date is None:
user_subscription.renewal_date = None user_subscription.renewal_date = None
elif renewal_date is not None: elif renewal_date is not None:
user_subscription.renewal_date = renewal_date user_subscription.renewal_date = renewal_date

View file

@ -1,5 +1,6 @@
import csv import csv
import json import json
from datetime import date, datetime, timedelta, timezone
from apscheduler.job import Job from apscheduler.job import Job
from django.contrib import admin, messages from django.contrib import admin, messages
@ -65,6 +66,38 @@ admin.site.register(DjangoJob, KhojDjangoJobAdmin)
class KhojUserAdmin(UserAdmin): class KhojUserAdmin(UserAdmin):
class DateJoinedAfterFilter(admin.SimpleListFilter):
title = "Joined after"
parameter_name = "joined_after"
def lookups(self, request, model_admin):
return (
("1d", "Last 24 hours"),
("7d", "Last 7 days"),
("30d", "Last 30 days"),
("90d", "Last 90 days"),
)
def queryset(self, request, queryset):
if self.value():
days = int(self.value().rstrip("d"))
date_threshold = datetime.now() - timedelta(days=days)
return queryset.filter(date_joined__gte=date_threshold)
return queryset
class HasGoogleAuthFilter(admin.SimpleListFilter):
title = "Has Google Auth"
parameter_name = "has_google_auth"
def lookups(self, request, model_admin):
return (("True", "True"), ("False", "False"))
def queryset(self, request, queryset):
if self.value() == "True":
return queryset.filter(googleuser__isnull=False)
if self.value() == "False":
return queryset.filter(googleuser__isnull=True)
list_display = ( list_display = (
"id", "id",
"email", "email",
@ -78,6 +111,12 @@ class KhojUserAdmin(UserAdmin):
search_fields = ("email", "username", "phone_number", "uuid") search_fields = ("email", "username", "phone_number", "uuid")
filter_horizontal = ("groups", "user_permissions") filter_horizontal = ("groups", "user_permissions")
list_filter = (
HasGoogleAuthFilter,
DateJoinedAfterFilter,
"verified_email",
) + UserAdmin.list_filter
fieldsets = ( fieldsets = (
( (
"Personal info", "Personal info",

View file

@ -432,7 +432,15 @@ def chat_sessions(
conversations = conversations[:8] conversations = conversations[:8]
sessions = conversations.values_list( sessions = conversations.values_list(
"id", "slug", "title", "agent__slug", "agent__name", "created_at", "updated_at" "id",
"slug",
"title",
"agent__slug",
"agent__name",
"created_at",
"updated_at",
"agent__style_icon",
"agent__style_color",
) )
session_values = [ session_values = [
@ -442,6 +450,8 @@ def chat_sessions(
"agent_name": session[4], "agent_name": session[4],
"created": session[5].strftime("%Y-%m-%d %H:%M:%S"), "created": session[5].strftime("%Y-%m-%d %H:%M:%S"),
"updated": session[6].strftime("%Y-%m-%d %H:%M:%S"), "updated": session[6].strftime("%Y-%m-%d %H:%M:%S"),
"agent_icon": session[7],
"agent_color": session[8],
} }
for session in sessions for session in sessions
] ]

View file

@ -66,16 +66,23 @@ async def subscribe(request: Request):
success = user is not None success = user is not None
elif event_type in {"customer.subscription.updated"}: elif event_type in {"customer.subscription.updated"}:
user_subscription = await sync_to_async(adapters.get_user_subscription)(customer_email) user_subscription = await sync_to_async(adapters.get_user_subscription)(customer_email)
renewal_date = None
if subscription["current_period_end"]:
renewal_date = datetime.fromtimestamp(subscription["current_period_end"], tz=timezone.utc)
# Allow updating subscription status if paid user # Allow updating subscription status if paid user
if user_subscription and user_subscription.renewal_date: if user_subscription and user_subscription.renewal_date:
# Mark user as unsubscribed or resubscribed # Mark user as unsubscribed or resubscribed
is_recurring = not subscription["cancel_at_period_end"] is_recurring = not subscription["cancel_at_period_end"]
user, is_new = await adapters.set_user_subscription(customer_email, is_recurring=is_recurring) user, is_new = await adapters.set_user_subscription(
customer_email, is_recurring=is_recurring, renewal_date=renewal_date
)
success = user is not None success = user is not None
elif event_type in {"customer.subscription.deleted"}: elif event_type in {"customer.subscription.deleted"}:
# Reset the user to trial state # Reset the user to trial state
user, is_new = await adapters.set_user_subscription( user, is_new = await adapters.set_user_subscription(
customer_email, is_recurring=False, renewal_date=False, type=Subscription.Type.TRIAL customer_email, is_recurring=False, renewal_date=None, type=Subscription.Type.TRIAL
) )
success = user is not None success = user is not None