windmill-components 1.501.23 → 1.502.2

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.
Files changed (30) hide show
  1. package/package/components/Dev.svelte +4 -5
  2. package/package/components/apps/components/display/AppCarouselList.svelte +39 -4
  3. package/package/components/apps/editor/appUtils.js +4 -0
  4. package/package/components/apps/editor/componentsPanel/componentControlUtils.js +3 -1
  5. package/package/components/apps/editor/settingsPanel/ComponentPanelDataSource.svelte +0 -1
  6. package/package/components/auditLogs/AuditLogsFilters.svelte +12 -2
  7. package/package/components/auditLogs/AuditLogsTable.svelte +209 -122
  8. package/package/components/auditLogs/AuditLogsTable.svelte.d.ts +5 -20
  9. package/package/components/auditLogs/AuditLogsTimeline.svelte +449 -0
  10. package/package/components/auditLogs/AuditLogsTimeline.svelte.d.ts +16 -0
  11. package/package/components/copilot/chat/AIChatDisplay.svelte +23 -165
  12. package/package/components/copilot/chat/AIChatInput.svelte +128 -0
  13. package/package/components/copilot/chat/AIChatInput.svelte.d.ts +16 -0
  14. package/package/components/copilot/chat/AIChatManager.svelte.d.ts +4 -1
  15. package/package/components/copilot/chat/AIChatManager.svelte.js +33 -13
  16. package/package/components/copilot/chat/AIChatMessage.svelte +93 -0
  17. package/package/components/copilot/chat/AIChatMessage.svelte.d.ts +12 -0
  18. package/package/components/copilot/chat/ContextTextarea.svelte +13 -15
  19. package/package/components/copilot/chat/ContextTextarea.svelte.d.ts +4 -2
  20. package/package/components/copilot/chat/flow/FlowAIChat.svelte +11 -0
  21. package/package/components/copilot/chat/shared.d.ts +13 -3
  22. package/package/components/flow_builder.d.ts +10 -1
  23. package/package/components/graph/FlowGraphV2.svelte +1 -0
  24. package/package/components/schema/EditableSchemaWrapper.svelte +3 -3
  25. package/package/components/search/GlobalSearchModal.svelte +28 -18
  26. package/package/gen/core/OpenAPI.js +1 -1
  27. package/package/gen/schemas.gen.d.ts +11 -2
  28. package/package/gen/schemas.gen.js +11 -2
  29. package/package/gen/types.gen.d.ts +5 -2
  30. package/package.json +2 -1
@@ -0,0 +1,449 @@
1
+ <script lang="ts">import 'chartjs-adapter-date-fns';
2
+ import zoomPlugin from 'chartjs-plugin-zoom';
3
+ import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement, TimeScale } from 'chart.js';
4
+ import { Scatter } from '../chartjs-wrappers/chartJs';
5
+ import { Loader2 } from 'lucide-svelte';
6
+ import { untrack } from 'svelte';
7
+ import { sleep } from '../../utils';
8
+ import { usePromise } from '../../svelte5Utils.svelte';
9
+ let { logs = [], minTimeSet, maxTimeSet, onMissingJobSpan, onZoom, onLogSelected } = $props();
10
+ // Register ChartJS components
11
+ ChartJS.register(Title, Tooltip, Legend, zoomPlugin, LineElement, CategoryScale, LinearScale, PointElement, TimeScale);
12
+ function addSeconds(date, seconds) {
13
+ date.setTime(date.getTime() + seconds * 1000);
14
+ return date;
15
+ }
16
+ const zoomOptions = {
17
+ pan: {
18
+ mode: 'x',
19
+ enabled: true,
20
+ modifierKey: 'ctrl',
21
+ onPanComplete: ({ chart }) => {
22
+ chartInstance = chart;
23
+ onZoom?.({
24
+ min: addSeconds(new Date(chart.scales.x.min), -1),
25
+ max: addSeconds(new Date(chart.scales.x.max), 1)
26
+ });
27
+ }
28
+ },
29
+ zoom: {
30
+ drag: {
31
+ enabled: true
32
+ },
33
+ mode: 'x',
34
+ scaleMode: 'y',
35
+ onZoom: ({ chart }) => {
36
+ chartInstance = chart;
37
+ onZoom?.({
38
+ min: addSeconds(new Date(chart.scales.x.min), -1),
39
+ max: addSeconds(new Date(chart.scales.x.max), 1)
40
+ });
41
+ }
42
+ }
43
+ };
44
+ // Color mapping for different action kinds
45
+ const actionColors = {
46
+ Execute: '#3b82f6', // blue
47
+ Delete: '#ef4444', // red
48
+ Update: '#eab308', // yellow
49
+ Create: '#22c55e', // green
50
+ default: '#6b7280' // gray
51
+ };
52
+ function getActionColor(actionKind) {
53
+ return actionColors[actionKind] || actionColors.default;
54
+ }
55
+ async function groupLogsBySpan(logs, onMissingJobSpan) {
56
+ const grouped = {};
57
+ const jobGrouped = new Map();
58
+ for (const log of logs) {
59
+ const spanId = log.span || 'untraced';
60
+ if (spanId.startsWith('job-span-')) {
61
+ const jobid = spanId.slice('job-span-'.length);
62
+ if (!jobGrouped.has(jobid)) {
63
+ jobGrouped.set(jobid, []);
64
+ }
65
+ jobGrouped.get(jobid)?.push(log);
66
+ continue;
67
+ }
68
+ if (!grouped[spanId]) {
69
+ grouped[spanId] = [];
70
+ }
71
+ grouped[spanId].push(log);
72
+ }
73
+ for (const jobid of jobGrouped.keys()) {
74
+ const j = Object.values(grouped)
75
+ .flat()
76
+ .find((log) => log.parameters?.uuid === jobid);
77
+ if (j?.span != undefined) {
78
+ grouped[j.span].push(...jobGrouped.get(jobid));
79
+ jobGrouped.get(jobid)?.push(j);
80
+ }
81
+ else {
82
+ // Try to fetch missing job execution audit log
83
+ if (onMissingJobSpan) {
84
+ try {
85
+ const jobLogs = jobGrouped.get(jobid);
86
+ const additionalLogs = await onMissingJobSpan(jobid, jobLogs);
87
+ // Look for the job execution audit log in the new results
88
+ const jobExecutionLog = additionalLogs.find((log) => log.parameters?.uuid === jobid);
89
+ if (jobExecutionLog?.span) {
90
+ if (!grouped[jobExecutionLog.span]) {
91
+ grouped[jobExecutionLog.span] = [];
92
+ }
93
+ grouped[jobExecutionLog.span].push(jobExecutionLog, ...jobLogs);
94
+ jobGrouped.get(jobid)?.push(jobExecutionLog);
95
+ continue;
96
+ }
97
+ }
98
+ catch (error) {
99
+ console.warn(`Failed to fetch missing job audit span for job ${jobid}:`, error);
100
+ }
101
+ }
102
+ if (!grouped[jobid]) {
103
+ grouped[jobid] = [];
104
+ }
105
+ grouped[jobid].push(...jobGrouped.get(jobid));
106
+ }
107
+ }
108
+ // Sort logs within each span by timestamp
109
+ Object.values(grouped).forEach((spanLogs) => {
110
+ spanLogs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
111
+ });
112
+ return { grouped, jobGrouped };
113
+ }
114
+ function isDark() {
115
+ return document.documentElement.classList.contains('dark');
116
+ }
117
+ // Function to apply zoom-aware jittering to overlapping points
118
+ function applyJittering(dataPoints, baseY, chartInstance) {
119
+ if (dataPoints.length <= 1)
120
+ return dataPoints;
121
+ // Sort by timestamp
122
+ const sorted = [...dataPoints].sort((a, b) => new Date(a.x).getTime() - new Date(b.x).getTime());
123
+ // Calculate visual overlap based on chart scale
124
+ const pointRadius = 0.8; // Current point radius
125
+ const overlapThreshold = pointRadius * 2; // Points overlap if closer than this in pixels
126
+ // Group points that visually overlap
127
+ const groups = [];
128
+ let currentGroup = [sorted[0]];
129
+ for (let i = 1; i < sorted.length; i++) {
130
+ const prevTime = new Date(sorted[i - 1].x).getTime();
131
+ const currTime = new Date(sorted[i].x).getTime();
132
+ // Calculate pixel distance between points
133
+ let pixelDistance = overlapThreshold + 1; // Default to no overlap
134
+ if (chartInstance && chartInstance.scales && chartInstance.scales.x) {
135
+ const prevPixel = chartInstance.scales.x.getPixelForValue(prevTime);
136
+ const currPixel = chartInstance.scales.x.getPixelForValue(currTime);
137
+ pixelDistance = Math.abs(currPixel - prevPixel);
138
+ }
139
+ else {
140
+ pixelDistance = 20000;
141
+ }
142
+ if (pixelDistance < overlapThreshold) {
143
+ currentGroup.push(sorted[i]);
144
+ }
145
+ else {
146
+ groups.push(currentGroup);
147
+ currentGroup = [sorted[i]];
148
+ }
149
+ }
150
+ groups.push(currentGroup);
151
+ const jitteredPoints = [];
152
+ groups.forEach((group) => {
153
+ if (group.length === 1) {
154
+ jitteredPoints.push({
155
+ ...group[0],
156
+ y: baseY,
157
+ isCluster: false,
158
+ clusterSize: 1
159
+ });
160
+ }
161
+ else {
162
+ const jitterRange = 0.4;
163
+ group.forEach((point, index) => {
164
+ let jitterOffset = (1 - Math.exp(-group.length / 50)) * jitterRange * (Math.random() - 0.5);
165
+ jitteredPoints.push({
166
+ ...point,
167
+ y: baseY + jitterOffset,
168
+ originalY: baseY,
169
+ isCluster: false,
170
+ clusterSize: group.length,
171
+ clusterIndex: index
172
+ });
173
+ });
174
+ }
175
+ });
176
+ return jitteredPoints;
177
+ }
178
+ // Set chart defaults based on theme
179
+ ChartJS.defaults.color = isDark() ? '#ccc' : '#666';
180
+ ChartJS.defaults.borderColor = isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
181
+ async function getGroupedData() {
182
+ if (logs.length === 0) {
183
+ await sleep(1);
184
+ return { grouped: {}, jobGrouped: new Map() };
185
+ }
186
+ try {
187
+ return await groupLogsBySpan(logs, onMissingJobSpan);
188
+ }
189
+ catch (error) {
190
+ console.error('Error grouping logs:', error);
191
+ return await groupLogsBySpan(logs);
192
+ }
193
+ }
194
+ let groupedData = usePromise(getGroupedData);
195
+ // let isGrouping = $state(false)
196
+ let chartInstance = $state(null);
197
+ let { minTime, maxTime } = $derived(computeMinMaxTime(logs, minTimeSet, maxTimeSet));
198
+ function computeMinMaxTime(logs, minTimeSet, maxTimeSet) {
199
+ let minTime = addSeconds(new Date(), -300);
200
+ let maxTime = new Date();
201
+ let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined;
202
+ let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined;
203
+ if (minTimeSetDate && maxTimeSetDate) {
204
+ minTime = minTimeSetDate;
205
+ maxTime = maxTimeSetDate;
206
+ return { minTime, maxTime };
207
+ }
208
+ if (logs == undefined || logs?.length == 0) {
209
+ minTime = minTimeSetDate ?? addSeconds(new Date(), -300);
210
+ maxTime = maxTimeSetDate ?? new Date();
211
+ return { minTime, maxTime };
212
+ }
213
+ const maxLogsTime = new Date(logs.reduce((max, current) => new Date(current.timestamp) > new Date(max.timestamp) ? current : max).timestamp);
214
+ const maxJob = maxTimeSetDate === undefined ? new Date() : maxLogsTime;
215
+ const minJob = new Date(logs.reduce((max, current) => new Date(current.timestamp) < new Date(max.timestamp) ? current : max).timestamp);
216
+ const diff = (maxJob.getTime() - minJob.getTime()) / 20000;
217
+ minTime = minTimeSetDate ?? addSeconds(minJob, -diff);
218
+ if (maxTimeSetDate) {
219
+ maxTime = maxTimeSetDate ?? maxJob;
220
+ }
221
+ else {
222
+ maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff);
223
+ }
224
+ return { minTime, maxTime };
225
+ }
226
+ $effect(() => {
227
+ logs && untrack(() => groupedData.refresh());
228
+ });
229
+ const groupedLogs = $derived(groupedData.value?.grouped ?? {});
230
+ const jobGrouped = $derived(groupedData.value?.jobGrouped ?? new Map());
231
+ const spanIds = $derived(Object.keys(groupedLogs).sort());
232
+ const spanAuthors = $derived(spanIds.map((span) => {
233
+ if (span == 'untraced') {
234
+ return 'untraced';
235
+ }
236
+ const endUser = groupedLogs[span][0]?.parameters?.end_user;
237
+ const endUserText = endUser ? ` (${endUser})` : '';
238
+ return groupedLogs[span]?.length > 0 ? `${groupedLogs[span][0].username}${endUserText}` : '';
239
+ }));
240
+ // Transform data for ChartJS scatter plot
241
+ const chartData = $derived(() => {
242
+ if (untrack(() => logs.length) === 0) {
243
+ return { datasets: [] };
244
+ }
245
+ const datasets = [];
246
+ // Create datasets for regular span groups (points only)
247
+ spanIds.forEach((spanId, index) => {
248
+ const spanLogs = groupedLogs[spanId];
249
+ // Create initial data points
250
+ const dataPoints = spanLogs.map((log) => ({
251
+ x: log.timestamp,
252
+ y: index, // Each span gets its own y-axis position
253
+ log: log // Store full log data for tooltips
254
+ }));
255
+ // Apply zoom-aware jittering to spread out overlapping points
256
+ const jitteredPoints = applyJittering(dataPoints, index, chartInstance);
257
+ // const jitteredPoints = dataPoints
258
+ datasets.push({
259
+ label: spanId === 'untraced' ? 'Untraced' : spanId,
260
+ data: jitteredPoints,
261
+ backgroundColor: jitteredPoints.map((point) => {
262
+ const baseColor = getActionColor(point.log.action_kind);
263
+ // Make clustered points slightly more opaque
264
+ return point.isCluster ? baseColor + 'E0' : baseColor;
265
+ }),
266
+ borderColor: jitteredPoints.map((point) => {
267
+ const baseColor = getActionColor(point.log.action_kind);
268
+ // Add white border to clustered points for better visibility
269
+ return point.isCluster ? '#ffffff' : baseColor;
270
+ }),
271
+ borderWidth: jitteredPoints.map((point) => (point.isCluster ? 1 : 1)),
272
+ pointRadius: jitteredPoints.map((point) => (point.isCluster ? 3 : 3)),
273
+ pointHoverRadius: jitteredPoints.map((point) => (point.isCluster ? 5 : 5)),
274
+ showLine: false
275
+ });
276
+ });
277
+ // Create datasets for job-connected lines
278
+ jobGrouped.forEach((jobLogs, jobId) => {
279
+ if (jobLogs.length > 1) {
280
+ // Only create lines if there are multiple points
281
+ // Sort job logs by timestamp to ensure proper line connection
282
+ const sortedJobLogs = [...jobLogs].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
283
+ // Find the y-position for each log based on its span and jittered position
284
+ const lineData = sortedJobLogs.map((log) => {
285
+ const spanId = log.span || 'untraced';
286
+ let baseYPosition = spanIds.indexOf(spanId);
287
+ if (baseYPosition === -1) {
288
+ // Handle job-span logs that might not be in regular spans
289
+ const jobSpanId = spanId.startsWith('job-span-')
290
+ ? spanId.slice('job-span-'.length)
291
+ : spanId;
292
+ baseYPosition = spanIds.findIndex((id) => id === jobSpanId);
293
+ if (baseYPosition === -1) {
294
+ // If still not found, assign to the span where this job's audit logs are grouped
295
+ const auditSpan = Object.entries(groupedLogs).find(([, spanLogs]) => spanLogs.some((l) => l.parameters?.uuid === jobId))?.[0];
296
+ baseYPosition = auditSpan ? spanIds.indexOf(auditSpan) : 0;
297
+ }
298
+ }
299
+ // Find the jittered position for this specific log
300
+ let jitteredY = baseYPosition;
301
+ if (baseYPosition >= 0 && baseYPosition < datasets.length) {
302
+ const spanDataset = datasets[baseYPosition];
303
+ if (spanDataset && spanDataset.data) {
304
+ const matchingPoint = spanDataset.data.find((point) => point.log && point.log.id === log.id);
305
+ if (matchingPoint) {
306
+ jitteredY = matchingPoint.y;
307
+ }
308
+ }
309
+ }
310
+ return {
311
+ x: log.timestamp,
312
+ y: jitteredY,
313
+ log: log
314
+ };
315
+ });
316
+ datasets.push({
317
+ label: `Job ${jobId} Connection`,
318
+ data: lineData,
319
+ backgroundColor: 'transparent',
320
+ borderColor: '#8b5cf6', // Purple color for job connections
321
+ borderWidth: 2,
322
+ pointRadius: 0, // Hide points for connection lines
323
+ pointHoverRadius: 0,
324
+ showLine: true,
325
+ tension: 0, // Straight lines
326
+ fill: false
327
+ });
328
+ }
329
+ });
330
+ return { datasets };
331
+ });
332
+ const chartOptions = $derived(() => ({
333
+ responsive: true,
334
+ maintainAspectRatio: false,
335
+ plugins: {
336
+ zoom: zoomOptions,
337
+ legend: {
338
+ display: false // We'll create our own legend
339
+ },
340
+ tooltip: {
341
+ callbacks: {
342
+ title: function (context) {
343
+ const log = context[0].raw.log;
344
+ let title = `${log.operation} - ${log.action_kind}`;
345
+ return title;
346
+ },
347
+ label: function (context) {
348
+ const log = context.raw.log;
349
+ const labels = [
350
+ `User: ${log.username}`,
351
+ `Resource: ${log.resource}`,
352
+ `Time: ${new Date(log.timestamp).toLocaleString()}`
353
+ ];
354
+ return labels;
355
+ }
356
+ }
357
+ }
358
+ },
359
+ scales: {
360
+ x: {
361
+ type: 'time',
362
+ time: {
363
+ displayFormats: {
364
+ millisecond: 'HH:mm:ss.SSS',
365
+ second: 'HH:mm:ss',
366
+ minute: 'HH:mm',
367
+ hour: 'MMM dd HH:mm',
368
+ day: 'MMM dd',
369
+ week: 'MMM dd',
370
+ month: 'MMM yyyy',
371
+ quarter: 'MMM yyyy',
372
+ year: 'yyyy'
373
+ }
374
+ },
375
+ title: {
376
+ display: true,
377
+ text: 'Time'
378
+ },
379
+ grid: {
380
+ display: false
381
+ },
382
+ ticks: {
383
+ maxTicksLimit: 15
384
+ },
385
+ min: minTime.getTime(),
386
+ max: maxTime.getTime()
387
+ },
388
+ y: {
389
+ type: 'linear',
390
+ min: -1,
391
+ max: spanIds.length,
392
+ grid: {
393
+ display: true,
394
+ color: isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
395
+ },
396
+ ticks: {
397
+ autoSkip: false,
398
+ maxTicksLimit: 22,
399
+ stepSize: 1,
400
+ callback: function (value) {
401
+ if (spanIds.length > 20) {
402
+ return '';
403
+ }
404
+ const index = Math.round(value);
405
+ if (index >= 0 && index < spanIds.length) {
406
+ // const spanId = `${spanAuthors[index]} - ${index}`
407
+ const spanId = spanAuthors[index];
408
+ return spanId === 'untraced' ? 'Untraced' : spanId.slice(0, 30);
409
+ }
410
+ return '';
411
+ }
412
+ }
413
+ }
414
+ },
415
+ onClick: (event, elements, chart) => {
416
+ // Capture chart instance for jittering calculations
417
+ if (!chartInstance) {
418
+ chartInstance = chart;
419
+ }
420
+ if (elements.length > 0) {
421
+ const element = elements[0];
422
+ const log = chartData().datasets[element.datasetIndex].data[element.index].log;
423
+ onLogSelected?.(log);
424
+ }
425
+ },
426
+ onHover: (event, elements, chart) => {
427
+ // Capture chart instance for jittering calculations
428
+ if (!chartInstance) {
429
+ chartInstance = chart;
430
+ }
431
+ },
432
+ animation: {
433
+ duration: 300
434
+ }
435
+ }));
436
+ </script>
437
+
438
+ <div class="p-4 bg-surface mb-4 h-full">
439
+ {#if logs.length === 0}
440
+ <div class="text-center py-8 text-secondary"> No audit logs to display </div>
441
+ {:else if !groupedData || groupedData.status === 'loading'}
442
+ <div class="text-center py-8 text-secondary">
443
+ <Loader2 size={24} class="animate-spin mx-auto mb-2" />
444
+ Processing audit logs...
445
+ </div>
446
+ {:else}
447
+ <Scatter data={chartData()} options={chartOptions()} />
448
+ {/if}
449
+ </div>
@@ -0,0 +1,16 @@
1
+ import 'chartjs-adapter-date-fns';
2
+ import type { AuditLog } from '../../gen';
3
+ interface Props {
4
+ logs: AuditLog[];
5
+ minTimeSet: string | undefined;
6
+ maxTimeSet: string | undefined;
7
+ onMissingJobSpan?: (jobId: string, jobLogs: AuditLog[]) => Promise<AuditLog[]>;
8
+ onZoom?: (range: {
9
+ min: Date;
10
+ max: Date;
11
+ }) => void;
12
+ onLogSelected?: (log: any) => void;
13
+ }
14
+ declare const AuditLogsTimeline: import("svelte").Component<Props, {}, "">;
15
+ type AuditLogsTimeline = ReturnType<typeof AuditLogsTimeline>;
16
+ export default AuditLogsTimeline;
@@ -1,34 +1,22 @@
1
- <script lang="ts">import { twMerge } from 'tailwind-merge';
2
- import AssistantMessage from './AssistantMessage.svelte';
1
+ <script lang="ts">import AIChatMessage from './AIChatMessage.svelte';
3
2
  import {} from 'svelte';
4
- import { CheckIcon, HistoryIcon, Loader2, Plus, RefreshCwIcon, StopCircleIcon, Undo2Icon, X, XIcon } from 'lucide-svelte';
5
- import autosize from '../../../autosize';
3
+ import { CheckIcon, HistoryIcon, Loader2, Plus, StopCircleIcon, X, XIcon } from 'lucide-svelte';
6
4
  import Button from '../../common/button/Button.svelte';
7
5
  import Popover from '../../meltComponents/Popover.svelte';
8
6
  import {} from './shared';
9
- import ContextElementBadge from './ContextElementBadge.svelte';
10
- import ContextTextarea from './ContextTextarea.svelte';
11
- import AvailableContextList from './AvailableContextList.svelte';
12
7
  import ChatQuickActions from './ChatQuickActions.svelte';
13
8
  import ProviderModelSelector from './ProviderModelSelector.svelte';
14
9
  import ChatMode from './ChatMode.svelte';
15
10
  import Markdown from 'svelte-exmarkdown';
16
11
  import { aiChatManager, AIMode } from './AIChatManager.svelte';
12
+ import AIChatInput from './AIChatInput.svelte';
17
13
  let { messages, pastChats, hasDiff, diffMode = false, // todo: remove default
18
14
  selectedContext = $bindable([]), // todo: remove default
19
15
  availableContext = [], // todo: remove default
20
16
  loadPastChat, deletePastChat, saveAndClear, cancel, askAi = () => { }, // todo: remove default,
21
17
  headerLeft, headerRight, disabled = false, disabledMessage = '', suggestions = [] } = $props();
22
- let contextTextareaComponent = $state();
23
- let instructionsTextarea = $state();
24
- export function focusInput() {
25
- if (aiChatManager.mode === 'script') {
26
- contextTextareaComponent?.focus();
27
- }
28
- else {
29
- instructionsTextarea?.focus();
30
- }
31
- }
18
+ let aiChatInput = $state();
19
+ let editingMessageIndex = $state(null);
32
20
  let scrollEl = $state();
33
21
  async function scrollDown() {
34
22
  scrollEl?.scrollTo({
@@ -40,29 +28,11 @@ let height = $state(0);
40
28
  $effect(() => {
41
29
  aiChatManager.automaticScroll && height && scrollDown();
42
30
  });
43
- function addContextToSelection(contextElement) {
44
- if (selectedContext &&
45
- availableContext &&
46
- !selectedContext.find((c) => c.type === contextElement.type && c.title === contextElement.title) &&
47
- availableContext.find((c) => c.type === contextElement.type && c.title === contextElement.title)) {
48
- selectedContext = [...selectedContext, contextElement];
49
- }
50
- }
51
31
  function submitSuggestion(suggestion) {
52
- aiChatManager.instructions = suggestion;
53
- aiChatManager.sendRequest();
32
+ aiChatManager.sendRequest({ instructions: suggestion });
54
33
  }
55
- function isLastUserMessage(messageIndex) {
56
- // Find the last user message index
57
- for (let i = messages.length - 1; i >= 0; i--) {
58
- if (messages[i].role === 'user') {
59
- return i === messageIndex;
60
- }
61
- }
62
- return false;
63
- }
64
- function restartGeneration(messageIndex) {
65
- aiChatManager.restartLastGeneration(messageIndex);
34
+ export function focusInput() {
35
+ aiChatInput?.focusInput();
66
36
  }
67
37
  </script>
68
38
 
@@ -152,68 +122,13 @@ function restartGeneration(messageIndex) {
152
122
  >
153
123
  <div class="flex flex-col" bind:clientHeight={height}>
154
124
  {#each messages as message, messageIndex}
155
- <div class={twMerge(message.role === 'user' && messageIndex > 0 && 'mt-6', 'mb-2')}>
156
- {#if message.role === 'user' && message.contextElements}
157
- <div class="flex flex-row gap-1 mb-1 overflow-scroll no-scrollbar px-2">
158
- {#each message.contextElements as element}
159
- <ContextElementBadge contextElement={element} />
160
- {/each}
161
- </div>
162
- {/if}
163
- <div
164
- class={twMerge(
165
- 'text-sm py-1 mx-2',
166
- message.role === 'user' &&
167
- 'px-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 rounded-lg relative group',
168
- (message.role === 'assistant' || message.role === 'tool') && 'px-[1px]',
169
- message.role === 'tool' && 'text-tertiary'
170
- )}
171
- >
172
- {#if message.role === 'assistant'}
173
- <AssistantMessage {message} />
174
- {:else}
175
- {message.content}
176
- {/if}
177
-
178
- {#if message.role === 'user' && isLastUserMessage(messageIndex) && !aiChatManager.loading}
179
- <div
180
- class="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
181
- >
182
- <Button
183
- size="xs2"
184
- variant="border"
185
- color="light"
186
- iconOnly
187
- title="Restart generation"
188
- startIcon={{ icon: RefreshCwIcon }}
189
- btnClasses="!p-1 !h-6 !w-6"
190
- on:click={() => restartGeneration(messageIndex)}
191
- />
192
- </div>
193
- {/if}
194
- </div>
195
- {#if message.role === 'user' && message.snapshot}
196
- <div
197
- class="mx-2 text-sm text-tertiary flex flex-row items-center justify-between gap-2 mt-2"
198
- >
199
- Saved a flow snapshot
200
- <Button
201
- size="xs2"
202
- variant="border"
203
- color="light"
204
- on:click={() => {
205
- if (message.snapshot) {
206
- aiChatManager.flowAiChatHelpers?.revertToSnapshot(message.snapshot)
207
- }
208
- }}
209
- title="Revert to snapshot"
210
- startIcon={{ icon: Undo2Icon }}
211
- >
212
- Revert
213
- </Button>
214
- </div>
215
- {/if}
216
- </div>
125
+ <AIChatMessage
126
+ {message}
127
+ {messageIndex}
128
+ {availableContext}
129
+ bind:selectedContext
130
+ bind:editingMessageIndex
131
+ />
217
132
  {/each}
218
133
  {#if aiChatManager.loading && !aiChatManager.currentReply}
219
134
  <div class="mb-6 py-1 px-2">
@@ -267,71 +182,14 @@ function restartGeneration(messageIndex) {
267
182
  </Button>
268
183
  </div>
269
184
  {/if}
270
- {#if aiChatManager.mode === 'script'}
271
- <div class="flex flex-row gap-1 mb-1 overflow-scroll pt-2 px-2 no-scrollbar">
272
- <Popover>
273
- <svelte:fragment slot="trigger">
274
- <div
275
- class="border rounded-md px-1 py-0.5 font-normal text-tertiary text-xs hover:bg-surface-hover"
276
- >@</div
277
- >
278
- </svelte:fragment>
279
- <svelte:fragment slot="content" let:close>
280
- <AvailableContextList
281
- {availableContext}
282
- {selectedContext}
283
- onSelect={(element) => {
284
- addContextToSelection(element)
285
- close()
286
- }}
287
- />
288
- </svelte:fragment>
289
- </Popover>
290
- {#each selectedContext as element}
291
- <ContextElementBadge
292
- contextElement={element}
293
- deletable
294
- on:delete={() => {
295
- selectedContext = selectedContext?.filter(
296
- (c) => c.type !== element.type || c.title !== element.title
297
- )
298
- }}
299
- />
300
- {/each}
301
- </div>
302
- <ContextTextarea
303
- bind:this={contextTextareaComponent}
304
- {availableContext}
305
- {selectedContext}
306
- isFirstMessage={messages.length === 0}
307
- onAddContext={(contextElement) => addContextToSelection(contextElement)}
308
- onSendRequest={() => {
309
- if (!aiChatManager.loading) {
310
- aiChatManager.sendRequest()
311
- }
312
- }}
313
- onUpdateInstructions={(value) => (aiChatManager.instructions = value)}
314
- {disabled}
315
- />
316
- {:else}
317
- <div class="relative w-full px-2 scroll-pb-2 pt-2">
318
- <textarea
319
- bind:this={instructionsTextarea}
320
- bind:value={aiChatManager.instructions}
321
- use:autosize
322
- onkeydown={(e) => {
323
- if (e.key === 'Enter' && !e.shiftKey && !aiChatManager.loading) {
324
- e.preventDefault()
325
- aiChatManager.sendRequest()
326
- }
327
- }}
328
- rows={3}
329
- placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
330
- class="resize-none"
331
- {disabled}
332
- ></textarea>
333
- </div>
334
- {/if}
185
+ <AIChatInput
186
+ bind:this={aiChatInput}
187
+ bind:selectedContext
188
+ {availableContext}
189
+ {disabled}
190
+ isFirstMessage={messages.length === 0}
191
+ placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
192
+ />
335
193
  <div
336
194
  class={`flex flex-row ${
337
195
  aiChatManager.mode === 'script' && hasDiff ? 'justify-between' : 'justify-end'