feat: add orchestration state progress indicator
- OrchestrationProgress stepper component with 5 phases - Visual state: completed (green check), active (blue ring), pending (gray) - Smooth CSS transitions between states - Integrated into chat page, visible during streaming Closes #8 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,4 @@
|
|||||||
| #5 | Chat page layout and message list component | COMPLETED | [issue-005.md](issue-005.md) |
|
| #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) |
|
| #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) |
|
| #7 | Streaming response rendering | COMPLETED | [issue-007.md](issue-007.md) |
|
||||||
|
| #8 | Orchestration state progress indicator | COMPLETED | [issue-008.md](issue-008.md) |
|
||||||
|
|||||||
16
implementation-plans/issue-008.md
Normal file
16
implementation-plans/issue-008.md
Normal 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
|
||||||
56
src/lib/components/OrchestrationProgress.svelte
Normal file
56
src/lib/components/OrchestrationProgress.svelte
Normal 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'}
|
||||||
|
✓
|
||||||
|
{: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>
|
||||||
@@ -2,15 +2,19 @@
|
|||||||
import type { ChatMessage } from '$lib/types';
|
import type { ChatMessage } from '$lib/types';
|
||||||
import MessageList from '$lib/components/MessageList.svelte';
|
import MessageList from '$lib/components/MessageList.svelte';
|
||||||
import MessageInput from '$lib/components/MessageInput.svelte';
|
import MessageInput from '$lib/components/MessageInput.svelte';
|
||||||
|
import OrchestrationProgress from '$lib/components/OrchestrationProgress.svelte';
|
||||||
import { processRequest, OrchestratorError } from '$lib/services/orchestrator';
|
import { processRequest, OrchestratorError } from '$lib/services/orchestrator';
|
||||||
|
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
|
||||||
|
|
||||||
let messages: ChatMessage[] = $state([]);
|
let messages: ChatMessage[] = $state([]);
|
||||||
let isStreaming = $state(false);
|
let isStreaming = $state(false);
|
||||||
let error: string | null = $state(null);
|
let error: string | null = $state(null);
|
||||||
let sessionId = $state(crypto.randomUUID());
|
let sessionId = $state(crypto.randomUUID());
|
||||||
|
let orchestrationState: OrchestrationState = $state(OrchestrationState.UNSPECIFIED);
|
||||||
|
|
||||||
async function handleSend(content: string) {
|
async function handleSend(content: string) {
|
||||||
error = null;
|
error = null;
|
||||||
|
orchestrationState = OrchestrationState.UNSPECIFIED;
|
||||||
|
|
||||||
const userMessage: ChatMessage = {
|
const userMessage: ChatMessage = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const response of processRequest(sessionId, content)) {
|
for await (const response of processRequest(sessionId, content)) {
|
||||||
|
orchestrationState = response.state;
|
||||||
const idx = messages.length - 1;
|
const idx = messages.length - 1;
|
||||||
messages[idx] = {
|
messages[idx] = {
|
||||||
...messages[idx],
|
...messages[idx],
|
||||||
@@ -44,7 +49,6 @@
|
|||||||
? `Error (${err.code}): ${err.message}`
|
? `Error (${err.code}): ${err.message}`
|
||||||
: 'An unexpected error occurred';
|
: 'An unexpected error occurred';
|
||||||
error = msg;
|
error = msg;
|
||||||
// Update the assistant message with the error
|
|
||||||
const idx = messages.length - 1;
|
const idx = messages.length - 1;
|
||||||
messages[idx] = {
|
messages[idx] = {
|
||||||
...messages[idx],
|
...messages[idx],
|
||||||
@@ -63,6 +67,10 @@
|
|||||||
|
|
||||||
<MessageList {messages} />
|
<MessageList {messages} />
|
||||||
|
|
||||||
|
{#if isStreaming}
|
||||||
|
<OrchestrationProgress state={orchestrationState} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isStreaming && messages.length > 0 && messages[messages.length - 1].content === ''}
|
{#if isStreaming && messages.length > 0 && messages[messages.length - 1].content === ''}
|
||||||
<div class="flex justify-start px-4 pb-2">
|
<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">
|
<div class="flex items-center gap-1.5 rounded-2xl bg-gray-200 px-4 py-2.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user