Skip to content

Commit 9a9c201

Browse files
authored
feat(UI): Text prompt (open-edge-platform#822)
1 parent 494b24c commit 9a9c201

File tree

8 files changed

+229
-36
lines changed

8 files changed

+229
-36
lines changed

application/ui/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export {
5757
type SchemaVisualPromptSchema as VisualPromptType,
5858
type SchemaPromptsListSchema as VisualPromptListType,
5959
type SchemaVisualPromptListItemSchema as VisualPromptItemType,
60+
type SchemaTextPromptSchema as TextPromptType,
6061
type SchemaFrameMetadata as FrameAPIType,
6162
type SchemaFrameListResponse as FramesResponseType,
6263
type SchemaSinkCreateSchema as SinkCreateType,

application/ui/src/features/prompts/prompt-modes/prompt-mode.component.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,13 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import { usePromptMode } from '@/hooks';
7+
8+
import { TextPrompt } from '../text-prompt/text-prompt.component';
69
import { VisualPromptProvider } from '../visual-prompt/visual-prompt-provider.component';
710
import { VisualPrompt } from '../visual-prompt/visual-prompt.component';
811

912
export const PromptMode = () => {
10-
return (
11-
<VisualPromptProvider>
12-
<VisualPrompt />
13-
</VisualPromptProvider>
14-
);
15-
16-
/* TODO: Uncomment when we support text prompt
1713
const [mode] = usePromptMode();
1814

1915
if (mode === 'visual') {
@@ -24,5 +20,5 @@ export const PromptMode = () => {
2420
);
2521
}
2622

27-
return <TextPrompt />;*/
23+
return <TextPrompt />;
2824
};

application/ui/src/features/prompts/prompt.component.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { Flex, Heading, View } from '@geti/ui';
6+
import { Divider, Flex, Heading, View } from '@geti/ui';
77

88
import { ModelToolbar } from './models/model-toolbar/model-toolbar.component';
99
import { PromptMode } from './prompt-modes/prompt-mode.component';
10+
import { PromptModes } from './prompt-modes/prompt-modes.component';
1011

1112
export const Prompt = () => {
1213
return (
@@ -22,11 +23,9 @@ export const Prompt = () => {
2223
<Heading margin={0}>Prompt</Heading>
2324
<View padding={'size-300'} flex={1}>
2425
<Flex direction={'column'} gap={'size-300'} height={'100%'}>
25-
{/* TODO: Uncomment when we support text prompt
26-
<PromptModes />
26+
<PromptModes />
2727

28-
<Divider size={'S'} />
29-
*/}
28+
<Divider size={'S'} />
3029

3130
<Flex flex={1} direction={'column'} gap={'size-200'}>
3231
<ModelToolbar />
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { $api, TextPromptType } from '@/api';
7+
import { useProjectIdentifier } from '@/hooks';
8+
import { toast } from '@geti/ui';
9+
10+
export const useGetTextPrompts = (): TextPromptType[] => {
11+
const { projectId } = useProjectIdentifier();
12+
13+
const { data } = $api.useSuspenseQuery('get', '/api/v1/projects/{project_id}/prompts', {
14+
params: { path: { project_id: projectId } },
15+
});
16+
17+
return (data.prompts ?? []).filter((prompt): prompt is TextPromptType => prompt.type === 'TEXT');
18+
};
19+
20+
export const useCreateTextPrompt = () => {
21+
const { projectId } = useProjectIdentifier();
22+
23+
return $api.useMutation('post', '/api/v1/projects/{project_id}/prompts', {
24+
meta: {
25+
invalidates: [
26+
['get', '/api/v1/projects/{project_id}/prompts', { params: { path: { project_id: projectId } } }],
27+
],
28+
error: {
29+
notify: true,
30+
},
31+
},
32+
onSuccess: () => {
33+
toast({
34+
type: 'success',
35+
message: 'Prompt created successfully.',
36+
});
37+
},
38+
});
39+
};
40+
41+
export const useDeleteTextPrompt = () => {
42+
const { projectId } = useProjectIdentifier();
43+
44+
return $api.useMutation('delete', '/api/v1/projects/{project_id}/prompts/{prompt_id}', {
45+
meta: {
46+
invalidates: [
47+
['get', '/api/v1/projects/{project_id}/prompts', { params: { path: { project_id: projectId } } }],
48+
],
49+
error: {
50+
notify: true,
51+
},
52+
},
53+
onSuccess: () => {
54+
toast({
55+
type: 'success',
56+
message: 'Prompt deleted successfully.',
57+
});
58+
},
59+
});
60+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright (C) 2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { CSSProperties } from 'react';
7+
8+
import { TextPromptType } from '@/api';
9+
import { ActionButton, Text } from '@geti/ui';
10+
import { Close } from '@geti/ui/icons';
11+
import { getDistinctColorBasedOnHash } from '@geti/ui/utils';
12+
13+
import classes from './text-prompt-badge.module.scss';
14+
15+
type TextPromptBadgeProps = {
16+
prompt: TextPromptType;
17+
onDelete: (id: string) => void;
18+
};
19+
20+
export const TextPromptBadge = ({ prompt, onDelete }: TextPromptBadgeProps) => {
21+
const color = getDistinctColorBasedOnHash(prompt.id);
22+
23+
return (
24+
<div
25+
className={classes.badge}
26+
style={{ '--badgeBgColor': color } as CSSProperties}
27+
aria-label={`Text prompt: ${prompt.content}`}
28+
>
29+
<Text UNSAFE_className={classes.badgeText}>{prompt.content}</Text>
30+
<ActionButton
31+
isQuiet
32+
aria-label={`Delete prompt: ${prompt.content}`}
33+
onPress={() => onDelete(prompt.id)}
34+
UNSAFE_className={classes.deleteButton}
35+
>
36+
<Close />
37+
</ActionButton>
38+
</div>
39+
);
40+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.badge {
2+
--foregroundColor: lch(from var(--badgeBgColor) calc((50 - l) * infinity) 0 0);
3+
--spectrum-high-contrast-button-text: var(--foregroundColor);
4+
5+
background-color: var(--badgeBgColor);
6+
border-radius: var(--spectrum-alias-border-radius-regular);
7+
padding: var(--spectrum-global-dimension-size-50) var(--spectrum-global-dimension-size-100);
8+
display: flex;
9+
align-items: center;
10+
gap: var(--spectrum-global-dimension-size-50);
11+
width: fit-content;
12+
min-height: var(--spectrum-global-dimension-size-400);
13+
transition: padding 0.2s;
14+
15+
& .deleteButton {
16+
opacity: 0;
17+
transition:
18+
opacity 0.2s,
19+
width 0.2s;
20+
overflow: hidden;
21+
min-width: unset;
22+
width: 0;
23+
height: var(--spectrum-global-dimension-size-300);
24+
color: var(--foregroundColor);
25+
border-radius: var(--spectrum-alias-border-radius-regular);
26+
}
27+
}
28+
29+
.badge:hover,
30+
.badge:focus-within {
31+
& .deleteButton {
32+
opacity: 1;
33+
background-color: color-mix(in srgb, var(--foregroundColor), transparent 80%) !important;
34+
width: var(--spectrum-global-dimension-size-300);
35+
}
36+
}
37+
38+
.badgeText {
39+
color: var(--foregroundColor);
40+
white-space: pre-wrap;
41+
word-break: break-word;
42+
}

application/ui/src/features/prompts/text-prompt/text-prompt.component.tsx

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,89 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { useState } from 'react';
6+
import { KeyboardEvent, Suspense, useState } from 'react';
77

8-
import { Button, Flex, TextArea, View } from '@geti/ui';
8+
import { useProjectIdentifier } from '@/hooks';
9+
import { ActionButton, Flex, Loading, Text, TextArea } from '@geti/ui';
10+
import { Add } from '@geti/ui/icons';
11+
12+
import { useCreateTextPrompt, useDeleteTextPrompt, useGetTextPrompts } from './api/use-text-prompts';
13+
import { TextPromptBadge } from './text-prompt-badge/text-prompt-badge.component';
14+
15+
import classes from './text-prompt.module.scss';
16+
17+
const TextPromptBadgeList = () => {
18+
const prompts = useGetTextPrompts();
19+
const { projectId } = useProjectIdentifier();
20+
const deleteMutation = useDeleteTextPrompt();
21+
22+
const handleDelete = (promptId: string) => {
23+
deleteMutation.mutate({
24+
params: { path: { project_id: projectId, prompt_id: promptId } },
25+
});
26+
};
27+
28+
if (prompts.length === 0) {
29+
return <Text UNSAFE_className={classes.emptyState}>No text prompts yet.</Text>;
30+
}
31+
32+
return (
33+
<Flex direction={'column'} gap={'size-100'}>
34+
{prompts.map((prompt) => (
35+
<TextPromptBadge key={prompt.id} prompt={prompt} onDelete={handleDelete} />
36+
))}
37+
</Flex>
38+
);
39+
};
940

1041
export const TextPrompt = () => {
11-
const [prompt, setPrompt] = useState<string>('Enter your text prompt here');
42+
const [content, setContent] = useState('');
43+
const { projectId } = useProjectIdentifier();
44+
const createMutation = useCreateTextPrompt();
45+
46+
const isSubmitDisabled = content.trim() === '' || createMutation.isPending;
47+
48+
const handleKeyDown = (e: KeyboardEvent) => {
49+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey || !e.shiftKey)) {
50+
e.preventDefault();
51+
if (!isSubmitDisabled) handleAddTextPrompt();
52+
}
53+
};
54+
55+
const handleAddTextPrompt = () => {
56+
const trimmed = content.trim();
57+
58+
if (!trimmed) return;
1259

13-
const isSubmitDisabled = prompt.trim() === '';
60+
createMutation.mutate(
61+
{
62+
body: { type: 'TEXT', content: trimmed },
63+
params: { path: { project_id: projectId } },
64+
},
65+
{
66+
onSuccess: () => setContent(''),
67+
}
68+
);
69+
};
1470

1571
return (
16-
<View height={'100%'}>
17-
<View
18-
backgroundColor={'gray-50'}
19-
padding={'size-100'}
20-
borderRadius={'regular'}
21-
height={'100%'}
22-
maxHeight={'size-3000'}
23-
>
72+
<Flex direction={'column'} gap={'size-200'}>
73+
<Flex gap={'size-100'} alignItems={'end'}>
2474
<TextArea
25-
aria-label={'Text prompt'}
26-
value={prompt}
27-
onChange={setPrompt}
28-
width={'100%'}
29-
height={'100%'}
75+
aria-label={'New text prompt'}
76+
placeholder={'e.g. red car'}
77+
value={content}
78+
onChange={setContent}
79+
onKeyDown={handleKeyDown}
80+
flex={1}
3081
/>
31-
</View>
32-
<Flex justifyContent={'end'}>
33-
<Button marginTop={'size-200'} isDisabled={isSubmitDisabled}>
34-
Submit
35-
</Button>
82+
<ActionButton isDisabled={isSubmitDisabled} onPress={handleAddTextPrompt} aria-label={'Add prompt'}>
83+
<Add />
84+
</ActionButton>
3685
</Flex>
37-
</View>
86+
<Suspense fallback={<Loading mode={'inline'} size={'M'} />}>
87+
<TextPromptBadgeList />
88+
</Suspense>
89+
</Flex>
3890
);
3991
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.emptyState {
2+
color: var(--spectrum-global-color-gray-600);
3+
}

0 commit comments

Comments
 (0)