Merge pull request 'feat: add orchestration state progress indicator' (#28) from feature/issue-8-progress-indicator into main

This commit was merged in pull request #28.
This commit is contained in:
2026-03-12 11:33:58 +01:00
4 changed files with 82 additions and 1 deletions

View File

@@ -9,3 +9,4 @@
| #5 | Chat page layout and message list component | COMPLETED | [issue-005.md](issue-005.md) |
| #6 | Message input with send and keyboard shortcuts | COMPLETED | [issue-006.md](issue-006.md) |
| #7 | Streaming response rendering | COMPLETED | [issue-007.md](issue-007.md) |
| #8 | Orchestration state progress indicator | COMPLETED | [issue-008.md](issue-008.md) |

View File

@@ -0,0 +1,16 @@
---
---
# Issue #8: Orchestration state progress indicator
**Status:** COMPLETED
**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/8
**Branch:** `feature/issue-8-progress-indicator`
## Acceptance Criteria
- [x] Stepper/progress bar component showing orchestration phases
- [x] Phases: Decomposing → Dispatching → Executing → Compacting → Complete
- [x] Updates in real-time as OrchestrationState changes in the stream
- [x] Current phase visually highlighted, completed phases marked
- [x] Smooth transitions between states

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
let { state }: { state: OrchestrationState } = $props();
const phases = [
{ state: OrchestrationState.DECOMPOSING, label: 'Decomposing' },
{ state: OrchestrationState.DISPATCHING, label: 'Dispatching' },
{ state: OrchestrationState.EXECUTING, label: 'Executing' },
{ state: OrchestrationState.COMPACTING, label: 'Compacting' },
{ state: OrchestrationState.COMPLETE, label: 'Complete' }
];
function getStatus(phaseState: OrchestrationState): 'completed' | 'active' | 'pending' {
if (state === OrchestrationState.UNSPECIFIED) return 'pending';
if (phaseState < state) return 'completed';
if (phaseState === state) return 'active';
return 'pending';
}
</script>
<div class="mx-4 mb-2 rounded-xl bg-gray-50 px-4 py-3">
<div class="flex items-center justify-between">
{#each phases as phase, i (phase.state)}
{@const status = getStatus(phase.state)}
<div class="flex flex-col items-center gap-1">
<div
class="flex h-7 w-7 items-center justify-center rounded-full text-xs font-medium transition-all duration-300
{status === 'completed'
? 'bg-green-500 text-white'
: status === 'active'
? 'bg-blue-500 text-white ring-2 ring-blue-300 ring-offset-1'
: 'bg-gray-200 text-gray-500'}"
>
{#if status === 'completed'}
&#10003;
{:else}
{i + 1}
{/if}
</div>
<span
class="text-xs transition-colors duration-300
{status === 'active' ? 'font-medium text-blue-600' : 'text-gray-500'}"
>
{phase.label}
</span>
</div>
{#if i < phases.length - 1}
<div
class="mb-5 h-0.5 flex-1 transition-colors duration-300
{getStatus(phases[i + 1].state) !== 'pending' ? 'bg-green-500' : 'bg-gray-200'}"
></div>
{/if}
{/each}
</div>
</div>

View File

@@ -2,15 +2,19 @@
import type { ChatMessage } from '$lib/types';
import MessageList from '$lib/components/MessageList.svelte';
import MessageInput from '$lib/components/MessageInput.svelte';
import OrchestrationProgress from '$lib/components/OrchestrationProgress.svelte';
import { processRequest, OrchestratorError } from '$lib/services/orchestrator';
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
let messages: ChatMessage[] = $state([]);
let isStreaming = $state(false);
let error: string | null = $state(null);
let sessionId = $state(crypto.randomUUID());
let orchestrationState: OrchestrationState = $state(OrchestrationState.UNSPECIFIED);
async function handleSend(content: string) {
error = null;
orchestrationState = OrchestrationState.UNSPECIFIED;
const userMessage: ChatMessage = {
id: crypto.randomUUID(),
@@ -32,6 +36,7 @@
try {
for await (const response of processRequest(sessionId, content)) {
orchestrationState = response.state;
const idx = messages.length - 1;
messages[idx] = {
...messages[idx],
@@ -44,7 +49,6 @@
? `Error (${err.code}): ${err.message}`
: 'An unexpected error occurred';
error = msg;
// Update the assistant message with the error
const idx = messages.length - 1;
messages[idx] = {
...messages[idx],
@@ -63,6 +67,10 @@
<MessageList {messages} />
{#if isStreaming}
<OrchestrationProgress state={orchestrationState} />
{/if}
{#if isStreaming && messages.length > 0 && messages[messages.length - 1].content === ''}
<div class="flex justify-start px-4 pb-2">
<div class="flex items-center gap-1.5 rounded-2xl bg-gray-200 px-4 py-2.5">