Skip to content
Open
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
32 changes: 32 additions & 0 deletions client/webui/frontend/src/stories/MessageAttribution.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MessageAttribution } from "@/lib/components/chat/MessageAttribution";
import { mockCollaborativeUsers } from "@/lib/mockData/collaborativeChat";

const meta: Meta<typeof MessageAttribution> = {
title: "Chat/MessageAttribution",
component: MessageAttribution,
tags: ["autodocs"],
parameters: {
layout: "padded",
},
};

export default meta;

type Story = StoryObj<typeof MessageAttribution>;

export const UserAttribution: Story = {
args: {
type: "user",
name: mockCollaborativeUsers.alice.name,
timestamp: Date.now() - 10 * 60 * 1000,
userIndex: 0,
},
};

export const AgentAttribution: Story = {
args: {
type: "agent",
name: "OrchestratorAgent",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ShareNotificationMessage } from "@/lib/components/chat/ShareNotificationMessage";
import { mockActiveCollaborativeSession } from "@/lib/mockData/collaborativeChat";

const meta: Meta<typeof ShareNotificationMessage> = {
title: "Chat/ShareNotificationMessage",
component: ShareNotificationMessage,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
};

export default meta;

type Story = StoryObj<typeof ShareNotificationMessage>;

export const Default: Story = {
args: {
sharedBy: mockActiveCollaborativeSession.sharedByName!,
sharedWith: mockActiveCollaborativeSession.sharedWithNames!,
timestamp: mockActiveCollaborativeSession.sharedAt!,
},
};

export const MultipleUsers: Story = {
args: {
sharedBy: "Olive Operations",
sharedWith: ["Parminder Procurement", "Charlie Chang", "David Developer"],
timestamp: Date.now() - 2 * 60 * 60 * 1000,
},
};
38 changes: 38 additions & 0 deletions client/webui/frontend/src/stories/UserPresenceAvatars.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { UserPresenceAvatars } from "@/lib/components/chat/UserPresenceAvatars";
import { mockCollaborativeUsers } from "@/lib/mockData/collaborativeChat";

const meta: Meta<typeof UserPresenceAvatars> = {
title: "Chat/UserPresenceAvatars",
component: UserPresenceAvatars,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
};

export default meta;

type Story = StoryObj<typeof UserPresenceAvatars>;

export const Default: Story = {
args: {
users: [
{ ...mockCollaborativeUsers.alice, isOnline: true },
{ ...mockCollaborativeUsers.bob, isOnline: true },
],
},
};

export const ManyUsers: Story = {
args: {
users: [
mockCollaborativeUsers.alice,
mockCollaborativeUsers.bob,
mockCollaborativeUsers.charlie,
{ ...mockCollaborativeUsers.alice, id: "user-4", name: "User Four", email: "user4@example.com", isOnline: false },
{ ...mockCollaborativeUsers.bob, id: "user-5", name: "User Five", email: "user5@example.com", isOnline: false },
{ ...mockCollaborativeUsers.charlie, id: "user-6", name: "User Six", email: "user6@example.com", isOnline: true },
],
},
};
83 changes: 83 additions & 0 deletions client/webui/frontend/src/stories/chat/ChatPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ChatPage } from "@/lib/components/pages/ChatPage";
import { expect, screen, userEvent, within } from "storybook/test";
import { http, HttpResponse } from "msw";
import { defaultPromptGroups } from "../data/prompts";
import type { MessageFE } from "@/lib/types/fe";

const handlers = [
http.get("*/api/v1/prompts/groups/all", () => {
Expand Down Expand Up @@ -233,3 +234,85 @@ export const AgentDropdownFiltersWorkflows: Story = {
expect(mockWorkflowElements).toBe(false);
},
};

// Mock messages for collaborative chat showing conversation between Alice, Bob, and Charlie (current user)
const collaborativeMessages: MessageFE[] = [
// Alice's messages before sharing (indices 0, 1, 2, 3)
{
isUser: true,
parts: [{ kind: "text", text: "Hi! Can you help me create a Python script to process CSV files?" }],
},
{
isUser: false,
parts: [{ kind: "text", text: "I'd be happy to help! I'll create a script that reads CSV files, processes the data, and generates reports." }],
},
{
isUser: true,
parts: [{ kind: "text", text: "Great! Can you also add error handling for missing columns?" }],
},
{
isUser: false,
parts: [
{ kind: "text", text: "I've created a Python script with comprehensive error handling for missing columns." },
{
kind: "artifact",
status: "completed",
name: "csv_processor.py",
description: "Python script for CSV processing",
file: {
name: "csv_processor.py",
mime_type: "text/x-python",
size: 2400,
last_modified: new Date().toISOString(),
},
},
],
},
// Share event will eventually be a real message type from backend
// For now, showing with ShareNotification component at end of ChatMessageList
// Bob's messages after being added (indices 4, 5, 6, 7)
{
isUser: true,
parts: [{ kind: "text", text: "Thanks for adding me! Does this handle different CSV delimiters like semicolons?" }],
},
{
isUser: false,
parts: [{ kind: "text", text: "Yes! The script uses Python's csv.Sniffer to automatically detect delimiters including semicolons, tabs, and pipes." }],
},
// Charlie's message (current user - no attribution) (index 6)
{
isUser: true,
parts: [{ kind: "text", text: "I just tested it with a semicolon-delimited file and it works perfectly!" }],
},
{
isUser: false,
parts: [{ kind: "text", text: "Great to hear! The auto-detection is working as expected." }],
},
// Alice responds (index 8)
{
isUser: true,
parts: [{ kind: "text", text: "Excellent! Can we also add summary statistics?" }],
},
{
isUser: false,
parts: [{ kind: "text", text: "Absolutely! I'll add a summary module that calculates count, mean, median, and standard deviation for numeric columns." }],
},
];

export const CollaborativeChat: Story = {
parameters: {
chatContext: {
sessionId: "mock-collaborative-session",
sessionName: "Python Script Development",
messages: collaborativeMessages,
isResponding: false,
isCancelling: false,
selectedAgentName: "OrchestratorAgent",
isSidePanelCollapsed: true,
activeSidePanelTab: "files",
},
configContext: {
persistenceEnabled: false,
},
},
};
154 changes: 154 additions & 0 deletions client/webui/frontend/src/stories/chat/ShareDialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { http, HttpResponse } from "msw";
import { ShareDialog } from "@/lib/components/share/ShareDialog";

// Mock data
const mockShareLink = {
share_id: "share-123",
share_url: "https://example.com/share/abc123xyz",
session_id: "test-session-123",
created_time: Math.floor(Date.now() / 1000) - 86400, // Created 1 day ago
require_authentication: true,
};

const mockOwner = {
owner_email: "owner@example.com",
};

// Mock viewer added 2 days ago (will show as outdated for recent sessions)
const mockViewerOld = {
user_email: "viewer@example.com",
added_at: Math.floor(Date.now() / 1000) - 172800, // Added 2 days ago
access_level: "read-only" as const,
};

// Mock viewer added recently (will show as up-to-date)
const mockViewerRecent = {
user_email: "viewer@example.com",
added_at: Math.floor(Date.now() / 1000) - 3600, // Added 1 hour ago
access_level: "read-only" as const,
};

/** Shared args across all stories */
const sharedArgs = {
sessionId: "test-session-123",
sessionTitle: "My Chat Session",
sessionUpdatedTime: new Date().toISOString(),
open: true,
onOpenChange: () => {},
onError: (error: string) => console.error(error),
onSuccess: (message: string) => console.log(message),
} satisfies Partial<typeof ShareDialog extends React.ComponentType<infer P> ? P : never>;

const meta: Meta<typeof ShareDialog> = {
title: "Chat/ShareDialog",
component: ShareDialog,
tags: ["autodocs"],
parameters: {
layout: "centered",
configContext: {
identityServiceType: "local", // Enable user typeahead
},
msw: {
handlers: [
// Get share link - correct endpoint
http.get("/api/v1/share/link/:sessionId", () => {
return HttpResponse.json(mockShareLink);
}),
// Get share users with 1 viewer - correct endpoint
http.get("/api/v1/share/:shareId/users", () => {
return HttpResponse.json({
...mockOwner,
users: [mockViewerRecent],
});
}),
// Add share users
http.post("/api/v1/share/:shareId/users", () => {
return HttpResponse.json({ success: true });
}),
// Delete share users
http.delete("/api/v1/share/:shareId/users", () => {
return HttpResponse.json({ success: true });
}),
],
},
},
};

export default meta;
type Story = StoryObj<typeof ShareDialog>;

// Default state with owner and 1 viewer
export const Default: Story = {
args: { ...sharedArgs },
};

// With add row visible
export const WithAddRow: Story = {
args: { ...sharedArgs, defaultShowAddRow: true },
};

// With public link visible
export const WithPublicLink: Story = {
args: { ...sharedArgs, defaultShowPublicLink: true },
};

// With both add row and public link
export const FullyExpanded: Story = {
args: {
...sharedArgs,
sessionTitle: "My Chat Session with a Very Long Title That Might Need Truncation",
defaultShowAddRow: true,
defaultShowPublicLink: true,
},
};

// With outdated session (shows update snapshot button for the viewer)
export const WithOutdatedSession: Story = {
args: {
...sharedArgs,
sessionTitle: "Recently Updated Chat",
// Session updated 1 day ago (after viewer was added 2 days ago)
sessionUpdatedTime: new Date(Date.now() - 86400000).toISOString(),
},
parameters: {
msw: {
handlers: [
http.get("/api/v1/share/link/:sessionId", () => {
return HttpResponse.json(mockShareLink);
}),
http.get("/api/v1/share/:shareId/users", () => {
return HttpResponse.json({
...mockOwner,
users: [mockViewerOld], // Use old viewer to trigger outdated snapshot
});
}),
],
},
},
};

// With no viewers (just owner)
export const OnlyOwner: Story = {
args: { ...sharedArgs, sessionTitle: "Chat Not Shared Yet" },
parameters: {
msw: {
handlers: [
http.get("/api/v1/share/link/:sessionId", () => {
return HttpResponse.json(mockShareLink);
}),
http.get("/api/v1/share/:shareId/users", () => {
return HttpResponse.json({
...mockOwner,
users: [], // No viewers
});
}),
],
},
},
};

// Closed state
export const Closed: Story = {
args: { ...sharedArgs, open: false },
};
Loading