Inline ListChangedIndicator with titles, normalize screen gaps, and rename ResultPanel to ToolResultPanel
- Place ListChangedIndicator on same line as section titles using Group with space-between in ToolControls, ResourceControls, and PromptControls - Add ListChanged stories for ResourceControls and show indicators in ConnectedView stories - Normalize ScreenLayout gap from "xl" to "md" across all screens for consistent spacing - Rename ResultPanel to ToolResultPanel (component, interface, files, and stories) - Replace Grid layout in ToolsScreen with equal-width flex children for detail and result panels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ed6d79c5ed
commit
5594e940c9
16 changed files with 69 additions and 56 deletions
|
|
@ -32,7 +32,7 @@ export function ListChangedIndicator({
|
|||
<Group gap="xs">
|
||||
<Dot />
|
||||
<UpdateLabel>List updated</UpdateLabel>
|
||||
<RefreshButton onClick={onRefresh}>↻ Refresh</RefreshButton>
|
||||
<RefreshButton onClick={onRefresh}>Refresh</RefreshButton>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ function formatPlaceholder(name: string): string {
|
|||
}
|
||||
|
||||
export function PromptArgumentsForm({
|
||||
name,
|
||||
description,
|
||||
arguments: promptArguments,
|
||||
argumentValues,
|
||||
|
|
@ -34,7 +33,7 @@ export function PromptArgumentsForm({
|
|||
}: PromptArgumentsFormProps) {
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Title order={4}>{name}</Title>
|
||||
<Title order={4}>Prompt Arguments</Title>
|
||||
{description && <DescriptionText>{description}</DescriptionText>}
|
||||
{promptArguments.length > 0 && (
|
||||
<Stack gap="sm">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { Group, 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";
|
||||
|
|
@ -30,8 +30,10 @@ export function PromptControls({
|
|||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>Prompts</Title>
|
||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>Prompts</Title>
|
||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder="Search prompts..."
|
||||
value={searchText}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,13 @@ export const WithSearch: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const ListChanged: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
listChanged: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
resources: [],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { Accordion, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
||||
import {
|
||||
Accordion,
|
||||
Group,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
TextInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||
import { ResourceListItem } from "../ResourceListItem/ResourceListItem";
|
||||
import { ResourceSubscribedItem } from "../ResourceSubscribedItem/ResourceSubscribedItem";
|
||||
|
|
@ -68,8 +75,10 @@ export function ResourceControls({
|
|||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>Resources</Title>
|
||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>Resources</Title>
|
||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
value={searchText}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
|
||||
import { ToolListItem } from "../ToolListItem/ToolListItem";
|
||||
import type { ToolListItemProps } from "../ToolListItem/ToolListItem";
|
||||
|
|
@ -32,8 +32,10 @@ export function ToolControls({
|
|||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Title order={4}>Tools</Title>
|
||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||
<Group justify="space-between">
|
||||
<Title order={4}>Tools</Title>
|
||||
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
|
||||
</Group>
|
||||
<TextInput
|
||||
placeholder="Search tools..."
|
||||
value={searchText}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { ToolResultPanel } from "./ToolResultPanel";
|
||||
|
||||
const meta: Meta<typeof ToolResultPanel> = {
|
||||
title: "Groups/ToolResultPanel",
|
||||
component: ToolResultPanel,
|
||||
args: {
|
||||
onClear: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ToolResultPanel>;
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
content: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const TextResult: Story = {
|
||||
args: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The current weather in San Francisco is 65°F and sunny.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const JsonResult: Story = {
|
||||
args: {
|
||||
content: [
|
||||
{
|
||||
type: "json",
|
||||
text: '{"temperature":65,"unit":"fahrenheit","condition":"sunny","city":"San Francisco"}',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ImageResult: Story = {
|
||||
args: {
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const MixedContent: Story = {
|
||||
args: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Here is the generated image based on your description:",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { Button, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
||||
|
||||
export interface ResultContentItem {
|
||||
type: string;
|
||||
text?: string;
|
||||
data?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface ToolResultPanelProps {
|
||||
content: ResultContentItem[];
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
const ClearButton = Button.withProps({
|
||||
variant: "light",
|
||||
size: "sm",
|
||||
});
|
||||
|
||||
function resolveTextType(type: string): "text" | "json" {
|
||||
return type === "text" ? "text" : "json";
|
||||
}
|
||||
|
||||
function resolveMediaType(mimeType?: string): "image" | "audio" {
|
||||
return mimeType?.startsWith("image") ? "image" : "audio";
|
||||
}
|
||||
|
||||
export function ToolResultPanel({ content, onClear }: ToolResultPanelProps) {
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={4}>Results</Title>
|
||||
{content.length === 0 ? (
|
||||
<Text c="dimmed">No results yet</Text>
|
||||
) : (
|
||||
<>
|
||||
{content.map((item, index) => {
|
||||
if (item.text !== undefined) {
|
||||
return (
|
||||
<ContentViewer
|
||||
key={index}
|
||||
type={resolveTextType(item.type)}
|
||||
content={item.text}
|
||||
copyable
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (item.data !== undefined) {
|
||||
return (
|
||||
<ContentViewer
|
||||
key={index}
|
||||
type={resolveMediaType(item.mimeType)}
|
||||
content={item.data}
|
||||
mimeType={item.mimeType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<Group>
|
||||
<ClearButton onClick={onClear}>Clear</ClearButton>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export interface HistoryScreenProps {
|
|||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||
gap: "xl",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const ALL_LEVELS_VISIBLE: Record<string, boolean> = {
|
|||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||
gap: "xl",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export interface PromptsScreenProps {
|
|||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||
gap: "xl",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export interface ResourcesScreenProps {
|
|||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||
gap: "xl",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface TasksScreenProps {
|
|||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||
gap: "xl",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { fn } from "storybook/test";
|
|||
import { ToolsScreen } from "./ToolsScreen";
|
||||
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
|
||||
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||
import type { ResultPanelProps } from "../../groups/ResultPanel/ResultPanel";
|
||||
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
|
||||
|
||||
const meta: Meta<typeof ToolsScreen> = {
|
||||
title: "Screens/ToolsScreen",
|
||||
|
|
@ -67,7 +67,7 @@ const selectedToolData: ToolDetailPanelProps = {
|
|||
onCancel: fn(),
|
||||
};
|
||||
|
||||
const resultData: ResultPanelProps = {
|
||||
const resultData: ToolResultPanelProps = {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { Card, Flex, Grid, Stack, Text } from "@mantine/core";
|
||||
import { Card, Flex, Stack, Text } from "@mantine/core";
|
||||
import { ToolControls } from "../../groups/ToolControls/ToolControls";
|
||||
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||
import { ResultPanel } from "../../groups/ResultPanel/ResultPanel";
|
||||
import { ToolResultPanel } from "../../groups/ToolResultPanel/ToolResultPanel";
|
||||
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
|
||||
import type { ToolDetailPanelProps } from "../../groups/ToolDetailPanel/ToolDetailPanel";
|
||||
import type { ResultPanelProps } from "../../groups/ResultPanel/ResultPanel";
|
||||
import type { ToolResultPanelProps } from "../../groups/ToolResultPanel/ToolResultPanel";
|
||||
|
||||
export interface ToolsScreenProps {
|
||||
tools: ToolListItemProps[];
|
||||
selectedTool?: ToolDetailPanelProps;
|
||||
result?: ResultPanelProps;
|
||||
result?: ToolResultPanelProps;
|
||||
listChanged: boolean;
|
||||
searchText: string;
|
||||
onSearchChange: (text: string) => void;
|
||||
|
|
@ -20,8 +20,9 @@ export interface ToolsScreenProps {
|
|||
const ScreenLayout = Flex.withProps({
|
||||
variant: "screen",
|
||||
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
||||
gap: "xl",
|
||||
gap: "md",
|
||||
p: "xl",
|
||||
align: "flex-start",
|
||||
});
|
||||
|
||||
const Sidebar = Stack.withProps({
|
||||
|
|
@ -34,10 +35,9 @@ const SidebarCard = Card.withProps({
|
|||
padding: "lg",
|
||||
});
|
||||
|
||||
const FullHeightCard = Card.withProps({
|
||||
const ContentCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
|
|
@ -71,27 +71,21 @@ export function ToolsScreen({
|
|||
</SidebarCard>
|
||||
</Sidebar>
|
||||
|
||||
<Grid align="stretch" flex={1}>
|
||||
<Grid.Col span={7}>
|
||||
<FullHeightCard>
|
||||
{selectedTool ? (
|
||||
<ToolDetailPanel {...selectedTool} />
|
||||
) : (
|
||||
<EmptyState>Select a tool to view details</EmptyState>
|
||||
)}
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
<ContentCard flex={1}>
|
||||
{selectedTool ? (
|
||||
<ToolDetailPanel {...selectedTool} />
|
||||
) : (
|
||||
<EmptyState>Select a tool to view details</EmptyState>
|
||||
)}
|
||||
</ContentCard>
|
||||
|
||||
<Grid.Col span={5}>
|
||||
<FullHeightCard>
|
||||
{result ? (
|
||||
<ResultPanel {...result} />
|
||||
) : (
|
||||
<EmptyState>Results will appear here</EmptyState>
|
||||
)}
|
||||
</FullHeightCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<ContentCard flex={1}>
|
||||
{result ? (
|
||||
<ToolResultPanel {...result} />
|
||||
) : (
|
||||
<EmptyState>Results will appear here</EmptyState>
|
||||
)}
|
||||
</ContentCard>
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ export const ToolsActive: Story = {
|
|||
onClear: fn(),
|
||||
}}
|
||||
searchText=""
|
||||
listChanged={false}
|
||||
listChanged={true}
|
||||
onSearchChange={fn()}
|
||||
onRefreshList={fn()}
|
||||
onSelectTool={fn()}
|
||||
|
|
@ -446,7 +446,7 @@ export const ResourcesActive: Story = {
|
|||
lastUpdated: "2026-03-17T11:15:00Z",
|
||||
isSubscribed: false,
|
||||
}}
|
||||
listChanged={false}
|
||||
listChanged={true}
|
||||
searchText=""
|
||||
onSearchChange={fn()}
|
||||
onRefreshList={fn()}
|
||||
|
|
@ -513,7 +513,7 @@ export const PromptsActive: Story = {
|
|||
{ role: "assistant", content: "Hola, como estas?" },
|
||||
],
|
||||
}}
|
||||
listChanged={false}
|
||||
listChanged={true}
|
||||
searchText=""
|
||||
onSearchChange={fn()}
|
||||
onRefreshList={fn()}
|
||||
|
|
|
|||
Loading…
Reference in a new issue