@flowdrop/flowdrop 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/enhanced-client.d.ts +29 -16
- package/dist/api/enhanced-client.js +0 -14
- package/dist/components/PipelineStatus.svelte +9 -12
- package/dist/components/WorkflowEditor.svelte +3 -0
- package/dist/components/interrupt/ChoicePrompt.svelte +24 -5
- package/dist/components/interrupt/ConfirmationPrompt.svelte +5 -0
- package/dist/components/interrupt/InterruptBubble.svelte +12 -0
- package/dist/components/interrupt/ReviewPrompt.svelte +20 -0
- package/dist/components/interrupt/TextInputPrompt.svelte +5 -0
- package/dist/components/nodes/GatewayNode.svelte +2 -6
- package/dist/components/nodes/WorkflowNode.svelte +2 -6
- package/dist/components/playground/ChatInput.svelte +359 -0
- package/dist/components/playground/ChatInput.svelte.d.ts +14 -0
- package/dist/components/playground/ChatPanel.svelte +100 -724
- package/dist/components/playground/ChatPanel.svelte.d.ts +9 -26
- package/dist/components/playground/ControlPanel.svelte +496 -0
- package/dist/components/playground/ControlPanel.svelte.d.ts +20 -0
- package/dist/components/playground/ExecutionConsole.svelte +163 -0
- package/dist/components/playground/ExecutionConsole.svelte.d.ts +14 -0
- package/dist/components/playground/MessageStream.svelte +283 -0
- package/dist/components/playground/MessageStream.svelte.d.ts +27 -0
- package/dist/components/playground/PipelineKanbanView.svelte +284 -0
- package/dist/components/playground/PipelineKanbanView.svelte.d.ts +11 -0
- package/dist/components/playground/PipelinePanel.svelte +204 -65
- package/dist/components/playground/PipelinePanel.svelte.d.ts +3 -1
- package/dist/components/playground/PipelineTableView.svelte +376 -0
- package/dist/components/playground/PipelineTableView.svelte.d.ts +11 -0
- package/dist/components/playground/Playground.svelte +262 -1200
- package/dist/components/playground/Playground.svelte.d.ts +0 -13
- package/dist/components/playground/PlaygroundStudio.svelte +35 -61
- package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -1
- package/dist/components/playground/pipelineViewUtils.svelte.d.ts +22 -0
- package/dist/components/playground/pipelineViewUtils.svelte.js +77 -0
- package/dist/messages/defaults.d.ts +24 -0
- package/dist/messages/defaults.js +24 -0
- package/dist/playground/index.d.ts +6 -1
- package/dist/playground/index.js +6 -0
- package/dist/playground/mount.d.ts +3 -0
- package/dist/playground/mount.js +3 -2
- package/dist/stores/playgroundStore.svelte.d.ts +6 -0
- package/dist/stores/playgroundStore.svelte.js +21 -1
- package/dist/types/index.d.ts +28 -2
- package/dist/types/playground.d.ts +5 -2
- package/dist/types/playground.js +5 -7
- package/dist/utils/nodeStatus.js +15 -5
- package/package.json +1 -1
|
@@ -34,6 +34,34 @@ export declare class ApiError extends Error {
|
|
|
34
34
|
* const client = new EnhancedFlowDropApiClient(config);
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
+
export interface PipelineDataResponse {
|
|
38
|
+
status: string;
|
|
39
|
+
jobs: Array<Record<string, unknown>>;
|
|
40
|
+
node_statuses: Record<string, {
|
|
41
|
+
status: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
job_status_summary: {
|
|
45
|
+
total: number;
|
|
46
|
+
pending: number;
|
|
47
|
+
running: number;
|
|
48
|
+
completed: number;
|
|
49
|
+
failed: number;
|
|
50
|
+
cancelled: number;
|
|
51
|
+
skipped?: number;
|
|
52
|
+
paused?: number;
|
|
53
|
+
interrupted?: number;
|
|
54
|
+
};
|
|
55
|
+
kanban_config?: {
|
|
56
|
+
columns: Array<{
|
|
57
|
+
key: string;
|
|
58
|
+
label: string;
|
|
59
|
+
statuses: string[];
|
|
60
|
+
icon?: string;
|
|
61
|
+
color?: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
37
65
|
export declare class EnhancedFlowDropApiClient {
|
|
38
66
|
private config;
|
|
39
67
|
private authProvider;
|
|
@@ -164,20 +192,5 @@ export declare class EnhancedFlowDropApiClient {
|
|
|
164
192
|
/**
|
|
165
193
|
* Fetch pipeline data including job information and status
|
|
166
194
|
*/
|
|
167
|
-
getPipelineData(pipelineId: string): Promise<
|
|
168
|
-
status: string;
|
|
169
|
-
jobs: Array<Record<string, unknown>>;
|
|
170
|
-
node_statuses: Record<string, {
|
|
171
|
-
status: string;
|
|
172
|
-
[key: string]: unknown;
|
|
173
|
-
}>;
|
|
174
|
-
job_status_summary: {
|
|
175
|
-
total: number;
|
|
176
|
-
pending: number;
|
|
177
|
-
running: number;
|
|
178
|
-
completed: number;
|
|
179
|
-
failed: number;
|
|
180
|
-
cancelled: number;
|
|
181
|
-
};
|
|
182
|
-
}>;
|
|
195
|
+
getPipelineData(pipelineId: string): Promise<PipelineDataResponse>;
|
|
183
196
|
}
|
|
@@ -28,20 +28,6 @@ export class ApiError extends Error {
|
|
|
28
28
|
this.errorData = errorData;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Enhanced HTTP API client for FlowDrop with configurable endpoints
|
|
33
|
-
*
|
|
34
|
-
* Supports pluggable authentication via AuthProvider interface.
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* // With AuthProvider
|
|
39
|
-
* const client = new EnhancedFlowDropApiClient(config, authProvider);
|
|
40
|
-
*
|
|
41
|
-
* // Without authentication
|
|
42
|
-
* const client = new EnhancedFlowDropApiClient(config);
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
31
|
export class EnhancedFlowDropApiClient {
|
|
46
32
|
config;
|
|
47
33
|
authProvider;
|
|
@@ -117,23 +117,20 @@
|
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
// Update node statuses based on job data
|
|
120
|
+
// Update node statuses based on job data — only set what the server reported
|
|
121
121
|
if (jobStatusData.node_statuses) {
|
|
122
122
|
const newNodeStatuses: Record<string, 'pending' | 'running' | 'completed' | 'error'> = {};
|
|
123
123
|
|
|
124
|
-
// Initialize all nodes as pending
|
|
125
|
-
if (workflow && workflow.nodes) {
|
|
126
|
-
workflow.nodes.forEach((node) => {
|
|
127
|
-
newNodeStatuses[node.id] = 'pending';
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Override with actual job statuses
|
|
132
124
|
for (const nodeId in jobStatusData.node_statuses) {
|
|
133
125
|
const status = jobStatusData.node_statuses[nodeId].status;
|
|
134
|
-
if (
|
|
135
|
-
newNodeStatuses[nodeId] =
|
|
136
|
-
|
|
126
|
+
if (status === 'failed' || status === 'cancelled') {
|
|
127
|
+
newNodeStatuses[nodeId] = 'error';
|
|
128
|
+
} else if (status === 'running' || status === 'paused' || status === 'interrupted') {
|
|
129
|
+
newNodeStatuses[nodeId] = 'running';
|
|
130
|
+
} else if (status === 'completed' || status === 'skipped') {
|
|
131
|
+
newNodeStatuses[nodeId] = 'completed';
|
|
132
|
+
} else if (status === 'pending' || status === 'idle') {
|
|
133
|
+
newNodeStatuses[nodeId] = 'pending';
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
136
|
nodeStatuses = newNodeStatuses;
|
|
@@ -855,6 +855,9 @@
|
|
|
855
855
|
{initialViewport}
|
|
856
856
|
colorMode={getResolvedTheme() as ColorMode}
|
|
857
857
|
fitView={getEditorSettings().fitViewOnLoad}
|
|
858
|
+
nodesDraggable={!props.lockWorkflow && !props.readOnly}
|
|
859
|
+
nodesConnectable={!props.lockWorkflow && !props.readOnly}
|
|
860
|
+
elementsSelectable={!props.lockWorkflow && !props.readOnly}
|
|
858
861
|
>
|
|
859
862
|
<Controls />
|
|
860
863
|
{#if !props.readOnly && !props.lockWorkflow && props.onToggleConsole}
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
// Hoist the choice branch — counter, min, max, submit reads.
|
|
46
46
|
const t = $derived(m().interrupt.choice);
|
|
47
47
|
|
|
48
|
+
// Unique name for the radio/checkbox group so multiple ChoicePrompts on
|
|
49
|
+
// screen don't share the same HTML group and interfere with each other.
|
|
50
|
+
const groupName = `choice-option-${Math.random().toString(36).slice(2, 8)}`;
|
|
51
|
+
|
|
48
52
|
/** Local state for selected values */
|
|
49
53
|
let selectedValues = $state<Set<string>>(new Set());
|
|
50
54
|
|
|
@@ -131,7 +135,7 @@
|
|
|
131
135
|
{/if}
|
|
132
136
|
|
|
133
137
|
<!-- Options -->
|
|
134
|
-
<div class="choice-prompt__options" role={isMultiple ? 'group' : 'radiogroup'}>
|
|
138
|
+
<div class="choice-prompt__options" role={isMultiple ? 'group' : 'radiogroup'} aria-label={config.message}>
|
|
135
139
|
{#each config.options as option (option.value)}
|
|
136
140
|
{@const isChecked = isResolved ? isOptionResolved(option) : selectedValues.has(option.value)}
|
|
137
141
|
<label
|
|
@@ -141,7 +145,7 @@
|
|
|
141
145
|
>
|
|
142
146
|
<input
|
|
143
147
|
type={isMultiple ? 'checkbox' : 'radio'}
|
|
144
|
-
name=
|
|
148
|
+
name={groupName}
|
|
145
149
|
value={option.value}
|
|
146
150
|
checked={isChecked}
|
|
147
151
|
disabled={isResolved || isSubmitting}
|
|
@@ -295,9 +299,19 @@
|
|
|
295
299
|
|
|
296
300
|
.choice-prompt__input {
|
|
297
301
|
position: absolute;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
width: 1px;
|
|
303
|
+
height: 1px;
|
|
304
|
+
padding: 0;
|
|
305
|
+
margin: -1px;
|
|
306
|
+
overflow: hidden;
|
|
307
|
+
clip: rect(0, 0, 0, 0);
|
|
308
|
+
white-space: nowrap;
|
|
309
|
+
border: 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.choice-prompt__option:focus-within {
|
|
313
|
+
outline: 2px solid var(--fd-ring);
|
|
314
|
+
outline-offset: 2px;
|
|
301
315
|
}
|
|
302
316
|
|
|
303
317
|
.choice-prompt__checkmark {
|
|
@@ -372,6 +386,11 @@
|
|
|
372
386
|
transform: translateY(-1px);
|
|
373
387
|
}
|
|
374
388
|
|
|
389
|
+
.choice-prompt__submit:focus-visible {
|
|
390
|
+
outline: 2px solid var(--fd-ring);
|
|
391
|
+
outline-offset: 2px;
|
|
392
|
+
}
|
|
393
|
+
|
|
375
394
|
.choice-prompt__submit:disabled {
|
|
376
395
|
opacity: 0.5;
|
|
377
396
|
cursor: not-allowed;
|
|
@@ -256,6 +256,8 @@
|
|
|
256
256
|
class:interrupt-bubble--cancelled={currentInterrupt.machineState.status === 'cancelled'}
|
|
257
257
|
class:interrupt-bubble--submitting={isSubmitting}
|
|
258
258
|
class:interrupt-bubble--error={currentInterrupt.machineState.status === 'error'}
|
|
259
|
+
role="group"
|
|
260
|
+
aria-label={getTypeLabel(currentInterrupt.type)}
|
|
259
261
|
>
|
|
260
262
|
<!-- Header -->
|
|
261
263
|
<div class="interrupt-bubble__header">
|
|
@@ -517,6 +519,11 @@
|
|
|
517
519
|
background-color: var(--fd-error-hover);
|
|
518
520
|
}
|
|
519
521
|
|
|
522
|
+
.interrupt-bubble__retry-btn:focus-visible {
|
|
523
|
+
outline: 2px solid var(--fd-ring);
|
|
524
|
+
outline-offset: 2px;
|
|
525
|
+
}
|
|
526
|
+
|
|
520
527
|
/* Body - prompt content area, full width */
|
|
521
528
|
.interrupt-bubble__body {
|
|
522
529
|
padding: var(--fd-space-xl);
|
|
@@ -589,6 +596,11 @@
|
|
|
589
596
|
background-color: var(--fd-error-muted);
|
|
590
597
|
}
|
|
591
598
|
|
|
599
|
+
.interrupt-bubble__cancel-btn:focus-visible {
|
|
600
|
+
outline: 2px solid var(--fd-ring);
|
|
601
|
+
outline-offset: 2px;
|
|
602
|
+
}
|
|
603
|
+
|
|
592
604
|
.interrupt-bubble__cancel-btn:disabled {
|
|
593
605
|
opacity: 0.5;
|
|
594
606
|
cursor: not-allowed;
|
|
@@ -533,6 +533,11 @@
|
|
|
533
533
|
color: var(--fd-error);
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
.review-prompt__bulk-btn:focus-visible {
|
|
537
|
+
outline: 2px solid var(--fd-ring);
|
|
538
|
+
outline-offset: 2px;
|
|
539
|
+
}
|
|
540
|
+
|
|
536
541
|
.review-prompt__bulk-btn:disabled {
|
|
537
542
|
opacity: 0.5;
|
|
538
543
|
cursor: not-allowed;
|
|
@@ -630,6 +635,11 @@
|
|
|
630
635
|
color: var(--fd-error-foreground);
|
|
631
636
|
}
|
|
632
637
|
|
|
638
|
+
.review-prompt__toggle-btn:focus-visible {
|
|
639
|
+
outline: 2px solid var(--fd-ring);
|
|
640
|
+
outline-offset: 2px;
|
|
641
|
+
}
|
|
642
|
+
|
|
633
643
|
.review-prompt__toggle-btn:disabled {
|
|
634
644
|
opacity: 0.5;
|
|
635
645
|
cursor: not-allowed;
|
|
@@ -722,6 +732,11 @@
|
|
|
722
732
|
border-color: var(--fd-border-strong);
|
|
723
733
|
}
|
|
724
734
|
|
|
735
|
+
.review-prompt__html-toggle-btn:focus-visible {
|
|
736
|
+
outline: 2px solid var(--fd-ring);
|
|
737
|
+
outline-offset: 2px;
|
|
738
|
+
}
|
|
739
|
+
|
|
725
740
|
/* Raw HTML code display */
|
|
726
741
|
.review-prompt__raw-html {
|
|
727
742
|
font-family: var(--fd-review-font-mono);
|
|
@@ -820,6 +835,11 @@
|
|
|
820
835
|
transform: translateY(-1px);
|
|
821
836
|
}
|
|
822
837
|
|
|
838
|
+
.review-prompt__submit:focus-visible {
|
|
839
|
+
outline: 2px solid var(--fd-ring);
|
|
840
|
+
outline-offset: 2px;
|
|
841
|
+
}
|
|
842
|
+
|
|
823
843
|
.review-prompt__submit:disabled {
|
|
824
844
|
opacity: 0.5;
|
|
825
845
|
cursor: not-allowed;
|
|
@@ -210,9 +210,7 @@
|
|
|
210
210
|
style="top: 50%; transform: translateY(-50%); --fd-handle-fill: {getDataTypeColorToken(
|
|
211
211
|
port.dataType
|
|
212
212
|
)}; --fd-handle-border-color: var(--fd-handle-border);"
|
|
213
|
-
|
|
214
|
-
tabindex={0}
|
|
215
|
-
aria-label={graph.connectInputPort({ name: port.name })}
|
|
213
|
+
tabindex={-1}
|
|
216
214
|
/>
|
|
217
215
|
|
|
218
216
|
<!-- Port Info: padding lives here so handle position is simple -->
|
|
@@ -298,9 +296,7 @@
|
|
|
298
296
|
: getDataTypeColorToken(
|
|
299
297
|
'trigger'
|
|
300
298
|
)}; --fd-handle-border-color: var(--fd-handle-border);"
|
|
301
|
-
|
|
302
|
-
tabindex={0}
|
|
303
|
-
aria-label={graph.connectBranch({ name: branch.name })}
|
|
299
|
+
tabindex={-1}
|
|
304
300
|
/>
|
|
305
301
|
</div>
|
|
306
302
|
{/each}
|
|
@@ -264,9 +264,7 @@
|
|
|
264
264
|
style="top: 50%; transform: translateY(-50%); --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColorToken(
|
|
265
265
|
port.dataType
|
|
266
266
|
)}); --fd-handle-border-color: var(--fd-handle-border);"
|
|
267
|
-
|
|
268
|
-
tabindex={0}
|
|
269
|
-
aria-label={graph.connectInputPort({ name: port.name })}
|
|
267
|
+
tabindex={-1}
|
|
270
268
|
/>
|
|
271
269
|
|
|
272
270
|
<!-- Port Info: padding lives here so handle position is simple -->
|
|
@@ -342,9 +340,7 @@
|
|
|
342
340
|
style="top: 50%; transform: translateY(-50%); --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColorToken(
|
|
343
341
|
port.dataType
|
|
344
342
|
)}); --fd-handle-border-color: var(--fd-handle-border);"
|
|
345
|
-
|
|
346
|
-
tabindex={0}
|
|
347
|
-
aria-label={graph.connectOutputPort({ name: port.name })}
|
|
343
|
+
tabindex={-1}
|
|
348
344
|
/>
|
|
349
345
|
</div>
|
|
350
346
|
{/each}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ChatInput Component
|
|
3
|
+
|
|
4
|
+
The textarea + Run/Send/Stop button. Reusable input primitive shared by
|
|
5
|
+
ChatPanel (conversational) and ControlPanel (orchestration controls).
|
|
6
|
+
|
|
7
|
+
Reads execution state from playgroundStore. Owns its own input string and
|
|
8
|
+
textarea ref; emits sent content via onSendMessage.
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import Icon from '@iconify/svelte';
|
|
13
|
+
import { tick, untrack } from 'svelte';
|
|
14
|
+
import { hasEnableRunFlag } from '../../types/playground.js';
|
|
15
|
+
import {
|
|
16
|
+
getMessages,
|
|
17
|
+
getIsExecuting,
|
|
18
|
+
getCanSendMessage,
|
|
19
|
+
getCurrentSession
|
|
20
|
+
} from '../../stores/playgroundStore.svelte.js';
|
|
21
|
+
import { m } from '../../messages/index.js';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
/** Show the textarea (default: true). When false, only the Run button is shown. */
|
|
26
|
+
showTextarea?: boolean;
|
|
27
|
+
/** Show the Run button when textarea is hidden (default: true) */
|
|
28
|
+
showRunButton?: boolean;
|
|
29
|
+
/** Message sent when Run is clicked with textarea hidden */
|
|
30
|
+
predefinedMessage?: string;
|
|
31
|
+
onSendMessage?: (content: string) => void;
|
|
32
|
+
onStopExecution?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
placeholder,
|
|
37
|
+
showTextarea = true,
|
|
38
|
+
showRunButton = true,
|
|
39
|
+
predefinedMessage,
|
|
40
|
+
onSendMessage,
|
|
41
|
+
onStopExecution
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
const actions = $derived(m().playground.actions);
|
|
45
|
+
const chat = $derived(m().playground.chat);
|
|
46
|
+
const states = $derived(m().playground.states);
|
|
47
|
+
|
|
48
|
+
const resolvedPlaceholder = $derived(placeholder ?? chat.placeholder);
|
|
49
|
+
const resolvedPredefinedMessage = $derived(predefinedMessage ?? chat.predefinedRun);
|
|
50
|
+
|
|
51
|
+
const noInputsAvailable = $derived(!showTextarea && !showRunButton);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tracks whether the Run button is enabled. Starts as true; becomes false
|
|
55
|
+
* after Run is clicked; re-enabled when the backend sends a message with
|
|
56
|
+
* enableRun: true metadata.
|
|
57
|
+
*/
|
|
58
|
+
let runEnabled = $state(true);
|
|
59
|
+
|
|
60
|
+
let inputValue = $state('');
|
|
61
|
+
let inputField: HTMLTextAreaElement | undefined;
|
|
62
|
+
|
|
63
|
+
// Count of enableRun messages seen so far — plain let, not $state.
|
|
64
|
+
// Written with untrack to make the bookkeeping intent explicit.
|
|
65
|
+
let seenEnableRunCount = 0;
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const count = getMessages().filter(m => hasEnableRunFlag(m.metadata)).length;
|
|
69
|
+
if (count > seenEnableRunCount) {
|
|
70
|
+
untrack(() => { seenEnableRunCount = count; });
|
|
71
|
+
runEnabled = true;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
$effect(() => {
|
|
76
|
+
if (getCurrentSession()?.id) {
|
|
77
|
+
untrack(() => { seenEnableRunCount = 0; });
|
|
78
|
+
runEnabled = true;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let wasExecuting = false;
|
|
83
|
+
|
|
84
|
+
/** Auto-focus input when execution completes */
|
|
85
|
+
$effect(() => {
|
|
86
|
+
const nowExecuting = getIsExecuting();
|
|
87
|
+
if (wasExecuting && !nowExecuting && inputField) {
|
|
88
|
+
tick().then(() => inputField?.focus({ preventScroll: true }));
|
|
89
|
+
}
|
|
90
|
+
untrack(() => { wasExecuting = nowExecuting; });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function handleSend(): void {
|
|
94
|
+
const trimmedValue = inputValue.trim();
|
|
95
|
+
if (!trimmedValue || !getCanSendMessage()) return;
|
|
96
|
+
|
|
97
|
+
onSendMessage?.(trimmedValue);
|
|
98
|
+
inputValue = '';
|
|
99
|
+
|
|
100
|
+
if (inputField) {
|
|
101
|
+
inputField.style.height = 'auto';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
tick().then(() => {
|
|
105
|
+
inputField?.focus({ preventScroll: true });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
110
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
handleSend();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleStop(): void {
|
|
117
|
+
onStopExecution?.();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleRun(): void {
|
|
121
|
+
if (getIsExecuting() || !runEnabled) return;
|
|
122
|
+
runEnabled = false;
|
|
123
|
+
onSendMessage?.(resolvedPredefinedMessage);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleInput(): void {
|
|
127
|
+
if (inputField) {
|
|
128
|
+
inputField.style.height = 'auto';
|
|
129
|
+
inputField.style.height = `${Math.min(inputField.scrollHeight, 120)}px`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<div class="chat-input">
|
|
135
|
+
{#if noInputsAvailable}
|
|
136
|
+
<div class="chat-input__no-inputs">
|
|
137
|
+
<Icon icon="mdi:information-outline" />
|
|
138
|
+
<span>{states.viewOnlyHelp}</span>
|
|
139
|
+
</div>
|
|
140
|
+
{:else}
|
|
141
|
+
<div class="chat-input__container" class:chat-input__container--run-only={!showTextarea}>
|
|
142
|
+
{#if showTextarea}
|
|
143
|
+
<div class="chat-input__wrapper">
|
|
144
|
+
<textarea
|
|
145
|
+
bind:this={inputField}
|
|
146
|
+
bind:value={inputValue}
|
|
147
|
+
class="chat-input__textarea"
|
|
148
|
+
placeholder={resolvedPlaceholder}
|
|
149
|
+
rows="1"
|
|
150
|
+
disabled={getIsExecuting() || !getCurrentSession()}
|
|
151
|
+
onkeydown={handleKeydown}
|
|
152
|
+
oninput={handleInput}
|
|
153
|
+
></textarea>
|
|
154
|
+
</div>
|
|
155
|
+
{/if}
|
|
156
|
+
|
|
157
|
+
{#if getIsExecuting()}
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="chat-input__stop-btn"
|
|
161
|
+
onclick={handleStop}
|
|
162
|
+
title={actions.stopTitle}
|
|
163
|
+
aria-label={actions.stopTitle}
|
|
164
|
+
>
|
|
165
|
+
<Icon icon="mdi:stop" />
|
|
166
|
+
{actions.stop}
|
|
167
|
+
</button>
|
|
168
|
+
{:else if showTextarea}
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="chat-input__send-btn"
|
|
172
|
+
onclick={handleSend}
|
|
173
|
+
disabled={!inputValue.trim() || !getCanSendMessage()}
|
|
174
|
+
title={actions.sendTitle}
|
|
175
|
+
aria-label={actions.sendTitle}
|
|
176
|
+
>
|
|
177
|
+
{actions.send}
|
|
178
|
+
</button>
|
|
179
|
+
{:else if showRunButton}
|
|
180
|
+
{@const runLabel = runEnabled ? actions.runTitle : actions.runWaitingTitle}
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
class="chat-input__run-btn"
|
|
184
|
+
onclick={handleRun}
|
|
185
|
+
disabled={!runEnabled}
|
|
186
|
+
title={runLabel}
|
|
187
|
+
aria-label={runLabel}
|
|
188
|
+
>
|
|
189
|
+
<Icon icon="mdi:play" />
|
|
190
|
+
{actions.run}
|
|
191
|
+
</button>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<style>
|
|
198
|
+
.chat-input {
|
|
199
|
+
flex-shrink: 0;
|
|
200
|
+
padding: var(--fd-space-xl) var(--fd-space-3xl) var(--fd-space-3xl);
|
|
201
|
+
background-color: var(--fd-background);
|
|
202
|
+
border-top: 1px solid var(--fd-border-muted);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.chat-input__container {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: flex-end;
|
|
208
|
+
gap: var(--fd-space-md);
|
|
209
|
+
max-width: 760px;
|
|
210
|
+
margin: 0 auto;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.chat-input__container--run-only {
|
|
214
|
+
justify-content: flex-end;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.chat-input__wrapper {
|
|
218
|
+
flex: 1;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: flex-end;
|
|
221
|
+
background-color: var(--fd-background);
|
|
222
|
+
border: 1px solid var(--fd-border);
|
|
223
|
+
border-radius: var(--fd-radius-xl);
|
|
224
|
+
padding: var(--fd-space-sm) var(--fd-space-md);
|
|
225
|
+
transition:
|
|
226
|
+
border-color var(--fd-transition-fast),
|
|
227
|
+
box-shadow var(--fd-transition-fast);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.chat-input__wrapper:focus-within {
|
|
231
|
+
border-color: var(--fd-primary);
|
|
232
|
+
box-shadow: 0 0 0 3px var(--fd-primary-muted);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.chat-input__textarea {
|
|
236
|
+
flex: 1;
|
|
237
|
+
border: none;
|
|
238
|
+
outline: none;
|
|
239
|
+
resize: none;
|
|
240
|
+
font-family: inherit;
|
|
241
|
+
font-size: var(--fd-text-base);
|
|
242
|
+
line-height: var(--fd-leading-normal);
|
|
243
|
+
max-height: 120px;
|
|
244
|
+
background: transparent;
|
|
245
|
+
color: var(--fd-foreground);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.chat-input__textarea::placeholder {
|
|
249
|
+
color: var(--fd-muted-foreground);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.chat-input__textarea:disabled {
|
|
253
|
+
cursor: not-allowed;
|
|
254
|
+
opacity: 0.6;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.chat-input__send-btn {
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
justify-content: center;
|
|
261
|
+
padding: var(--fd-space-sm) var(--fd-space-2xl);
|
|
262
|
+
border: none;
|
|
263
|
+
border-radius: var(--fd-radius-lg);
|
|
264
|
+
background-color: var(--fd-foreground);
|
|
265
|
+
color: var(--fd-background);
|
|
266
|
+
font-size: var(--fd-text-sm);
|
|
267
|
+
font-weight: 500;
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
transition: all var(--fd-transition-fast);
|
|
270
|
+
flex-shrink: 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.chat-input__send-btn:hover:not(:disabled) {
|
|
274
|
+
opacity: 0.85;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.chat-input__send-btn:disabled {
|
|
278
|
+
background-color: var(--fd-foreground);
|
|
279
|
+
color: var(--fd-background);
|
|
280
|
+
opacity: 0.3;
|
|
281
|
+
cursor: not-allowed;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.chat-input__stop-btn {
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: var(--fd-space-3xs);
|
|
288
|
+
padding: var(--fd-space-sm) var(--fd-space-xl);
|
|
289
|
+
border: none;
|
|
290
|
+
border-radius: var(--fd-radius-lg);
|
|
291
|
+
background-color: var(--fd-error);
|
|
292
|
+
color: var(--fd-error-foreground);
|
|
293
|
+
font-size: var(--fd-text-sm);
|
|
294
|
+
font-weight: 500;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
transition: background-color var(--fd-transition-fast);
|
|
297
|
+
flex-shrink: 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.chat-input__stop-btn:hover {
|
|
301
|
+
background-color: var(--fd-error-hover);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.chat-input__run-btn {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
gap: var(--fd-space-3xs);
|
|
308
|
+
padding: var(--fd-space-sm) var(--fd-space-2xl);
|
|
309
|
+
border: none;
|
|
310
|
+
border-radius: var(--fd-radius-lg);
|
|
311
|
+
background-color: var(--fd-success);
|
|
312
|
+
color: var(--fd-success-foreground);
|
|
313
|
+
font-size: var(--fd-text-sm);
|
|
314
|
+
font-weight: 500;
|
|
315
|
+
cursor: pointer;
|
|
316
|
+
transition: all var(--fd-transition-fast);
|
|
317
|
+
flex-shrink: 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.chat-input__run-btn:hover:not(:disabled) {
|
|
321
|
+
background-color: var(--fd-success-hover);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.chat-input__run-btn:disabled {
|
|
325
|
+
background-color: var(--fd-border);
|
|
326
|
+
color: var(--fd-muted-foreground);
|
|
327
|
+
cursor: not-allowed;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.chat-input__no-inputs {
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
justify-content: center;
|
|
334
|
+
gap: var(--fd-space-xs);
|
|
335
|
+
padding: var(--fd-space-md) var(--fd-space-xl);
|
|
336
|
+
background-color: var(--fd-muted);
|
|
337
|
+
border-radius: var(--fd-radius-lg);
|
|
338
|
+
color: var(--fd-muted-foreground);
|
|
339
|
+
font-size: var(--fd-text-sm);
|
|
340
|
+
max-width: 760px;
|
|
341
|
+
margin: 0 auto;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@media (max-width: 640px) {
|
|
345
|
+
.chat-input {
|
|
346
|
+
padding: var(--fd-space-md) var(--fd-space-xl) var(--fd-space-xl);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.chat-input__container {
|
|
350
|
+
gap: var(--fd-space-xs);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.chat-input__send-btn,
|
|
354
|
+
.chat-input__stop-btn,
|
|
355
|
+
.chat-input__run-btn {
|
|
356
|
+
padding: var(--fd-space-xs) var(--fd-space-xl);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
</style>
|