The pending operator-question card now renders inline in ChatThread (the newest thing in the conversation) instead of in the context rail — it's part of the chat, not a separate side panel, and the panel's hasContext gate no longer needs to special-case it. The desktop mascot's reactions are now entirely about whichever task window has focus, not fleet-wide events: thinking/talking is a new continuous `busy` behavior that tracks the focused session's own streaming state (thinking before any text arrives, talking once it does — using the previously-unwired peep/talk sprite), eureka fires with the actual knowledge title that was recorded, happy fires with the task's own completion summary, and alarmed now means "this task needs your OK" (an operator question was raised) rather than a fleet-wide critical/signal event. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
78 lines
2.6 KiB
Svelte
78 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import { answerQuestion as postAnswer, type SessionQuestion } from '$lib/api'
|
|
import { Button } from '$lib/components/ui/button'
|
|
import { Textarea } from '$lib/components/ui/textarea'
|
|
import CircleHelpIcon from '@lucide/svelte/icons/circle-help'
|
|
|
|
// Prop-driven (not store-imported) — see SessionGraph.svelte for why.
|
|
let { sessionId, question }: { sessionId: string | null; question: SessionQuestion | null } = $props()
|
|
|
|
let freeText = $state('')
|
|
let submitting = $state(false)
|
|
|
|
async function submit(answer: string) {
|
|
const sid = sessionId
|
|
const q = question
|
|
if (!sid || !q || !answer.trim() || submitting) return
|
|
submitting = true
|
|
const ok = await postAnswer(sid, q.id, answer.trim())
|
|
submitting = false
|
|
if (ok) freeText = ''
|
|
// No local optimistic clear: the question.answered event (which the POST
|
|
// triggers server-side) updates the store — this stays truthful if the
|
|
// POST reports ok but the event is somehow delayed.
|
|
}
|
|
</script>
|
|
|
|
{#if question}
|
|
{@const q = question}
|
|
<div class="flex flex-col gap-2 rounded-2xl border border-warning/30 bg-warning/5 px-3.5 py-3">
|
|
<div class="flex items-start gap-2">
|
|
<CircleHelpIcon class="mt-0.5 size-4 shrink-0 text-warning" />
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium leading-snug">{q.prompt}</p>
|
|
{#if q.context.why}
|
|
<p class="mt-0.5 text-xs text-muted-foreground">{q.context.why}</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if q.context.entities?.length}
|
|
<div class="ml-6 flex flex-wrap gap-1">
|
|
{#each q.context.entities as slug}
|
|
<span class="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px]">{slug}</span>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if q.context.options?.length}
|
|
<div class="ml-6 flex flex-wrap gap-1.5">
|
|
{#each q.context.options as opt}
|
|
<Button size="sm" variant="outline" class="h-7 px-2.5 text-xs" disabled={submitting} onclick={() => submit(opt)}>
|
|
{opt}
|
|
</Button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="ml-6 flex items-end gap-1.5">
|
|
<Textarea
|
|
bind:value={freeText}
|
|
placeholder="Or type an answer…"
|
|
rows={1}
|
|
class="max-h-24 min-h-0 resize-none text-xs"
|
|
disabled={submitting}
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
submit(freeText)
|
|
}
|
|
}}
|
|
/>
|
|
<Button size="sm" class="h-7 px-2.5 text-xs" disabled={!freeText.trim() || submitting} onclick={() => submit(freeText)}>
|
|
Send
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{/if}
|