diff --git a/clients/web/src/components/groups/LogControls/LogControls.stories.tsx b/clients/web/src/components/groups/LogControls/LogControls.stories.tsx index c9b281dc..2b21e5d1 100644 --- a/clients/web/src/components/groups/LogControls/LogControls.stories.tsx +++ b/clients/web/src/components/groups/LogControls/LogControls.stories.tsx @@ -20,8 +20,7 @@ const meta: Meta = { onSetLevel: fn(), onFilterChange: fn(), onToggleLevel: fn(), - onClear: fn(), - onExport: fn(), + onToggleAllLevels: fn(), }, }; diff --git a/clients/web/src/components/groups/LogControls/LogControls.tsx b/clients/web/src/components/groups/LogControls/LogControls.tsx index ab218334..af45d3ab 100644 --- a/clients/web/src/components/groups/LogControls/LogControls.tsx +++ b/clients/web/src/components/groups/LogControls/LogControls.tsx @@ -1,12 +1,12 @@ import { Button, - Checkbox, Group, Select, Stack, Text, TextInput, Title, + UnstyledButton, } from "@mantine/core"; const LOG_LEVELS = [ @@ -20,22 +20,17 @@ const LOG_LEVELS = [ "emergency", ] as const; -const LEVEL_COLORS: Record = { +const LEVEL_COLORS: Record = { debug: { c: "dimmed" }, info: { c: "blue" }, notice: { c: "teal" }, warning: { c: "yellow" }, error: { c: "red" }, - critical: { c: "red", fw: 700 }, - alert: { c: "red", fw: 700 }, - emergency: { c: "red", fw: 900 }, + critical: { c: "red" }, + alert: { c: "red" }, + emergency: { c: "red" }, }; -const ToolbarButton = Button.withProps({ - variant: "light", - size: "sm", -}); - const SubtleButton = Button.withProps({ variant: "subtle", size: "xs", @@ -49,8 +44,6 @@ export interface LogControlsProps { onFilterChange: (text: string) => void; onToggleLevel: (level: string, visible: boolean) => void; onToggleAllLevels: () => void; - onClear: () => void; - onExport: () => void; } export function LogControls({ @@ -61,8 +54,6 @@ export function LogControls({ onFilterChange, onToggleLevel, onToggleAllLevels, - onClear, - onExport, }: LogControlsProps) { return ( @@ -71,6 +62,7 @@ export function LogControls({ Active Level ({ value: p.name, label: p.name }))} - value={selectedPrompt ?? null} - onChange={(value) => { - if (value) { - onSelectPrompt(value); - } - }} - placeholder="Choose a prompt..." - /> - {selectedPrompt && selectedPromptInfo?.description && ( - {selectedPromptInfo.description} - )} + {name} + {description && {description}} {promptArguments.length > 0 && ( - <> + {promptArguments.map((arg) => ( ))} - + )} - + + + ); } diff --git a/clients/web/src/components/groups/PromptControls/PromptControls.stories.tsx b/clients/web/src/components/groups/PromptControls/PromptControls.stories.tsx new file mode 100644 index 00000000..53b85908 --- /dev/null +++ b/clients/web/src/components/groups/PromptControls/PromptControls.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { PromptControls } from "./PromptControls"; + +const meta: Meta = { + title: "Groups/PromptControls", + component: PromptControls, + args: { + searchText: "", + onSearchChange: fn(), + onRefreshList: fn(), + onSelectPrompt: fn(), + listChanged: false, + }, +}; + +export default meta; +type Story = StoryObj; + +const samplePrompts = [ + { + name: "summarize", + description: "Summarize the given text into key points", + selected: false, + }, + { + name: "translate", + description: "Translate text from one language to another", + selected: false, + }, + { + name: "analyze", + description: "Analyze sentiment and tone of the text", + selected: false, + }, + { + name: "code-review", + description: "Review code for issues", + selected: false, + }, + { name: "refactor", selected: false }, +]; + +export const Default: Story = { + args: { + prompts: samplePrompts, + }, +}; + +export const WithSelection: Story = { + args: { + prompts: samplePrompts.map((p) => + p.name === "translate" ? { ...p, selected: true } : p, + ), + }, +}; + +export const WithSearch: Story = { + args: { + prompts: samplePrompts, + searchText: "sum", + }, +}; + +export const ListChanged: Story = { + args: { + prompts: samplePrompts, + listChanged: true, + }, +}; + +export const Empty: Story = { + args: { + prompts: [], + }, +}; diff --git a/clients/web/src/components/groups/PromptControls/PromptControls.tsx b/clients/web/src/components/groups/PromptControls/PromptControls.tsx new file mode 100644 index 00000000..7d3dd8c6 --- /dev/null +++ b/clients/web/src/components/groups/PromptControls/PromptControls.tsx @@ -0,0 +1,55 @@ +import { ScrollArea, Stack, TextInput, Title } from "@mantine/core"; +import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator"; +import { PromptListItem } from "../PromptListItem/PromptListItem"; +import type { PromptItem } from "../../screens/PromptsScreen/PromptsScreen"; + +export interface PromptControlsProps { + prompts: PromptItem[]; + listChanged: boolean; + searchText: string; + onSearchChange: (text: string) => void; + onRefreshList: () => void; + onSelectPrompt: (name: string) => void; +} + +function listMaxHeight(): string { + return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)"; +} + +export function PromptControls({ + prompts, + listChanged, + searchText, + onSearchChange, + onRefreshList, + onSelectPrompt, +}: PromptControlsProps) { + const filteredPrompts = prompts.filter((p) => + p.name.toLowerCase().includes(searchText.toLowerCase()), + ); + + return ( + + Prompts + + onSearchChange(e.currentTarget.value)} + /> + + + {filteredPrompts.map((prompt) => ( + onSelectPrompt(prompt.name)} + /> + ))} + + + + ); +} diff --git a/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx b/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx new file mode 100644 index 00000000..baadb52d --- /dev/null +++ b/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { PromptListItem } from "./PromptListItem"; + +const meta: Meta = { + title: "Groups/PromptListItem", + component: PromptListItem, + args: { + onClick: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + name: "summarize", + description: "Summarize the given text into key points", + selected: false, + }, +}; + +export const Selected: Story = { + args: { + name: "translate", + description: "Translate text from one language to another", + selected: true, + }, +}; + +export const NoDescription: Story = { + args: { + name: "code-review", + selected: false, + }, +}; diff --git a/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx b/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx new file mode 100644 index 00000000..c4debda5 --- /dev/null +++ b/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx @@ -0,0 +1,40 @@ +import { Stack, Text, UnstyledButton } from "@mantine/core"; + +export interface PromptListItemProps { + name: string; + description?: string; + selected: boolean; + onClick: () => void; +} + +const NameText = Text.withProps({ + fw: 500, +}); + +const DescriptionText = Text.withProps({ + size: "xs", + c: "dimmed", + lineClamp: 1, +}); + +export function PromptListItem({ + name, + description, + selected, + onClick, +}: PromptListItemProps) { + return ( + + + {name} + {description && {description}} + + + ); +} diff --git a/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx b/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx new file mode 100644 index 00000000..33be2c4d --- /dev/null +++ b/clients/web/src/components/groups/ToolControls/ToolControls.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { ToolControls } from "./ToolControls"; + +const meta: Meta = { + title: "Groups/ToolControls", + component: ToolControls, + args: { + searchText: "", + onSearchChange: fn(), + onRefreshList: fn(), + onSelectTool: fn(), + listChanged: false, + }, +}; + +export default meta; +type Story = StoryObj; + +const sampleTools = [ + { + name: "send_message", + title: "Send Message", + selected: false, + onClick: fn(), + }, + { + name: "create_record", + title: "Create Record", + selected: false, + onClick: fn(), + }, + { name: "delete_records", selected: false, onClick: fn() }, + { name: "list_users", selected: false, onClick: fn() }, + { name: "batch_process", selected: false, onClick: fn() }, +]; + +export const Default: Story = { + args: { + tools: sampleTools, + }, +}; + +export const WithSelection: Story = { + args: { + tools: sampleTools.map((t) => + t.name === "create_record" ? { ...t, selected: true } : t, + ), + }, +}; + +export const WithSearch: Story = { + args: { + tools: sampleTools, + searchText: "send", + }, +}; + +export const ListChanged: Story = { + args: { + tools: sampleTools, + listChanged: true, + }, +}; + +export const Empty: Story = { + args: { + tools: [], + }, +}; diff --git a/clients/web/src/components/groups/ToolControls/ToolControls.tsx b/clients/web/src/components/groups/ToolControls/ToolControls.tsx new file mode 100644 index 00000000..23eb7300 --- /dev/null +++ b/clients/web/src/components/groups/ToolControls/ToolControls.tsx @@ -0,0 +1,55 @@ +import { ScrollArea, Stack, TextInput, Title } from "@mantine/core"; +import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator"; +import { ToolListItem } from "../ToolListItem/ToolListItem"; +import type { ToolListItemProps } from "../ToolListItem/ToolListItem"; + +export interface ToolControlsProps { + tools: ToolListItemProps[]; + listChanged: boolean; + searchText: string; + onSearchChange: (text: string) => void; + onRefreshList: () => void; + onSelectTool: (name: string) => void; +} + +function listMaxHeight(): string { + return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)"; +} + +export function ToolControls({ + tools, + listChanged, + searchText, + onSearchChange, + onRefreshList, + onSelectTool, +}: ToolControlsProps) { + const filteredTools = searchText + ? tools.filter((tool) => + tool.name.toLowerCase().includes(searchText.toLowerCase()), + ) + : tools; + + return ( + + Tools + + onSearchChange(e.currentTarget.value)} + /> + + + {filteredTools.map((tool) => ( + onSelectTool(tool.name)} + /> + ))} + + + + ); +} diff --git a/clients/web/src/components/screens/HistoryScreen/HistoryScreen.tsx b/clients/web/src/components/screens/HistoryScreen/HistoryScreen.tsx index ca7436fd..1326c0cf 100644 --- a/clients/web/src/components/screens/HistoryScreen/HistoryScreen.tsx +++ b/clients/web/src/components/screens/HistoryScreen/HistoryScreen.tsx @@ -19,7 +19,7 @@ const ScreenLayout = Flex.withProps({ }); const Sidebar = Stack.withProps({ - w: 280, + w: 340, flex: "0 0 auto", }); diff --git a/clients/web/src/components/screens/LoggingScreen/LoggingScreen.tsx b/clients/web/src/components/screens/LoggingScreen/LoggingScreen.tsx index 74e35f97..ac856bca 100644 --- a/clients/web/src/components/screens/LoggingScreen/LoggingScreen.tsx +++ b/clients/web/src/components/screens/LoggingScreen/LoggingScreen.tsx @@ -34,7 +34,7 @@ const ScreenLayout = Flex.withProps({ }); const Sidebar = Stack.withProps({ - w: 280, + w: 340, flex: "0 0 auto", }); @@ -82,8 +82,6 @@ export function LoggingScreen({ onFilterChange={setFilterText} onToggleLevel={handleToggleLevel} onToggleAllLevels={handleToggleAllLevels} - onClear={onClear} - onExport={onExport} /> @@ -94,6 +92,8 @@ export function LoggingScreen({ autoScroll={autoScroll} onToggleAutoScroll={onToggleAutoScroll} onCopyAll={onCopyAll} + onClear={onClear} + onExport={onExport} /> ); diff --git a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx index 9124660e..ece2ee01 100644 --- a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx +++ b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx @@ -1,69 +1,90 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { fn } from "storybook/test"; import { PromptsScreen } from "./PromptsScreen"; +import type { PromptItem, SelectedPrompt } from "./PromptsScreen"; const meta: Meta = { title: "Screens/PromptsScreen", component: PromptsScreen, parameters: { layout: "fullscreen" }, args: { + searchText: "", + onSearchChange: fn(), onRefreshList: fn(), + onSelectPrompt: fn(), + onArgumentChange: fn(), + onGetPrompt: fn(), + listChanged: false, }, }; export default meta; type Story = StoryObj; +const samplePrompts: PromptItem[] = [ + { + name: "summarize", + description: "Summarize the given text into key points", + selected: false, + }, + { + name: "translate", + description: "Translate text from one language to another", + selected: false, + }, + { + name: "code-review", + description: "Review code for issues", + selected: false, + }, + { + name: "analyze", + description: "Analyze sentiment and tone of the text", + selected: false, + }, + { name: "refactor", selected: false }, +]; + +const selectedTranslate: SelectedPrompt = { + name: "translate", + description: "Translate text from one language to another", + arguments: [ + { name: "text", required: true, description: "The text to translate" }, + { + name: "targetLanguage", + required: true, + description: "Target language code", + }, + ], + argumentValues: {}, +}; + export const NoSelection: Story = { args: { - promptForm: { - prompts: [ - { name: "summarize", description: "Summarize a document" }, - { - name: "translate", - description: "Translate text to another language", - }, - { name: "code-review", description: "Review code for issues" }, - ], - selectedPrompt: undefined, - arguments: [], - argumentValues: {}, - onSelectPrompt: fn(), - onArgumentChange: fn(), - onGetPrompt: fn(), - }, - messages: undefined, - listChanged: false, + prompts: samplePrompts, + }, +}; + +export const PromptSelected: Story = { + args: { + prompts: samplePrompts.map((p) => + p.name === "translate" ? { ...p, selected: true } : p, + ), + selectedPrompt: selectedTranslate, }, }; export const WithResult: Story = { args: { - promptForm: { - prompts: [ - { name: "summarize", description: "Summarize a document" }, - { - name: "translate", - description: "Translate text to another language", - }, - { name: "code-review", description: "Review code for issues" }, - ], - selectedPrompt: "translate", - arguments: [ - { name: "text", required: true, description: "The text to translate" }, - { - name: "targetLanguage", - required: true, - description: "Target language code", - }, - ], + prompts: samplePrompts.map((p) => + p.name === "translate" ? { ...p, selected: true } : p, + ), + selectedPrompt: { + ...selectedTranslate, argumentValues: { text: "Hello, how are you?", targetLanguage: "es", }, - onSelectPrompt: fn(), - onArgumentChange: fn(), - onGetPrompt: fn(), }, messages: { onCopyAll: fn(), @@ -79,6 +100,18 @@ export const WithResult: Story = { }, ], }, - listChanged: false, + }, +}; + +export const ListChanged: Story = { + args: { + prompts: samplePrompts, + listChanged: true, + }, +}; + +export const Empty: Story = { + args: { + prompts: [], }, }; diff --git a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx index 3fe718f8..1321710c 100644 --- a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx +++ b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx @@ -1,26 +1,56 @@ -import { Card, Container, Grid, Stack, Text } from "@mantine/core"; +import { Card, Flex, ScrollArea, Stack, Text } from "@mantine/core"; +import { PromptControls } from "../../groups/PromptControls/PromptControls"; import { PromptArgumentsForm } from "../../groups/PromptArgumentsForm/PromptArgumentsForm"; import { PromptMessagesDisplay } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay"; -import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator"; -import type { PromptArgumentsFormProps } from "../../groups/PromptArgumentsForm/PromptArgumentsForm"; +import type { PromptArgument } from "../../groups/PromptArgumentsForm/PromptArgumentsForm"; import type { PromptMessagesDisplayProps } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay"; -export interface PromptsScreenProps { - promptForm: PromptArgumentsFormProps; - messages?: PromptMessagesDisplayProps; - listChanged: boolean; - onRefreshList: () => void; +export interface PromptItem { + name: string; + description?: string; + selected: boolean; } -const PageContainer = Container.withProps({ - size: "xl", - py: "xl", +export interface SelectedPrompt { + name: string; + description?: string; + arguments: PromptArgument[]; + argumentValues: Record; +} + +export interface PromptsScreenProps { + prompts: PromptItem[]; + selectedPrompt?: SelectedPrompt; + messages?: PromptMessagesDisplayProps; + listChanged: boolean; + searchText: string; + onSearchChange: (text: string) => void; + onRefreshList: () => void; + onSelectPrompt: (name: string) => void; + onArgumentChange: (name: string, value: string) => void; + onGetPrompt: () => void; +} + +const ScreenLayout = Flex.withProps({ + variant: "screen", + h: "calc(100vh - var(--app-shell-header-height, 0px))", + gap: "xl", + p: "xl", }); -const FullHeightCard = Card.withProps({ +const Sidebar = Stack.withProps({ + w: 340, + flex: "0 0 auto", +}); + +const SidebarCard = Card.withProps({ + withBorder: true, + padding: "lg", +}); + +const DetailCard = Card.withProps({ withBorder: true, padding: "lg", - h: "100%", }); const EmptyState = Text.withProps({ @@ -30,37 +60,62 @@ const EmptyState = Text.withProps({ }); export function PromptsScreen({ - promptForm, + prompts, + selectedPrompt, messages, listChanged, + searchText, + onSearchChange, onRefreshList, + onSelectPrompt, + onArgumentChange, + onGetPrompt, }: PromptsScreenProps) { return ( - - - - - - - - - - - - - {messages ? ( - - ) : ( - - Select a prompt and click Get Prompt to see messages - - )} - - - - + + + + + + + + + + {selectedPrompt ? ( + <> + + + + {messages && ( + + + + )} + + ) : ( + + Select a prompt to view details + + )} + + + ); } diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx index f8fd385d..4bf7f401 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx @@ -18,7 +18,7 @@ const ScreenLayout = Flex.withProps({ }); const Sidebar = Stack.withProps({ - w: 280, + w: 340, flex: "0 0 auto", }); diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx index b6f97322..3f182c37 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx @@ -1,14 +1,5 @@ -import { - Card, - Container, - Grid, - Stack, - Text, - TextInput, - Title, -} from "@mantine/core"; -import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator"; -import { ToolListItem } from "../../groups/ToolListItem/ToolListItem"; +import { Card, Flex, Grid, Stack, Text } from "@mantine/core"; +import { ToolControls } from "../../groups/ToolControls/ToolControls"; import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel"; import { ResultPanel } from "../../groups/ResultPanel/ResultPanel"; import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem"; @@ -26,9 +17,21 @@ export interface ToolsScreenProps { onSelectTool: (name: string) => void; } -const PageContainer = Container.withProps({ - size: "xl", - py: "xl", +const ScreenLayout = Flex.withProps({ + variant: "screen", + h: "calc(100vh - var(--app-shell-header-height, 0px))", + gap: "xl", + p: "xl", +}); + +const Sidebar = Stack.withProps({ + w: 340, + flex: "0 0 auto", +}); + +const SidebarCard = Card.withProps({ + withBorder: true, + padding: "lg", }); const FullHeightCard = Card.withProps({ @@ -53,40 +56,23 @@ export function ToolsScreen({ onRefreshList, onSelectTool, }: ToolsScreenProps) { - const filteredTools = searchText - ? tools.filter((tool) => - tool.name.toLowerCase().includes(searchText.toLowerCase()), - ) - : tools; - return ( - - - - - - Tools - - onSearchChange(event.currentTarget.value)} - /> - {filteredTools.map((tool) => ( - onSelectTool(tool.name)} - /> - ))} - - - + + + + + + - + + {selectedTool ? ( @@ -96,7 +82,7 @@ export function ToolsScreen({ - + {result ? ( @@ -106,6 +92,6 @@ export function ToolsScreen({ - + ); } diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index ed6f1e17..6b14eded 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -465,16 +465,26 @@ export const PromptsActive: Story = { activeTab: "Prompts", children: ( ), },