Attach Brain material without blocking a schedule's status

Saving a topic or resuming sent one PUT per ready document (about 150),
and a topic change left the schedule in draft (shown as paused) until
all of them were done. Creating a schedule waited on the same loop with
no sign of progress.

A topic change now re-activates (or keeps paused) right after the PATCH,
and resuming is one activate; both then attach only the documents the
schedule does not have yet. Creating a schedule still attaches every
ready document, three at a time and paced under the proxy's rate limit,
retrying a busy answer instead of skipping it, and the Instruction chat
shows how many are done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-30 13:44:26 +07:00
parent 7a51f35ec4
commit 74d7e01e4f
5 changed files with 155 additions and 25 deletions

View File

@@ -36,6 +36,8 @@ export function InstructionPage() {
const [agent, setAgent] = useState<AgentState>(EMPTY)
const [messages, setMessages] = useState<Message[]>([])
const [saving, setSaving] = useState(false)
// While schedules are created: which one, and how many Brain documents it has been given so far.
const [progress, setProgress] = useState<{ n: number; count: number; done: number; total: number } | null>(null)
const docs = useDocuments()
const [phase, setPhase] = useState<Phase>('idle')
// The live question whose typing has finished, so its reply options can show.
@@ -108,9 +110,10 @@ export function InstructionPage() {
// Approving from the plan before the agent asked about the language uses the conversation's language.
const language: OutputLanguage = state.language ?? (replyLang === 'id' ? 'Bahasa Indonesia' : 'English')
// One at a time so a failure (e.g. not an organization owner) is reported per schedule.
for (const { id, rule } of items) {
for (const [i, { id, rule }] of items.entries()) {
try {
await createSchedule(rule.type === 'content' ? { ...rule, language } : rule)
await createSchedule(rule.type === 'content' ? { ...rule, language } : rule, undefined,
(d, total) => setProgress({ n: i + 1, count: items.length, done: d, total }))
done.push({ rule, ok: true })
created.add(id)
} catch (e) {
@@ -118,6 +121,7 @@ export function InstructionPage() {
}
}
setSaving(false)
setProgress(null)
// A new schedule may start producing right away; the dashboard shows it under content in progress.
await Promise.all([queryClient.invalidateQueries({ queryKey: ['schedules'] }), queryClient.invalidateQueries({ queryKey: ['results'] })])
const ok = done.filter(o => o.ok).length
@@ -148,6 +152,7 @@ export function InstructionPage() {
{phase === 'pausing' && <Typing />}
{idle && live.text && (
<Bubble key={live.text} message={{ role: 'agent', text: live.text, typed: true }} lang={lang} busy={saving}
note={saving && progress?.total ? inLang(lang, () => t('agent.attaching', progress)) : undefined}
onTyped={() => setTypedLive(live.text)} onType={scroll} />
)}
</div>
@@ -221,8 +226,8 @@ function useTyped(text: string, enabled: boolean, onDone?: () => void, onType?:
return text.slice(0, shown)
}
function Bubble({ message, lang, busy, onTyped, onType }: {
message: Message; lang: Lang; busy?: boolean; onTyped?: () => void; onType?: () => void
function Bubble({ message, lang, busy, note, onTyped, onType }: {
message: Message; lang: Lang; busy?: boolean; note?: string; onTyped?: () => void; onType?: () => void
}) {
const text = useTyped(message.text, !!message.typed, onTyped, onType)
const typing = text.length < message.text.length
@@ -232,6 +237,7 @@ function Bubble({ message, lang, busy, onTyped, onType }: {
<div className="ai-bubble">
{message.thought && <small className="ai-thought">{inLang(lang, () => t('agent.thought', { s: message.thought! }))}</small>}
<p className={typing ? 'typing' : undefined}>{text}</p>
{note && <small className="ai-thought" role="status">{note}</small>}
{message.outcomes && !typing && (
<ul className="rule-outcomes appear">
{message.outcomes.map((o, i) => (