mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-17 02:03:00 -05:00
Compare commits
3 Commits
dependabot
...
otto/copil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f55e1cd7c | ||
|
|
1681a84c2f | ||
|
|
275950c98c |
55
autogpt_platform/backend/poetry.lock
generated
55
autogpt_platform/backend/poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aio-pika"
|
||||
@@ -374,7 +374,7 @@ description = "LTS Port of Python audioop"
|
||||
optional = false
|
||||
python-versions = ">=3.13"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.13\""
|
||||
markers = "python_version >= \"3.13\""
|
||||
files = [
|
||||
{file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800"},
|
||||
{file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303"},
|
||||
@@ -474,7 +474,7 @@ description = "Backport of asyncio.Runner, a context manager that controls event
|
||||
optional = false
|
||||
python-versions = "<3.11,>=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.10\""
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"},
|
||||
{file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"},
|
||||
@@ -487,7 +487,7 @@ description = "Backport of CPython tarfile module"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"3.12\""
|
||||
markers = "python_version <= \"3.11\""
|
||||
files = [
|
||||
{file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"},
|
||||
{file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"},
|
||||
@@ -659,6 +659,7 @@ description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\" or sys_platform == \"darwin\""
|
||||
files = [
|
||||
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
||||
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
||||
@@ -1360,7 +1361,7 @@ description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
|
||||
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
|
||||
@@ -1389,21 +1390,18 @@ tests = ["coverage", "coveralls", "dill", "mock", "nose"]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.4.0"
|
||||
version = "38.3.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "faker-40.4.0-py3-none-any.whl", hash = "sha256:486d43c67ebbb136bc932406418744f9a0bdf2c07f77703ea78b58b77e9aa443"},
|
||||
{file = "faker-40.4.0.tar.gz", hash = "sha256:76f8e74a3df28c3e2ec2caafa956e19e37a132fdc7ea067bc41783affcfee364"},
|
||||
{file = "faker-38.3.0-py3-none-any.whl", hash = "sha256:b5499b6f2d090dec9b90ded8eae6a27daed10fda3e0dba7a182bb2be8a4d4a6b"},
|
||||
{file = "faker-38.3.0.tar.gz", hash = "sha256:4997de214510db9328ed83b9591891c66e91d7e9a903e5744e0255a68f51ac16"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
tzdata = ["tzdata"]
|
||||
tzdata = "*"
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
@@ -1845,16 +1843,16 @@ files = [
|
||||
google-auth = ">=2.14.1,<3.0.0"
|
||||
googleapis-common-protos = ">=1.56.2,<2.0.0"
|
||||
grpcio = [
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
{version = ">=1.33.2,<2.0.0", optional = true, markers = "extra == \"grpc\""},
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
]
|
||||
grpcio-status = [
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
{version = ">=1.33.2,<2.0.0", optional = true, markers = "extra == \"grpc\""},
|
||||
{version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""},
|
||||
]
|
||||
proto-plus = [
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
]
|
||||
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
requests = ">=2.18.0,<3.0.0"
|
||||
@@ -1965,8 +1963,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras
|
||||
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
|
||||
grpcio = ">=1.33.2,<2.0.0"
|
||||
proto-plus = [
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
]
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
|
||||
@@ -2026,9 +2024,9 @@ google-cloud-core = ">=2.0.0,<3.0.0"
|
||||
grpc-google-iam-v1 = ">=0.12.4,<1.0.0"
|
||||
opentelemetry-api = ">=1.9.0"
|
||||
proto-plus = [
|
||||
{version = ">=1.22.0,<2.0.0"},
|
||||
{version = ">=1.22.2,<2.0.0", markers = "python_version >= \"3.11\""},
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.2,<2.0.0", markers = "python_version >= \"3.11\" and python_version < \"3.13\""},
|
||||
{version = ">=1.22.0,<2.0.0", markers = "python_version < \"3.11\""},
|
||||
]
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
|
||||
@@ -3872,7 +3870,7 @@ description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.10\""
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"},
|
||||
{file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"},
|
||||
@@ -4357,9 +4355,9 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
python-dateutil = ">=2.8.2"
|
||||
pytz = ">=2020.1"
|
||||
@@ -4602,8 +4600,8 @@ pinecone-plugin-interface = ">=0.0.7,<0.0.8"
|
||||
python-dateutil = ">=2.5.3"
|
||||
typing-extensions = ">=3.7.4"
|
||||
urllib3 = [
|
||||
{version = ">=1.26.5", markers = "python_version >= \"3.12\" and python_version < \"4.0\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""},
|
||||
{version = ">=1.26.5", markers = "python_version >= \"3.12\" and python_version < \"4.0\""},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -5431,7 +5429,7 @@ description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "implementation_name != \"PyPy\""
|
||||
markers = "(platform_python_implementation != \"PyPy\" or sys_platform == \"darwin\") and implementation_name != \"PyPy\""
|
||||
files = [
|
||||
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
|
||||
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
|
||||
@@ -6200,10 +6198,10 @@ files = [
|
||||
grpcio = ">=1.41.0"
|
||||
httpx = {version = ">=0.20.0", extras = ["http2"]}
|
||||
numpy = [
|
||||
{version = ">=2.1.0", markers = "python_version == \"3.13\""},
|
||||
{version = ">=1.21", markers = "python_version == \"3.11\""},
|
||||
{version = ">=1.26", markers = "python_version == \"3.12\""},
|
||||
{version = ">=1.21,<2.3.0", markers = "python_version == \"3.10\""},
|
||||
{version = ">=1.21", markers = "python_version == \"3.11\""},
|
||||
{version = ">=2.1.0", markers = "python_version == \"3.13\""},
|
||||
{version = ">=1.26", markers = "python_version == \"3.12\""},
|
||||
]
|
||||
portalocker = ">=2.7.0,<4.0"
|
||||
protobuf = ">=3.20.0"
|
||||
@@ -7409,7 +7407,7 @@ description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"},
|
||||
{file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"},
|
||||
@@ -7585,7 +7583,6 @@ files = [
|
||||
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
|
||||
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
|
||||
]
|
||||
markers = {dev = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
@@ -8533,4 +8530,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "73d5ca72e97dce394fe111708cb5be0c714f9812b016de45ad0b84de85a833bc"
|
||||
content-hash = "55e095de555482f0fe47de7695f390fe93e7bcf739b31c391b2e5e3c3d938ae3"
|
||||
|
||||
@@ -93,7 +93,7 @@ posthog = "^7.6.0"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
aiohappyeyeballs = "^2.6.1"
|
||||
black = "^24.10.0"
|
||||
faker = "^40.4.0"
|
||||
faker = "^38.2.0"
|
||||
httpx = "^0.28.1"
|
||||
isort = "^5.13.2"
|
||||
poethepoet = "^0.41.0"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
// TODO: Replace with modern Dialog component when available
|
||||
import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog";
|
||||
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
||||
import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar";
|
||||
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
|
||||
@@ -97,13 +97,47 @@ export function CopilotPage() {
|
||||
)}
|
||||
{/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */}
|
||||
{isMobile && (
|
||||
<DeleteConfirmDialog
|
||||
entityType="chat"
|
||||
entityName={sessionToDelete?.title || "Untitled chat"}
|
||||
open={!!sessionToDelete}
|
||||
onOpenChange={(open) => !open && handleCancelDelete()}
|
||||
onDoDelete={handleConfirmDelete}
|
||||
/>
|
||||
<Dialog
|
||||
title="Delete chat"
|
||||
controlled={{
|
||||
isOpen: !!sessionToDelete,
|
||||
set: async (open) => {
|
||||
if (!open && !isDeleting) {
|
||||
handleCancelDelete();
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClose={handleCancelDelete}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<p className="text-neutral-600">
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-medium">
|
||||
"{sessionToDelete?.title || "Untitled chat"}"
|
||||
</span>
|
||||
? This action cannot be undone.
|
||||
</p>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={handleCancelDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={handleConfirmDelete}
|
||||
loading={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
)}
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
@@ -8,8 +8,7 @@ import { Button } from "@/components/atoms/Button/Button";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||
// TODO: Replace with modern Dialog component when available
|
||||
import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog";
|
||||
import { DeleteChatDialog } from "../DeleteChatDialog";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -92,6 +91,12 @@ export function ChatSidebar() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelDelete() {
|
||||
if (!isDeleting) {
|
||||
setSessionToDelete(null);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
@@ -257,12 +262,11 @@ export function ChatSidebar() {
|
||||
)}
|
||||
</Sidebar>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
entityType="chat"
|
||||
entityName={sessionToDelete?.title || "Untitled chat"}
|
||||
open={!!sessionToDelete}
|
||||
onOpenChange={(open) => !open && setSessionToDelete(null)}
|
||||
onDoDelete={handleConfirmDelete}
|
||||
<DeleteChatDialog
|
||||
session={sessionToDelete}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={handleCancelDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { expect, test, describe, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
cleanup,
|
||||
} from "@testing-library/react";
|
||||
import { ChatSidebar } from "../ChatSidebar";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import {
|
||||
getGetV2ListSessionsMockHandler,
|
||||
getDeleteV2DeleteSessionMockHandler204,
|
||||
getDeleteV2DeleteSessionMockHandler422,
|
||||
} from "@/app/api/__generated__/endpoints/chat/chat.msw";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { NuqsTestingAdapter } from "nuqs/adapters/testing";
|
||||
import { http, HttpResponse, delay } from "msw";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
// Mock sessions data
|
||||
const mockSessions = {
|
||||
sessions: [
|
||||
{
|
||||
id: "session-1",
|
||||
title: "First Chat",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "session-2",
|
||||
title: "Second Chat",
|
||||
created_at: new Date(Date.now() - 86400000).toISOString(),
|
||||
updated_at: new Date(Date.now() - 86400000).toISOString(),
|
||||
},
|
||||
{
|
||||
id: "session-3",
|
||||
title: null,
|
||||
created_at: new Date(Date.now() - 172800000).toISOString(),
|
||||
updated_at: new Date(Date.now() - 172800000).toISOString(),
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
};
|
||||
|
||||
function createTestQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function TestWrapper({
|
||||
children,
|
||||
searchParams = "",
|
||||
onUrlUpdate,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
searchParams?: string;
|
||||
onUrlUpdate?: (event: { queryString: string }) => void;
|
||||
}) {
|
||||
const queryClient = createTestQueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BackendAPIProvider>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={searchParams}
|
||||
hasMemory
|
||||
onUrlUpdate={onUrlUpdate}
|
||||
>
|
||||
<SidebarProvider defaultOpen={true}>{children}</SidebarProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</BackendAPIProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function renderChatSidebar(
|
||||
searchParams = "",
|
||||
onUrlUpdate?: (event: { queryString: string }) => void,
|
||||
) {
|
||||
return render(
|
||||
<TestWrapper searchParams={searchParams} onUrlUpdate={onUrlUpdate}>
|
||||
<ChatSidebar />
|
||||
</TestWrapper>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("ChatSidebar", () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
getGetV2ListSessionsMockHandler(() => mockSessions),
|
||||
getDeleteV2DeleteSessionMockHandler204(),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("Sessions List", () => {
|
||||
test("renders session list correctly", async () => {
|
||||
renderChatSidebar();
|
||||
|
||||
// Use getAllByText since component may render multiple times
|
||||
await waitFor(() => {
|
||||
const elements = screen.getAllByText("First Chat");
|
||||
expect(elements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(screen.getAllByText("Second Chat").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Untitled chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("shows empty state when no sessions", async () => {
|
||||
server.use(
|
||||
getGetV2ListSessionsMockHandler(() => ({
|
||||
sessions: [],
|
||||
total: 0,
|
||||
})),
|
||||
);
|
||||
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getAllByText("No conversations yet").length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("formats dates correctly", async () => {
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
expect(screen.getAllByText("Today").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Yesterday").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("2 days ago").length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Delete Dialog", () => {
|
||||
test("opens delete dialog when trash button is clicked", async () => {
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Find and click the delete button for the first session
|
||||
const deleteButtons = screen.getAllByLabelText("Delete chat");
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
// Dialog should appear with confirmation text
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete/),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("closes dialog when cancel is clicked", async () => {
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButtons = screen.getAllByLabelText("Delete chat");
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete/),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/Are you sure you want to delete/),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("calls delete API when delete button is clicked", async () => {
|
||||
const deleteMock = vi.fn();
|
||||
server.use(
|
||||
http.delete(
|
||||
"http://localhost:3000/api/proxy/api/chat/sessions/:sessionId",
|
||||
async ({ params }) => {
|
||||
deleteMock(params.sessionId);
|
||||
return new HttpResponse(null, { status: 204 });
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButtons = screen.getAllByLabelText("Delete chat");
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete/),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete" });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalledWith("session-1");
|
||||
});
|
||||
});
|
||||
|
||||
test("closes dialog after successful deletion", async () => {
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButtons = screen.getAllByLabelText("Delete chat");
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete/),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete" });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/Are you sure you want to delete/),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("handles deletion error gracefully", async () => {
|
||||
server.use(getDeleteV2DeleteSessionMockHandler422());
|
||||
|
||||
renderChatSidebar();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const deleteButtons = screen.getAllByLabelText("Delete chat");
|
||||
fireEvent.click(deleteButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Are you sure you want to delete/),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete" });
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
// Dialog should close even on error
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(/Are you sure you want to delete/),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Session Selection", () => {
|
||||
test("highlights currently selected session", async () => {
|
||||
renderChatSidebar("?sessionId=session-1");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Find the session with selected styling (text-zinc-600 indicates selected)
|
||||
const selectedSessions = screen.getAllByText("First Chat");
|
||||
const hasSelectedStyle = selectedSessions.some(
|
||||
(el) =>
|
||||
el.className.includes("text-zinc-600") ||
|
||||
el.closest("div")?.className.includes("bg-zinc-100"),
|
||||
);
|
||||
expect(hasSelectedStyle).toBe(true);
|
||||
});
|
||||
|
||||
test("selects session when clicked", async () => {
|
||||
const urlUpdateSpy = vi.fn();
|
||||
|
||||
renderChatSidebar("", urlUpdateSpy);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Find a session button and click it
|
||||
const sessionElements = screen.getAllByText("First Chat");
|
||||
const sessionButton = sessionElements[0].closest("button");
|
||||
if (sessionButton) {
|
||||
fireEvent.click(sessionButton);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(urlUpdateSpy).toHaveBeenCalled();
|
||||
const lastCall =
|
||||
urlUpdateSpy.mock.calls[urlUpdateSpy.mock.calls.length - 1];
|
||||
expect(lastCall[0].queryString).toContain("sessionId=session-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("New Chat", () => {
|
||||
test("clears session selection when new chat button is clicked", async () => {
|
||||
const urlUpdateSpy = vi.fn();
|
||||
|
||||
renderChatSidebar("?sessionId=session-1", urlUpdateSpy);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("First Chat").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const newChatButton = screen.getByRole("button", { name: "New Chat" });
|
||||
fireEvent.click(newChatButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(urlUpdateSpy).toHaveBeenCalled();
|
||||
const lastCall =
|
||||
urlUpdateSpy.mock.calls[urlUpdateSpy.mock.calls.length - 1];
|
||||
expect(lastCall[0].queryString).not.toContain("sessionId=session-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
|
||||
interface DeleteChatDialogProps {
|
||||
/** The session to delete, or null if dialog should be closed */
|
||||
session: { id: string; title: string | null | undefined } | null;
|
||||
/** Whether deletion is in progress */
|
||||
isDeleting: boolean;
|
||||
/** Called when user confirms deletion */
|
||||
onConfirm: () => void;
|
||||
/** Called when user cancels (only works when not deleting) */
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function DeleteChatDialog({
|
||||
session,
|
||||
isDeleting,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: DeleteChatDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
title="Delete chat"
|
||||
controlled={{
|
||||
isOpen: !!session,
|
||||
set: async (open) => {
|
||||
if (!open && !isDeleting) {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
}}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<p className="text-neutral-600">
|
||||
Are you sure you want to delete{" "}
|
||||
<span className="font-medium">
|
||||
"{session?.title || "Untitled chat"}"
|
||||
</span>
|
||||
? This action cannot be undone.
|
||||
</p>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={onCancel}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onConfirm}
|
||||
loading={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DeleteChatDialog } from "./DeleteChatDialog";
|
||||
Reference in New Issue
Block a user