2026-01-03 11:15:32 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { setElementIcon } from "Helpers/ElementHelper";
|
2026-02-19 20:48:59 +00:00
|
|
|
import { Copy } from "Enums/Copy";
|
2026-01-03 11:15:32 +00:00
|
|
|
import Spinner from "./Spinner.svelte";
|
2026-01-29 13:32:23 +00:00
|
|
|
import { tick, onDestroy } from "svelte";
|
2026-01-03 11:15:32 +00:00
|
|
|
import { fade, slide } from "svelte/transition";
|
|
|
|
|
import type { IExecutionPlanState } from "Stores/ExecutionPlanStore";
|
|
|
|
|
import type { Writable } from "svelte/store";
|
|
|
|
|
|
|
|
|
|
export let executionPlanState: Writable<IExecutionPlanState>;
|
|
|
|
|
export let busyPlanning = false;
|
|
|
|
|
|
|
|
|
|
let expanded = false;
|
|
|
|
|
let expandedHeight = 0;
|
|
|
|
|
let collapsedHeight = 0;
|
|
|
|
|
|
|
|
|
|
let wrapperDiv: HTMLDivElement;
|
|
|
|
|
let contentDiv: HTMLDivElement;
|
|
|
|
|
let stepElements: (HTMLDivElement | null)[] = [];
|
|
|
|
|
let isTransitioning = false;
|
2026-01-29 13:32:23 +00:00
|
|
|
let resizeObserver: ResizeObserver | null = null;
|
2026-07-11 13:46:39 +00:00
|
|
|
let isScrolledToTop = true;
|
|
|
|
|
let isScrolledToBottom = true;
|
2026-01-03 11:15:32 +00:00
|
|
|
|
|
|
|
|
$: steps = $executionPlanState.plan?.executionSteps;
|
2026-01-27 20:29:20 +00:00
|
|
|
$: activeStepIndex = $executionPlanState.currentStepIndex;
|
2026-01-03 11:15:32 +00:00
|
|
|
|
|
|
|
|
$: if (steps) {
|
2026-01-30 22:04:00 +00:00
|
|
|
tick().then(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
updateHeight();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:32:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: if (contentDiv) {
|
|
|
|
|
setupResizeObserver();
|
2026-01-03 11:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: if (activeStepIndex >= 0 && !isTransitioning) {
|
|
|
|
|
scrollToActiveStep();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 13:32:23 +00:00
|
|
|
function setupResizeObserver() {
|
|
|
|
|
if (resizeObserver) {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
updateHeight();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
resizeObserver.observe(contentDiv);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 11:15:32 +00:00
|
|
|
async function updateHeight() {
|
2026-01-07 20:40:16 +00:00
|
|
|
const firstStep = stepElements[0];
|
2026-01-30 22:04:00 +00:00
|
|
|
if (!contentDiv || !firstStep || !steps) {
|
2026-01-07 20:00:40 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-01-03 11:15:32 +00:00
|
|
|
|
2026-01-07 20:00:40 +00:00
|
|
|
expandedHeight = contentDiv.scrollHeight;
|
2026-01-03 11:15:32 +00:00
|
|
|
|
2026-01-07 20:40:16 +00:00
|
|
|
// Calculate heights fresh each time to ensure accurate measurements
|
|
|
|
|
const stepHeight = firstStep.offsetHeight;
|
|
|
|
|
const separatorHeight = firstStep.nextElementSibling?.clientHeight ?? 0;
|
|
|
|
|
|
2026-01-30 22:04:00 +00:00
|
|
|
const stepsToShow = Math.min(3, steps.length);
|
2026-01-07 20:40:16 +00:00
|
|
|
collapsedHeight = (stepsToShow * stepHeight) + (Math.max(0, stepsToShow - 1) * separatorHeight);
|
2026-07-11 13:46:39 +00:00
|
|
|
|
|
|
|
|
updateScrollFade();
|
2026-01-03 11:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-29 13:32:23 +00:00
|
|
|
onDestroy(() => {
|
|
|
|
|
if (resizeObserver) {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-11 13:46:39 +00:00
|
|
|
function updateScrollFade() {
|
|
|
|
|
if (!wrapperDiv) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const { scrollTop, scrollHeight, clientHeight } = wrapperDiv;
|
|
|
|
|
isScrolledToTop = scrollTop < 1;
|
|
|
|
|
isScrolledToBottom = scrollHeight - scrollTop - clientHeight < 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 11:15:32 +00:00
|
|
|
async function scrollToActiveStep() {
|
2026-01-29 13:32:23 +00:00
|
|
|
if (!wrapperDiv || !$executionPlanState.plan || activeStepIndex === -1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-03 11:15:32 +00:00
|
|
|
|
|
|
|
|
await tick();
|
|
|
|
|
const stepElement = stepElements[activeStepIndex];
|
|
|
|
|
if (!stepElement) return;
|
|
|
|
|
|
2026-02-16 21:56:31 +00:00
|
|
|
// Calculate position relative to the scroll container (wrapperDiv)
|
|
|
|
|
const stepTop = stepElement.offsetTop - contentDiv.offsetTop;
|
2026-01-03 11:15:32 +00:00
|
|
|
const stepHeight = stepElement.offsetHeight;
|
|
|
|
|
const wrapperHeight = wrapperDiv.clientHeight;
|
|
|
|
|
const scrollTo = stepTop - (wrapperHeight / 2) + (stepHeight / 2);
|
|
|
|
|
|
|
|
|
|
wrapperDiv.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleExpand() {
|
|
|
|
|
expanded = !expanded;
|
2026-01-30 22:04:00 +00:00
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
updateHeight();
|
|
|
|
|
});
|
2026-07-06 15:11:30 +00:00
|
|
|
|
|
|
|
|
isTransitioning = true;
|
|
|
|
|
const finishTransition = () => {
|
|
|
|
|
wrapperDiv.removeEventListener('transitionend', finishTransition);
|
|
|
|
|
clearTimeout(fallbackTimeoutId);
|
|
|
|
|
isTransitioning = false;
|
|
|
|
|
scrollToActiveStep();
|
|
|
|
|
};
|
|
|
|
|
// Fallback in case height doesn't actually change (e.g. <=3 steps), so transitionend never fires
|
|
|
|
|
const fallbackTimeoutId = setTimeout(finishTransition, 250);
|
|
|
|
|
wrapperDiv.addEventListener('transitionend', finishTransition, { once: true });
|
2026-01-03 11:15:32 +00:00
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
{#if busyPlanning}
|
|
|
|
|
<div id="chat-planning-in-progress" transition:slide>
|
2026-04-23 14:57:27 +00:00
|
|
|
<Spinner background=var(--background-secondary-alt)/>
|
2026-02-19 20:48:59 +00:00
|
|
|
<span id="chat-planning-in-progress-text">{Copy.PlanningInProgress}</span>
|
2026-01-03 11:15:32 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
2026-01-07 20:00:40 +00:00
|
|
|
{#if steps && steps?.length > 0}
|
2026-01-03 11:15:32 +00:00
|
|
|
<div id="chat-plan-area"
|
|
|
|
|
out:slide
|
|
|
|
|
on:click={toggleExpand}
|
|
|
|
|
on:keydown={(e) => e.key === 'Enter' && toggleExpand()}
|
|
|
|
|
aria-label={expanded ? "Collapse planned steps" : "Expand planned steps"}
|
|
|
|
|
role="button"
|
|
|
|
|
tabindex=0>
|
2026-07-11 13:46:39 +00:00
|
|
|
<div id="chat-plan-steps-wrapper" style:height="{expanded ? expandedHeight : collapsedHeight}px" bind:this={wrapperDiv} on:scroll={updateScrollFade}>
|
2026-01-03 11:15:32 +00:00
|
|
|
<div id="chat-plan-steps" bind:this={contentDiv}>
|
2026-01-07 20:00:40 +00:00
|
|
|
{#each steps as step, index }
|
2026-01-03 11:15:32 +00:00
|
|
|
<div class="chat-plan-step" bind:this={stepElements[index]}>
|
2026-01-27 20:29:20 +00:00
|
|
|
{#if index < activeStepIndex}
|
2026-01-03 11:15:32 +00:00
|
|
|
<div class="chat-plan-step-icon" use:setElementIcon={"circle-check"} style:color="var(--color-green)"></div>
|
|
|
|
|
{/if}
|
2026-01-27 20:29:20 +00:00
|
|
|
{#if index === activeStepIndex}
|
2026-01-03 11:15:32 +00:00
|
|
|
<div class="chat-plan-step-icon">
|
2026-04-23 14:57:27 +00:00
|
|
|
<Spinner background=var(--background-secondary-alt)/>
|
2026-01-03 11:15:32 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
2026-01-27 20:29:20 +00:00
|
|
|
{#if index > activeStepIndex}
|
2026-01-03 11:15:32 +00:00
|
|
|
<div class="chat-plan-step-icon" use:setElementIcon={"circle"} style:opacity={0.5}></div>
|
|
|
|
|
{/if}
|
|
|
|
|
<span class="chat-plan-step-span">
|
2026-01-27 20:29:20 +00:00
|
|
|
{`${index + 1}. ${step.description}`}
|
2026-01-03 11:15:32 +00:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-01-07 20:00:40 +00:00
|
|
|
{#if index < steps.length - 1}
|
2026-01-03 11:15:32 +00:00
|
|
|
<div class="chat-plan-step-icon"
|
|
|
|
|
use:setElementIcon={"ellipsis-vertical"}
|
2026-01-27 20:29:20 +00:00
|
|
|
style:opacity={index < activeStepIndex ? 1 : 0.25}>
|
2026-01-03 11:15:32 +00:00
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-07 20:00:40 +00:00
|
|
|
{#if steps.length > 2}
|
2026-07-11 13:46:39 +00:00
|
|
|
<div class="chat-plan-fade top-fade" class:hidden={isScrolledToTop}></div>
|
|
|
|
|
<div class="chat-plan-fade bottom-fade" class:hidden={isScrolledToBottom}></div>
|
2026-01-03 11:15:32 +00:00
|
|
|
{/if}
|
|
|
|
|
<div id="chat-plan-chevron"
|
|
|
|
|
class="transparent-button"
|
|
|
|
|
use:setElementIcon={expanded ? "chevrons-down-up" : "chevrons-up-down"}>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
#chat-planning-in-progress {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--size-4-2);
|
|
|
|
|
background-color: var(--background-secondary-alt);
|
|
|
|
|
border-radius: var(--radius-m);
|
|
|
|
|
padding: var(--size-4-2);
|
|
|
|
|
font-style: italic;
|
|
|
|
|
font-size: var(--font-smaller);
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-planning-in-progress-text {
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-plan-area {
|
|
|
|
|
grid-row: 1;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-rows: auto;
|
|
|
|
|
grid-template-columns: 1fr auto;
|
|
|
|
|
background-color: var(--background-secondary-alt);
|
|
|
|
|
border-radius: var(--radius-m);
|
|
|
|
|
padding: var(--size-4-1) var(--size-4-2);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-plan-steps-wrapper {
|
|
|
|
|
grid-row: auto;
|
|
|
|
|
grid-column: 1;
|
|
|
|
|
transition: height 0.2s ease-out;
|
|
|
|
|
overflow-x: hidden;
|
2026-01-04 23:40:31 +00:00
|
|
|
overflow-y: auto;
|
2026-01-03 11:15:32 +00:00
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
max-height: 20vh;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-plan-steps-wrapper::-webkit-scrollbar {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-plan-steps {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
font-size: var(--font-smallest);
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-plan-step {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-plan-step-span {
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-plan-step-icon {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding-right: var(--size-4-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#chat-plan-chevron {
|
|
|
|
|
grid-row: auto;
|
|
|
|
|
grid-column: 2;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-plan-fade {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
2026-01-04 18:52:44 +00:00
|
|
|
height: var(--size-4-1);
|
2026-01-03 11:15:32 +00:00
|
|
|
border-radius: var(--radius-m);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 1;
|
2026-07-11 13:46:39 +00:00
|
|
|
opacity: 1;
|
|
|
|
|
transition: opacity 0.2s ease-out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-plan-fade.hidden {
|
|
|
|
|
opacity: 0;
|
2026-01-03 11:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.top-fade {
|
|
|
|
|
top: var(--size-4-1);
|
|
|
|
|
background-image: linear-gradient(to bottom, var(--background-secondary-alt), transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bottom-fade {
|
|
|
|
|
bottom: var(--size-4-1);
|
|
|
|
|
background-image: linear-gradient(to top, var(--background-secondary-alt), transparent);
|
|
|
|
|
}
|
|
|
|
|
</style>
|