Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/webui/frontend/src/lib/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { api } from "./client";
export * from "./projects";
export * from "./sessions";
33 changes: 33 additions & 0 deletions client/webui/frontend/src/lib/api/sessions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { api } from "./client";

export interface BulkDeleteOptions {
sessionIds?: string[];
deleteAll?: boolean;
startDate?: number;
endDate?: number;
}

export interface BulkDeleteResult {
deletedCount: number;
failedCount: number;
totalRequested: number;
}

/**
* Bulk delete sessions based on various criteria
*/
export async function bulkDeleteSessions(options: BulkDeleteOptions): Promise<BulkDeleteResult> {
return api.webui.post<BulkDeleteResult>("/api/v1/sessions/bulk-delete", {
session_ids: options.sessionIds,
delete_all: options.deleteAll,
start_date: options.startDate,
end_date: options.endDate,
});
}

/**
* Delete all sessions for the current user
*/
export async function deleteAllSessions(): Promise<BulkDeleteResult> {
return bulkDeleteSessions({ deleteAll: true });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useState } from "react";
import { Calendar } from "lucide-react";
import { Button, Label, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input } from "@/lib/components/ui";
import { DialogFooter } from "@/lib/components/ui/dialog";
import { deleteAllSessions, bulkDeleteSessions } from "@/lib/api/sessions";
import { useChatContext } from "@/lib/hooks";

export const SessionManagementSettings: React.FC = () => {
const { addNotification, handleNewSession } = useChatContext();
const [loading, setLoading] = useState(false);
const [isDeleteAllDialogOpen, setIsDeleteAllDialogOpen] = useState(false);
const [isDateRangeDialogOpen, setIsDateRangeDialogOpen] = useState(false);
const [customStartDate, setCustomStartDate] = useState("");
const [customEndDate, setCustomEndDate] = useState("");

const handleDeleteAllConfirm = async () => {
setLoading(true);
try {
const result = await deleteAllSessions();

if (result) {
addNotification(`Successfully deleted ${result.deletedCount} session${result.deletedCount !== 1 ? "s" : ""}`, "success");

if (result.failedCount > 0) {
addNotification(`Failed to delete ${result.failedCount} session${result.failedCount !== 1 ? "s" : ""}`, "warning");
}

// Start a new session after deleting all
handleNewSession();
}
} catch (error) {
console.error("Failed to delete sessions:", error);
addNotification("Failed to delete sessions", "warning");
} finally {
setLoading(false);
setIsDeleteAllDialogOpen(false);
}
};

const handleDateRangeDelete = () => {
if (!customStartDate || !customEndDate) {
addNotification("Please select both start and end dates", "warning");
return;
}

const startDate = new Date(customStartDate).getTime();
const endDate = new Date(customEndDate).getTime();

if (startDate > endDate) {
addNotification("Start date must be before end date", "warning");
return;
}

setIsDateRangeDialogOpen(true);
};

const handleDateRangeDeleteConfirm = async () => {
setLoading(true);
try {
const startDate = new Date(customStartDate).getTime();
const endDate = new Date(customEndDate).getTime();

const result = await bulkDeleteSessions({ startDate, endDate });

if (result) {
addNotification(`Successfully deleted ${result.deletedCount} session${result.deletedCount !== 1 ? "s" : ""}`, "success");

if (result.failedCount > 0) {
addNotification(`Failed to delete ${result.failedCount} session${result.failedCount !== 1 ? "s" : ""}`, "warning");
}

// Clear the date inputs
setCustomStartDate("");
setCustomEndDate("");
}
} catch (error) {
console.error("Failed to delete sessions:", error);
addNotification("Failed to delete sessions", "warning");
} finally {
setLoading(false);
setIsDateRangeDialogOpen(false);
}
};

return (
<div className="space-y-6">
{/* Delete All Sessions Section */}
<div className="space-y-4">
<div className="border-b pb-2">
<h3 className="text-lg font-semibold">Delete All Sessions</h3>
</div>

<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Label className="font-medium">Remove all chat sessions</Label>
</div>
<Button variant="outline" onClick={() => setIsDeleteAllDialogOpen(true)}>
Delete All
</Button>
</div>
</div>

{/* Delete by Date Range Section */}
<div className="space-y-4">
<div className="border-b pb-2">
<h3 className="text-lg font-semibold">Delete by Date Range</h3>
</div>

<div className="flex items-center gap-2">
<Calendar className="size-4" />
<Label className="font-medium">Select date range to delete sessions</Label>
</div>

<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Start Date</Label>
<Input type="date" value={customStartDate} onChange={e => setCustomStartDate(e.target.value)} className="dark:[color-scheme:dark]" />
</div>
<div className="space-y-2">
<Label>End Date</Label>
<Input type="date" value={customEndDate} onChange={e => setCustomEndDate(e.target.value)} className="dark:[color-scheme:dark]" />
</div>
</div>

<Button variant="outline" onClick={handleDateRangeDelete} disabled={!customStartDate || !customEndDate}>
Delete Sessions in Range
</Button>
</div>

{/* Delete All Confirmation Dialog */}
<Dialog open={isDeleteAllDialogOpen} onOpenChange={setIsDeleteAllDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete All Sessions?</DialogTitle>
<DialogDescription>Are you sure you want to delete all your chat sessions? This action is permanent and cannot be undone. All messages, artifacts, and associated data will be removed.</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button variant="ghost" onClick={() => setIsDeleteAllDialogOpen(false)} disabled={loading}>
Cancel
</Button>
<Button variant="outline" onClick={handleDeleteAllConfirm} disabled={loading}>
{loading ? "Deleting..." : "Delete All Sessions"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

{/* Date Range Confirmation Dialog */}
<Dialog open={isDateRangeDialogOpen} onOpenChange={setIsDateRangeDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Sessions in Date Range?</DialogTitle>
<DialogDescription>
Are you sure you want to delete sessions from {customStartDate} to {customEndDate}? This action is permanent and cannot be undone. All messages, artifacts, and associated data will be removed.
</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button variant="ghost" onClick={() => setIsDateRangeDialogOpen(false)} disabled={loading}>
Cancel
</Button>
<Button variant="outline" onClick={handleDateRangeDeleteConfirm} disabled={loading}>
{loading ? "Deleting..." : "Delete Sessions"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React, { useState } from "react";
import { Info, Settings, Type, Volume2 } from "lucide-react";
import { Database, Info, Settings, Type, Volume2 } from "lucide-react";

import { cn } from "@/lib/utils";
import { useConfigContext } from "@/lib/hooks";

import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger, Tooltip, TooltipContent, TooltipTrigger, VisuallyHidden } from "@/lib/components/ui";
import { SpeechSettingsPanel } from "./SpeechSettings";
import { GeneralSettings } from "./GeneralSettings";
import { SessionManagementSettings } from "./SessionManagementSettings";
import { AboutProduct } from "@/lib/components/settings/AboutProduct";

type SettingsSection = "general" | "speech" | "about";
type SettingsSection = "general" | "speech" | "sessions" | "about";

interface SidebarItemProps {
icon: React.ReactNode;
Expand All @@ -34,7 +35,7 @@ interface SettingsDialogProps {
}

export const SettingsDialog: React.FC<SettingsDialogProps> = ({ iconOnly = false, open: controlledOpen, onOpenChange }) => {
const { configFeatureEnablement } = useConfigContext();
const { configFeatureEnablement, persistenceEnabled } = useConfigContext();
const [internalOpen, setInternalOpen] = useState(false);
const [activeSection, setActiveSection] = useState<SettingsSection>("general");

Expand All @@ -56,6 +57,8 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({ iconOnly = false
return <GeneralSettings />;
case "speech":
return <SpeechSettingsPanel />;
case "sessions":
return <SessionManagementSettings />;
default:
return <GeneralSettings />;
}
Expand All @@ -69,6 +72,8 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({ iconOnly = false
return "General";
case "speech":
return "Speech";
case "sessions":
return "Session Management";
default:
return "Settings";
}
Expand Down Expand Up @@ -116,6 +121,7 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({ iconOnly = false
<div className="flex-1 space-y-1 overflow-y-auto">
<SidebarItem icon={<Type className="size-4" />} label="General" active={activeSection === "general"} onClick={() => setActiveSection("general")} />
{speechEnabled && <SidebarItem icon={<Volume2 className="size-4" />} label="Speech" active={activeSection === "speech"} onClick={() => setActiveSection("speech")} />}
{persistenceEnabled && <SidebarItem icon={<Database className="size-4" />} label="Sessions" active={activeSection === "sessions"} onClick={() => setActiveSection("sessions")} />}
</div>
{/* Bottom items, static */}
<div className="space-y-1 pb-2">
Expand Down
1 change: 1 addition & 0 deletions client/webui/frontend/src/lib/components/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { SettingsDialog } from "./SettingsDialog";
export { SpeechSettingsPanel } from "./SpeechSettings";
export { GeneralSettings } from "./GeneralSettings";
export { SessionManagementSettings } from "./SessionManagementSettings";
Loading
Loading