Skip to content

Commit 53bcb2a

Browse files
committed
feat: decouple components from plugins by using slots architecture
1 parent 6ec952b commit 53bcb2a

File tree

28 files changed

+441
-231
lines changed

28 files changed

+441
-231
lines changed

typescript/ui/__tests__/integration/intergation.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import FeedbackForm from "../../src/plugins/FeedbackPlugin/components/FeedbackFo
3333
import { createStore } from "zustand";
3434
import { useHistoryStore } from "../../src/core/stores/HistoryStore/useHistoryStore";
3535
import { HistoryStore } from "../../src/core/types/history";
36-
import { API_URL } from "../../src/config";
36+
import { API_URL } from "../../src/core/config";
3737
import { createHistoryStore } from "../../src/ragbits/stores/HistoryStore/historyStore";
3838

3939
vi.mock("../../src/core/stores/HistoryStore/useHistoryStore", () => {

typescript/ui/__tests__/unit/ChatMessage.test.tsx

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { describe, it, expect, beforeEach, vi, Mock } from "vitest";
33
import ChatMessage from "../../src/core/components/ChatMessage/ChatMessage";
44
import { MessageRole } from "@ragbits/api-client-react";
55
import { enableMapSet } from "immer";
6-
import PluginWrapper from "../../src/core/utils/plugins/PluginWrapper";
76
import { ComponentProps, PropsWithChildren } from "react";
7+
import { Slot } from "../../src/core/components/Slot";
88

99
vi.mock("../../src/core/stores/HistoryStore/useHistoryStore", () => {
1010
return {
@@ -16,6 +16,9 @@ vi.mock("../../src/core/stores/HistoryStore/selectors", () => {
1616
return {
1717
useConversationProperty: vi.fn(),
1818
useMessage: vi.fn(),
19+
useHistoryActions: vi.fn(() => ({
20+
sendSilentConfirmation: vi.fn(),
21+
})),
1922
};
2023
});
2124

@@ -93,18 +96,17 @@ function mockStore(
9396
});
9497
}
9598

96-
const COMPONENT_TEST_ID: Record<string, string> = {
97-
FeedbackForm: "feedback-form",
98-
UsageButton: "usage-button",
99-
};
100-
101-
vi.mock("../../src/core/utils/plugins/PluginWrapper.tsx", () => ({
102-
default: ({ component }: ComponentProps<typeof PluginWrapper>) => {
103-
return (
104-
<div data-testid={COMPONENT_TEST_ID[component]} data-plugin={component}>
105-
{component}
106-
</div>
107-
);
99+
vi.mock("../../src/core/components/Slot.tsx", () => ({
100+
Slot: ({ name }: ComponentProps<typeof Slot>) => {
101+
if (name === "message.actions") {
102+
return (
103+
<>
104+
<div data-testid="feedback-form">FeedbackForm</div>
105+
<div data-testid="usage-button">UsageButton</div>
106+
</>
107+
);
108+
}
109+
return null;
108110
},
109111
}));
110112

@@ -142,38 +144,13 @@ describe("ChatMessage", () => {
142144

143145
it("shows feedback from when enabled", () => {
144146
mockStore(MessageRole.Assistant);
145-
vi.mock(
146-
"../../src/core/contexts/ConfigContex/useConfigContext.tsx",
147-
() => ({
148-
config: {
149-
feedback: {
150-
like: {
151-
enabled: true,
152-
form: null,
153-
},
154-
dislike: {
155-
enabled: true,
156-
form: null,
157-
},
158-
},
159-
},
160-
}),
161-
);
162147

163148
render(<ChatMessage messageId={MessageRole.Assistant} />);
164149
expect(screen.getByTestId("feedback-form")).toBeInTheDocument();
165150
});
166151

167152
it("shows usage button when enabled", () => {
168153
mockStore(MessageRole.Assistant);
169-
vi.mock(
170-
"../../src/core/contexts/ConfigContex/useConfigContext.tsx",
171-
() => ({
172-
config: {
173-
show_usage: true,
174-
},
175-
}),
176-
);
177154

178155
render(<ChatMessage messageId={MessageRole.Assistant} />);
179156
expect(screen.getByTestId("usage-button")).toBeInTheDocument();

typescript/ui/__tests__/unit/Layout.test.tsx

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { render, screen, waitFor } from "@testing-library/react";
2-
import { describe, expect, it, Mock, vi } from "vitest";
2+
import { describe, expect, it, Mock, vi, beforeEach, afterEach } from "vitest";
33
import userEvent from "@testing-library/user-event";
44
import Layout from "../../src/core/components/Layout";
5+
import { Slot } from "../../src/core/components/Slot";
6+
import { ComponentProps } from "react";
7+
58
vi.mock("../../src/core/components/DebugPanel", () => ({
69
default: ({ isOpen }: { isOpen: boolean }) => (
710
<div>isOpen: {JSON.stringify(isOpen)}</div>
@@ -26,31 +29,39 @@ vi.mock("../../src/core/stores/HistoryStore/selectors", () => {
2629
};
2730
});
2831

29-
vi.mock(
30-
"../../src/plugins/ChatHistoryPlugin/components/ChatHistory.tsx",
31-
() => ({
32-
default: () => <div>ChatHistory</div>,
33-
}),
34-
);
32+
// Track whether chat history should be enabled
33+
let chatHistoryEnabled = false;
34+
35+
vi.mock("../../src/core/components/Slot.tsx", () => ({
36+
Slot: ({ name }: ComponentProps<typeof Slot>) => {
37+
if (name === "layout.sidebar" && chatHistoryEnabled) {
38+
return <div>ChatHistory</div>;
39+
}
40+
if (name === "layout.headerActions") {
41+
return <div data-testid="header-actions-slot">HeaderActions</div>;
42+
}
43+
return null;
44+
},
45+
}));
46+
47+
vi.mock("../../src/core/utils/slots/useSlotHasFillers.ts", () => ({
48+
useSlotHasFillers: (slot: string) => {
49+
if (slot === "layout.sidebar") {
50+
return chatHistoryEnabled;
51+
}
52+
return false;
53+
},
54+
}));
3555

3656
import { useConfigContext } from "../../src/core/contexts/ConfigContext/useConfigContext";
3757
import { useThemeContext } from "../../src/core/contexts/ThemeContext/useThemeContext";
3858
import { Theme } from "../../src/core/contexts/ThemeContext/ThemeContext";
3959
import { useHistoryActions } from "../../src/core/stores/HistoryStore/selectors";
40-
import { pluginManager } from "../../src/core/utils/plugins/PluginManager";
41-
import {
42-
ChatHistoryPlugin,
43-
ChatHistoryPluginName,
44-
} from "../../src/plugins/ChatHistoryPlugin";
45-
46-
function mockConfig(
47-
isDebugEnabled: boolean = false,
48-
withClientSideHistory: boolean = false,
49-
) {
60+
61+
function mockConfig(isDebugEnabled: boolean = false) {
5062
(useConfigContext as Mock).mockReturnValue({
5163
config: {
5264
debug_mode: isDebugEnabled,
53-
conversationHistory: withClientSideHistory,
5465
},
5566
});
5667
}
@@ -63,18 +74,25 @@ function mockTheme(theme: Theme) {
6374
});
6475
}
6576

66-
const clearHistoryMock = vi.fn();
77+
const newConversationMock = vi.fn();
6778
const stopAnsweringMock = vi.fn();
6879
(useHistoryActions as Mock).mockReturnValue({
69-
clearHistory: clearHistoryMock,
80+
newConversation: newConversationMock,
7081
stopAnswering: stopAnsweringMock,
7182
});
7283

7384
describe("Layout", () => {
85+
beforeEach(() => {
86+
chatHistoryEnabled = false;
87+
});
88+
89+
afterEach(() => {
90+
vi.clearAllMocks();
91+
});
92+
7493
it("renders with chat history", async () => {
75-
pluginManager.register(ChatHistoryPlugin);
76-
pluginManager.activate(ChatHistoryPluginName);
77-
mockConfig(false, true);
94+
chatHistoryEnabled = true;
95+
mockConfig(false);
7896
mockTheme(Theme.LIGHT);
7997
render(
8098
<Layout
@@ -102,9 +120,10 @@ describe("Layout", () => {
102120
).toBeInTheDocument();
103121
expect(screen.queryByTestId("layout-debug-button")).toBeNull();
104122
});
123+
105124
it("renders without chat history", async () => {
106-
pluginManager.deactivate(ChatHistoryPluginName);
107-
mockConfig(false, false);
125+
chatHistoryEnabled = false;
126+
mockConfig(false);
108127
mockTheme(Theme.LIGHT);
109128
render(
110129
<Layout
@@ -119,9 +138,8 @@ describe("Layout", () => {
119138
expect(screen.getByText("Custom Title")).toBeInTheDocument();
120139
expect(screen.getByText("Custom Subtitle")).toBeInTheDocument();
121140
expect(screen.getByText("Custom Logo")).toBeInTheDocument();
122-
// Chat history
123141

124-
// Chat history
142+
// Chat history should not be present
125143
await waitFor(() => {
126144
expect(screen.queryByText("ChatHistory")).not.toBeInTheDocument();
127145
expect(
@@ -134,6 +152,7 @@ describe("Layout", () => {
134152
).toBeInTheDocument();
135153
expect(screen.queryByTestId("layout-debug-button")).toBeNull();
136154
});
155+
137156
it("renders without debug panel", () => {
138157
mockConfig();
139158
mockTheme(Theme.LIGHT);
@@ -197,6 +216,7 @@ describe("Layout", () => {
197216
await user.click(toggleThemeButton);
198217
expect(setThemeMock).toHaveBeenCalled();
199218
});
219+
200220
it('shows debug panel when "debug button" is clicked', async () => {
201221
mockConfig(true);
202222
mockTheme(Theme.LIGHT);

typescript/ui/__tests__/unit/PromptInput.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
22
import { describe, it, expect, vi } from "vitest";
3-
import PluginWrapper from "../../src/core/utils/plugins/PluginWrapper";
43
import { ComponentProps } from "react";
54
import HorizontalActions from "../../src/core/components/inputs/PromptInput/HorizontalActions";
65
import PromptInput from "../../src/core/components/inputs/PromptInput/PromptInput";
76
import userEvent from "@testing-library/user-event";
87
import { ChatMessage } from "../../src/core/types/history";
98
import { MessageRole } from "@ragbits/api-client";
9+
import { Slot } from "../../src/core/components/Slot";
1010

1111
vi.mock(
1212
"../../src/core/components/inputs/PromptInput/HorizontalActions.tsx",
@@ -16,10 +16,10 @@ vi.mock(
1616
}),
1717
);
1818

19-
vi.mock("../../src/core/utils/plugins/PluginWrapper.tsx", () => ({
20-
default: ({ component }: ComponentProps<typeof PluginWrapper>) => (
21-
<div data-testid="feedback-form" data-plugin={component}>
22-
{component}
19+
vi.mock("../../src/core/components/Slot.tsx", () => ({
20+
Slot: ({ name }: ComponentProps<typeof Slot>) => (
21+
<div data-testid="slot" data-slot-name={name}>
22+
{name === "prompt.beforeSend" ? "ChatOptionsForm" : name}
2323
</div>
2424
),
2525
}));

typescript/ui/__tests__/unit/plugin/ChatHistoryPlugin/ChatHistory.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { useHistoryActions } from "../../../../src/core/stores/HistoryStore/sele
2828
import { useHistoryStore } from "../../../../src/core/stores/HistoryStore/useHistoryStore";
2929
import ChatHistory from "../../../../src/plugins/ChatHistoryPlugin/components/ChatHistory";
3030
import { HistoryStore } from "../../../../src/core/types/history";
31-
import { isTemporaryConversation } from "../../../../src/ragbits/stores/HistoryStore/historyStore";
31+
import { isTemporaryConversation } from "../../../../src/core/stores/HistoryStore/utils";
3232

3333
const MOCK_CONVERSATIONS: HistoryStore["conversations"] = {
3434
"mock-id-1": {

typescript/ui/__tests__/unit/plugin/UsagePlugin/UsageButton.test.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
33
import { describe, it, expect } from "vitest";
44
import { ChatMessage } from "../../../../src/core/types/history";
55
import UsageButton from "../../../../src/plugins/UsagePlugin/components/UsageButton";
6+
import { MessageRole } from "@ragbits/api-client";
67

78
const mockUsage: Exclude<ChatMessage["usage"], undefined> = {
89
"gpt-4": {
@@ -21,17 +22,24 @@ const mockUsage: Exclude<ChatMessage["usage"], undefined> = {
2122
},
2223
};
2324

25+
const mockMessage: ChatMessage = {
26+
id: "test-message-id",
27+
role: MessageRole.Assistant,
28+
content: "Test message content",
29+
usage: mockUsage,
30+
};
31+
2432
describe("UsageButton", () => {
2533
const user = userEvent.setup();
2634
it("renders the info button", () => {
27-
render(<UsageButton usage={mockUsage} />);
35+
render(<UsageButton message={mockMessage} />);
2836
expect(
2937
screen.getByRole("button", { name: /open usage details/i }),
3038
).toBeInTheDocument();
3139
});
3240

3341
it("shows tooltip with summary info on hover", async () => {
34-
render(<UsageButton usage={mockUsage} />);
42+
render(<UsageButton message={mockMessage} />);
3543
const button = screen.getByRole("button", { name: /open usage details/i });
3644

3745
await user.tab();
@@ -46,7 +54,7 @@ describe("UsageButton", () => {
4654
});
4755

4856
it("opens modal with detailed usage table when clicked", async () => {
49-
render(<UsageButton usage={mockUsage} />);
57+
render(<UsageButton message={mockMessage} />);
5058
await user.click(
5159
screen.getByRole("button", { name: /open usage details/i }),
5260
);
@@ -76,7 +84,7 @@ describe("UsageButton", () => {
7684
});
7785

7886
it("closes modal when pressing Escape", async () => {
79-
render(<UsageButton usage={mockUsage} />);
87+
render(<UsageButton message={mockMessage} />);
8088
await user.click(
8189
screen.getByRole("button", { name: /open usage details/i }),
8290
);
@@ -89,4 +97,14 @@ describe("UsageButton", () => {
8997
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
9098
});
9199
});
100+
101+
it("returns null when message has no usage data", () => {
102+
const messageWithoutUsage: ChatMessage = {
103+
id: "test-message-id",
104+
role: MessageRole.Assistant,
105+
content: "Test message content",
106+
};
107+
const { container } = render(<UsageButton message={messageWithoutUsage} />);
108+
expect(container).toBeEmptyDOMElement();
109+
});
92110
});

typescript/ui/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import Layout from "./core/components/Layout";
22
import { useLayoutEffect, useMemo } from "react";
33
import { useConfigContext } from "./core/contexts/ConfigContext/useConfigContext";
4-
import { DEFAULT_LOGO, DEFAULT_SUBTITLE, DEFAULT_TITLE } from "./config";
4+
import { DEFAULT_LOGO, DEFAULT_SUBTITLE, DEFAULT_TITLE } from "./core/config";
55
import { Outlet } from "react-router";
66
import { isURL } from "./core/utils/media";
7+
import { usePluginActivation } from "./ragbits/PluginActivator";
78

89
const CUSTOM_FAVICON_ID = "generated-favicon";
910

1011
export default function App() {
12+
usePluginActivation();
13+
1114
const {
1215
config: { customization },
1316
} = useConfigContext();

0 commit comments

Comments
 (0)