Skip to content

Commit 38a2d79

Browse files
committed
feat(ui): submit canvas edit request
Signed-off-by: Petr Kadlec <petr@puradesign.cz>
1 parent eb65829 commit 38a2d79

File tree

26 files changed

+349
-158
lines changed

26 files changed

+349
-158
lines changed

apps/agentstack-sdk-py/examples/artifacts_test_agent.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,52 @@ async def artifacts_agent(
4444

4545
await context.store(input)
4646

47-
# canvas_edit_request = await canvas.parse_canvas_edit_request(message=input)
47+
canvas_edit_request = await canvas.parse_canvas_edit_request(message=input)
4848

49-
# history = [message async for message in context.load_history() if isinstance(message, Message) and message.parts]
49+
print(f"Canvas Edit Request: {canvas_edit_request}")
5050

51-
recipe_title = random.choice(RECIPE_TITLES)
51+
if canvas_edit_request:
52+
recipe_title = "Canvas Recipe EDITED"
5253

53-
response = f"""\
54-
Here's your recipe:
54+
original_recipe = (
55+
canvas_edit_request.artifact.parts[0].root.text
56+
if isinstance(canvas_edit_request.artifact.parts[0].root, TextPart)
57+
else ""
58+
)
59+
edited_part = original_recipe[canvas_edit_request.start_index : canvas_edit_request.end_index]
60+
description = f"You requested to edit this part:\n\n*{edited_part}*\n\n"
61+
62+
response = f"""\
63+
{description}
5564
5665
```recipe
57-
# {recipe_title}
66+
# Canvas Recipe EDITED
5867
5968
## Ingredients
6069
- bread (1 slice)
6170
- butter (1 slice)
6271
63-
![test image](agentstack://artifact-test-image)
72+
## Instructions
73+
1. Cut a slice of bread.
74+
2. Cut a slice of butter.
75+
3. Spread the slice of butter on the slice of bread.
76+
```
77+
78+
Enjoy your edited meal!
79+
"""
80+
else:
81+
recipe_title = random.choice(RECIPE_TITLES)
82+
description = "Here's your recipe:"
83+
84+
response = f"""\
85+
{description}
86+
87+
```recipe
88+
# {recipe_title}
89+
90+
## Ingredients
91+
- bread (1 slice)
92+
- butter (1 slice)
6493
6594
## Instructions
6695
1. Cut a slice of bread.

apps/agentstack-sdk-ts/src/client/a2a/extensions/handle-agent-card.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { oauthProviderExtension } from './services/oauth-provider';
1919
import { platformApiExtension } from './services/platform';
2020
import type { SecretDemands, SecretFulfillments } from './services/secrets';
2121
import { secretsExtension } from './services/secrets';
22+
import type { CanvasFulfillments } from './ui/canvas';
2223
import { canvasExtension } from './ui/canvas';
2324
import { oauthRequestExtension } from './ui/oauth';
2425
import type { SettingsDemands, SettingsFulfillments } from './ui/settings';
@@ -29,10 +30,11 @@ export interface Fulfillments {
2930
llm: (demand: LLMDemands) => Promise<LLMFulfillments>;
3031
embedding: (demand: EmbeddingDemands) => Promise<EmbeddingFulfillments>;
3132
mcp: (demand: MCPDemands) => Promise<MCPFulfillments>;
32-
oauth: (demand: OAuthDemands) => Promise<OAuthFulfillments>;
33+
oauth: (demand: OAuthDemands) => OAuthFulfillments;
3334
settings: (demand: SettingsDemands) => Promise<SettingsFulfillments>;
3435
secrets: (demand: SecretDemands) => Promise<SecretFulfillments>;
3536
form: (demand: FormDemands) => Promise<FormFulfillments>;
37+
canvasEditRequest: () => CanvasFulfillments | null;
3638
oauthRedirectUri: () => string | null;
3739
getContextToken: () => ContextToken;
3840
}
@@ -53,6 +55,7 @@ const fulfillOAuthDemand = fulfillServiceExtensionDemand(oauthProviderExtension)
5355
const fulfillSettingsDemand = fulfillServiceExtensionDemand(settingsExtension);
5456
const fulfillSecretDemand = fulfillServiceExtensionDemand(secretsExtension);
5557
const fulfillFormDemand = fulfillServiceExtensionDemand(formExtension);
58+
const fulfillCanvasDemand = fulfillServiceExtensionDemand(canvasExtension);
5659

5760
export const handleAgentCard = (agentCard: { capabilities: AgentCapabilities }) => {
5861
const extensions = agentCard.capabilities.extensions ?? [];
@@ -99,6 +102,13 @@ export const handleAgentCard = (agentCard: { capabilities: AgentCapabilities })
99102
fulfilledMetadata = fulfillFormDemand(fulfilledMetadata, await fulfillments.form(formDemands));
100103
}
101104

105+
if (canvasDemands !== undefined) {
106+
const canvasEditRequest = fulfillments.canvasEditRequest();
107+
if (canvasEditRequest) {
108+
fulfilledMetadata = fulfillCanvasDemand(fulfilledMetadata, canvasEditRequest);
109+
}
110+
}
111+
102112
const oauthRedirectUri = fulfillments.oauthRedirectUri();
103113
if (oauthRedirectUri) {
104114
fulfilledMetadata = {

apps/agentstack-sdk-ts/src/client/a2a/extensions/ui/canvas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const responseSchema = z.object({
2020
artifact_id: z.string(),
2121
});
2222

23-
type CanvasFulfillments = z.infer<typeof responseSchema>;
23+
export type CanvasFulfillments = z.infer<typeof responseSchema>;
2424

2525
export const canvasExtension: A2AServiceExtension<typeof URI, z.infer<typeof schema>, CanvasFulfillments> = {
2626
getDemandsSchema: () => schema,

apps/agentstack-sdk-ts/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './client/a2a/extensions/services/platform';
2121
export * from './client/a2a/extensions/services/secrets';
2222
export * from './client/a2a/extensions/types';
2323
export * from './client/a2a/extensions/ui/agent-detail';
24+
export * from './client/a2a/extensions/ui/canvas';
2425
export * from './client/a2a/extensions/ui/citation';
2526
export * from './client/a2a/extensions/ui/form-request';
2627
export * from './client/a2a/extensions/ui/oauth';

apps/agentstack-ui/src/api/a2a/client.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,6 @@ export const buildA2AClient = async <UIGenericPart = never>({
124124
.with({ kind: 'artifact-update' }, (event) => {
125125
taskId = event.taskId;
126126

127-
console.log({ demands });
128-
129127
const parts = handleArtifactUpdate(event, demands.canvasDemands !== undefined);
130128

131129
messageSubject.next({ type: RunResultType.Parts, parts, taskId });

apps/agentstack-ui/src/components/MarkdownContent/MarkdownContent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface MarkdownContentProps {
2525
children?: string;
2626
className?: string;
2727
remarkPlugins?: PluggableList;
28+
rehypePlugins?: PluggableList;
2829
components?: Components;
2930
}
3031

@@ -33,6 +34,7 @@ export function MarkdownContent({
3334
showMermaidDiagrams,
3435
className,
3536
remarkPlugins: remarkPluginsProps,
37+
rehypePlugins: rehypePluginsProps,
3638
components: componentsProps,
3739
children,
3840
}: MarkdownContentProps) {
@@ -49,11 +51,12 @@ export function MarkdownContent({
4951
);
5052

5153
const extendedRemarkPlugins = useMemo(() => [...remarkPlugins, ...(remarkPluginsProps ?? [])], [remarkPluginsProps]);
54+
const extendedRehypePlugins = useMemo(() => [...rehypePlugins, ...(rehypePluginsProps ?? [])], [rehypePluginsProps]);
5255

5356
return (
5457
<div ref={containerRef} className={clsx(classes.root, className)}>
5558
<Markdown
56-
rehypePlugins={rehypePlugins}
59+
rehypePlugins={extendedRehypePlugins}
5760
remarkPlugins={extendedRemarkPlugins}
5861
components={extendedComponents}
5962
urlTransform={urlTransform}

apps/agentstack-ui/src/components/MarkdownContent/rehype/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,5 @@ import rehypeKatex from 'rehype-katex';
77
import type { PluggableList } from 'unified';
88

99
import { rehypeInlineCode } from './rehypeInlineCode';
10-
import { rehypeSourcePosition } from './rehypeSourcePosition';
1110

12-
// TODO: remove rehypeSourcePosition from global plugins
13-
export const rehypePlugins = [rehypeKatex, rehypeInlineCode, rehypeSourcePosition] satisfies PluggableList;
11+
export const rehypePlugins = [rehypeKatex, rehypeInlineCode] satisfies PluggableList;

apps/agentstack-ui/src/modules/canvas/components/Canvas.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import { useMemo, useRef } from 'react';
77

88
import { CopyButton } from '#components/CopyButton/CopyButton.tsx';
9-
import type { MarkdownSelection } from '#components/MarkdownContent/index.ts';
109
import { UIMessagePartKind } from '#modules/messages/types.ts';
1110

1211
import { useCanvas } from '../contexts';
@@ -26,16 +25,6 @@ export function Canvas() {
2625
return null;
2726
}
2827

29-
const handleTextSelected = (selection: MarkdownSelection) => {
30-
console.log('Start index:', selection.start);
31-
console.log('End index:', selection.end);
32-
console.log('Selected text:', selection.text);
33-
console.log('Selected markdown:', content?.slice(selection.start, selection.end));
34-
35-
// Do something with the selection indices
36-
// e.g., send to backend, create annotation, etc.
37-
};
38-
3928
return (
4029
<div className={classes.root}>
4130
<div className={classes.container}>
@@ -48,7 +37,7 @@ export function Canvas() {
4837
</header>
4938

5039
<div className={classes.body} ref={contentRef}>
51-
<CanvasMarkdownContent className={classes.content} onTextSelected={handleTextSelected}>
40+
<CanvasMarkdownContent className={classes.content} artifactId={activeArtifact.artifactId}>
5241
{content}
5342
</CanvasMarkdownContent>
5443
</div>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
.form {
7+
block-size: rem(64px);
8+
display: flex;
9+
align-items: center;
10+
background-color: var(--cds-button-primary);
11+
padding: $spacing-04;
12+
border-radius: $border-radius;
13+
}
14+
15+
.content {
16+
border: 1px solid $border-subtle-inverse;
17+
border-radius: $border-radius;
18+
block-size: rem(40px);
19+
display: flex;
20+
align-items: center;
21+
padding-inline-end: $spacing-03;
22+
23+
&:focus-within {
24+
outline: 2px solid $focus;
25+
}
26+
27+
:global(.cds--text-input) {
28+
&,
29+
&:focus-within {
30+
border: none;
31+
background-color: transparent;
32+
inline-size: rem(250px);
33+
color: $text-inverse;
34+
outline: none;
35+
}
36+
}
37+
38+
button {
39+
&,
40+
&:not(:disabled) {
41+
color: $background;
42+
svg {
43+
fill: currentColor;
44+
}
45+
}
46+
&:hover {
47+
color: $text-primary;
48+
}
49+
&:disabled {
50+
color: $text-muted;
51+
}
52+
}
53+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Send } from '@carbon/icons-react';
7+
import { IconButton, TextInput } from '@carbon/react';
8+
import { useId } from 'react';
9+
import { useForm } from 'react-hook-form';
10+
11+
import classes from './CanvasEditForm.module.scss';
12+
13+
interface Props {
14+
onSubmit: (description: string) => void;
15+
}
16+
17+
export function CanvasEditForm({ onSubmit }: Props) {
18+
const id = useId();
19+
20+
const {
21+
handleSubmit,
22+
register,
23+
formState: { isValid },
24+
} = useForm<{ input: string }>();
25+
26+
return (
27+
<form className={classes.form} onSubmit={handleSubmit(({ input }) => onSubmit(input))}>
28+
<div className={classes.content}>
29+
<TextInput
30+
placeholder="How do you want to change it?"
31+
id={id}
32+
labelText=""
33+
autoFocus
34+
size="sm"
35+
className={classes.input}
36+
{...register('input', { required: true })}
37+
/>
38+
<IconButton label="Ask agent" kind="ghost" size="sm" type="submit" disabled={!isValid}>
39+
<Send />
40+
</IconButton>
41+
</div>
42+
</form>
43+
);
44+
}

0 commit comments

Comments
 (0)