Add streaming status UX and model load state indicator
Streaming status transparency:
- generation_task.py emits 'status' SSE events at each pipeline stage:
"Analyzing your request..." before intent classification, tool label
before each tool execution, "Generating/Composing response..." before
each LLM streaming round
- chat.ts adds streamingStatus ref; cleared on first chunk or done/error;
includes fast 5s poll loop after warmModel() until model shows as loaded
- ChatView.vue shows pulsing dot + italic status label above content area;
falls back to blinking cursor once content arrives
- HomeView.vue shows status label in dashboard panel instead of '...'
Model load state indicator:
- /api/chat/status now queries /api/tags and /api/ps in parallel to
distinguish installed-but-cold vs loaded-in-VRAM model states
- New model status values: 'not_found' | 'cold' | 'loaded' (was 'ready')
- chatReady true for both 'cold' and 'loaded' (cold models still work)
- AppHeader shows 5 states: gray pulse (checking), red (Ollama down),
orange (not installed), yellow pulse (cold), green (loaded)
- Inline short label ("Cold", "Ready", "Offline", etc.) visible without
hovering; detailed tooltip on hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -402,8 +402,12 @@ onUnmounted(() => {
|
||||
:tool-call="tc"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="store.streamingStatus" class="streaming-status-line">
|
||||
<span class="streaming-status-dot"></span>
|
||||
{{ store.streamingStatus }}
|
||||
</div>
|
||||
<div class="message-content prose" v-html="streamingRendered"></div>
|
||||
<span class="typing-indicator"></span>
|
||||
<span v-if="!store.streamingStatus" class="typing-indicator"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -698,6 +702,24 @@ onUnmounted(() => {
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.streaming-status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.streaming-status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
animation: blink 1s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.typing-indicator {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
|
||||
@@ -360,7 +360,11 @@ function clearDashboardResponse() {
|
||||
|
||||
<!-- Streaming text -->
|
||||
<div v-if="chatStore.streaming" class="dashboard-response-text streaming">
|
||||
<span v-if="chatStore.streamingContent">{{ chatStore.streamingContent }}</span>
|
||||
<div v-if="chatStore.streamingStatus && !chatStore.streamingContent" class="dashboard-status-line">
|
||||
<span class="dashboard-status-dot"></span>
|
||||
{{ chatStore.streamingStatus }}
|
||||
</div>
|
||||
<span v-else-if="chatStore.streamingContent">{{ chatStore.streamingContent }}</span>
|
||||
<span v-else class="thinking-dots">...</span>
|
||||
</div>
|
||||
<!-- Final text -->
|
||||
@@ -553,6 +557,21 @@ function clearDashboardResponse() {
|
||||
display: inline-block;
|
||||
animation: blink 1.2s infinite;
|
||||
}
|
||||
.dashboard-status-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-style: italic;
|
||||
}
|
||||
.dashboard-status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
animation: blink 1.2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
|
||||
Reference in New Issue
Block a user