feat: add AgentRealtimeProvider and integrate it into WorkbenchLayout and ConversationsPage
This commit is contained in:
@@ -35,7 +35,6 @@ import {
|
||||
} from "@/components/ui/resizable";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { useAgentConversationRealtime } from "@/hooks/use-agent-conversation-realtime";
|
||||
import { useI18n } from "@/i18n/provider";
|
||||
import {
|
||||
agentConversationFilterOptions,
|
||||
@@ -135,8 +134,6 @@ export function ConversationWorkbench() {
|
||||
});
|
||||
}
|
||||
|
||||
useAgentConversationRealtime();
|
||||
|
||||
const handleSidebarToggle = () => {
|
||||
const panel = sidebarPanelRef.current;
|
||||
if (!panel) {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { AgentRealtimeProvider } from "@/components/agent-realtime-provider";
|
||||
|
||||
import { ConversationWorkbench } from "./_components/conversation-workbench";
|
||||
|
||||
export default function ConversationsPage() {
|
||||
return <ConversationWorkbench />;
|
||||
return (
|
||||
<>
|
||||
<AgentRealtimeProvider />
|
||||
<ConversationWorkbench />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from "next/navigation"
|
||||
import type { CSSProperties, ReactNode } from "react"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { AgentRealtimeProvider } from "@/components/agent-realtime-provider"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { NotificationProvider } from "@/components/notification-provider"
|
||||
import { WorkbenchHeader } from "@/components/workbench-header"
|
||||
@@ -56,6 +57,7 @@ export default function WorkbenchLayout({
|
||||
}
|
||||
>
|
||||
<NotificationProvider>
|
||||
<AgentRealtimeProvider />
|
||||
<WorkbenchRail />
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<WorkbenchHeader />
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
const providerSource = await readFile(
|
||||
new URL("./agent-realtime-provider.tsx", import.meta.url),
|
||||
"utf8",
|
||||
).catch(() => "");
|
||||
const workbenchLayoutSource = await readFile(
|
||||
new URL("../app/workbench/layout.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const dashboardConversationsPageSource = await readFile(
|
||||
new URL("../app/dashboard/conversations/page.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const conversationWorkbenchSource = await readFile(
|
||||
new URL("../app/dashboard/conversations/_components/conversation-workbench.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
describe("AgentRealtimeProvider placement", () => {
|
||||
it("owns the agent realtime hook", () => {
|
||||
assert.match(providerSource, /export function AgentRealtimeProvider/);
|
||||
assert.match(providerSource, /useAgentConversationRealtime\(\)/);
|
||||
});
|
||||
|
||||
it("is mounted at the workbench layout level", () => {
|
||||
assert.match(workbenchLayoutSource, /import \{ AgentRealtimeProvider \}/);
|
||||
assert.match(workbenchLayoutSource, /<AgentRealtimeProvider \/>/);
|
||||
});
|
||||
|
||||
it("keeps dashboard conversations realtime without tying it to ConversationWorkbench", () => {
|
||||
assert.match(dashboardConversationsPageSource, /<AgentRealtimeProvider \/>/);
|
||||
assert.doesNotMatch(conversationWorkbenchSource, /useAgentConversationRealtime/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useAgentConversationRealtime } from "@/hooks/use-agent-conversation-realtime"
|
||||
|
||||
export function AgentRealtimeProvider() {
|
||||
useAgentConversationRealtime()
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -13,4 +13,9 @@ describe("workspace switcher config", () => {
|
||||
it("wraps the dropdown label in a Base UI menu group", async () => {
|
||||
assert.match(source, /<DropdownMenuGroup>[\s\S]*<DropdownMenuLabel>\{t\("workspace\.switchWorkspace"\)\}<\/DropdownMenuLabel>/);
|
||||
});
|
||||
|
||||
it("does not auto-open the rail menu from focus events", async () => {
|
||||
assert.doesNotMatch(source, /onFocus=\{openHoverMenu\}/);
|
||||
assert.doesNotMatch(source, /onBlur=\{closeHoverMenu\}/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import type { ReactElement } from "react"
|
||||
|
||||
import { useI18n } from "@/i18n/provider"
|
||||
@@ -82,6 +82,14 @@ export function WorkspaceSwitcher({
|
||||
}, 120)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (closeTimerRef.current) {
|
||||
clearTimeout(closeTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const triggerClassName = cn(
|
||||
"gap-2 text-left",
|
||||
variant === "header" &&
|
||||
@@ -133,8 +141,6 @@ export function WorkspaceSwitcher({
|
||||
<DropdownMenuTrigger
|
||||
onPointerEnter={openHoverMenu}
|
||||
onPointerLeave={closeHoverMenu}
|
||||
onFocus={openHoverMenu}
|
||||
onBlur={closeHoverMenu}
|
||||
render={
|
||||
trigger ?? <Button variant="ghost" className={triggerClassName} />
|
||||
}
|
||||
|
||||
@@ -49,6 +49,13 @@ async function login(page: Page) {
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function expectWorkbenchRealtimeOnline(page: Page) {
|
||||
await expect(page.getByText(/平台实时:在线|Realtime: online/)).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect(page.getByText(/平台实时:已断开|Realtime: disconnected/)).toHaveCount(0);
|
||||
}
|
||||
|
||||
test.describe("support workbench", () => {
|
||||
test("logs in and switches between workbench and dashboard", async ({ page }) => {
|
||||
const runtimeErrors: string[] = [];
|
||||
@@ -65,6 +72,7 @@ test.describe("support workbench", () => {
|
||||
await page.goto(`${baseUrl}/workbench/`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await screenshot(page, "03-workbench-initial");
|
||||
await expectWorkbenchRealtimeOnline(page);
|
||||
|
||||
await expect(page.getByText(/客服工作台|Support Workbench/).first()).toBeVisible();
|
||||
const conversationsEntry = page.getByRole("link", { name: /会话|Conversations/ }).first();
|
||||
@@ -110,6 +118,7 @@ test.describe("support workbench", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(returnedTicketsEntry).toHaveClass(/bg-sidebar-primary/);
|
||||
await expect(returnedConversationsEntry).not.toHaveClass(/bg-sidebar-primary/);
|
||||
await expectWorkbenchRealtimeOnline(page);
|
||||
await page.mouse.move(640, 360);
|
||||
await screenshot(page, "09-workbench-tickets-active");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user