Remove the Brain panel from the Brain and AI page

The page is back to the chat and the plan side by side. With nothing to
tick, new schedules always use the whole Brain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-29 11:50:39 +07:00
parent adbcfcc7ba
commit 643c3c39f9

View File

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Icon, ruleIcon } from '../components/Icon'
import { PageHead } from '../components/Page'
import { BrainPanel, choosable, useDocuments } from './Brain'
import { choosable, useDocuments } from './Brain'
import {
dayName, describeRule, kindLabel, KINDS, LANGUAGES, languageLabel, monthNote, videoCadence, weeklyTarget,
type ContentKind, type ContentRule, type OutputLanguage, type ResearchRule, type Rule,
@@ -36,14 +36,7 @@ export function InstructionPage() {
const [agent, setAgent] = useState<AgentState>(EMPTY)
const [messages, setMessages] = useState<Message[]>([])
const [saving, setSaving] = useState(false)
// Brain material the schedules are made from; none chosen means the whole Brain.
const [selected, setSelected] = useState<Set<number>>(new Set())
const docs = useDocuments()
// A chosen document that was deleted, or failed since, no longer counts.
const chosenDocs = choosable(docs.data).filter(d => selected.has(d.docId))
const chosen = chosenDocs.map(d => d.docId)
// How the chat and the plan name the chosen material: by title when there is one.
const sources = chosenDocs.length === 1 ? `“${chosenDocs[0].title}”` : chosenDocs.length ? t('brainPanel.nChosen', { n: chosenDocs.length }) : null
const [phase, setPhase] = useState<Phase>('idle')
// The live question whose typing has finished, so its reply options can show.
const [typedLive, setTypedLive] = useState('')
@@ -55,19 +48,16 @@ export function InstructionPage() {
const later = (fn: () => void, ms: number) => { timers.current.push(window.setTimeout(fn, ms)) }
useEffect(() => () => timers.current.forEach(clearTimeout), [])
// The agent sees the current choice, so ticking material in the Brain panel updates its question and plan.
const view: AgentState = { ...agent, sources }
const question = nextQuestion(view)
const question = nextQuestion(agent)
const ready = agent.items.filter(i => canSchedule(i.rule))
// Without a choice the whole Brain is attached, so every ready document counts.
const sourceCount = chosen.length || choosable(docs.data).length
const tooFewSources = sourceCount < MIN_PRODUCTION_SOURCES && ready.some(i => needsTwoSources(i.rule))
// The whole Brain is attached, so every ready document counts.
const tooFewSources = choosable(docs.data).length < MIN_PRODUCTION_SOURCES && ready.some(i => needsTwoSources(i.rule))
const hasContent = agent.items.some(i => i.rule.type === 'content')
// The agent's current turn: its question, or what it is waiting for. It stays live, so an edit in the plan updates it.
const live = inLang(lang, () => saving
? { text: t('agent.approving'), options: [] as string[] }
: question
? ask(question, view)
? ask(question, agent)
: agent.items.length
? {
text: hasContent && agent.language
@@ -89,7 +79,7 @@ export function InstructionPage() {
// A picked option is in the conversation's language already; typed text may switch it.
const replyLang = live.options.includes(input) ? lang : chatLangOf(input) ?? lang
setChatLang(replyLang)
const turn = inLang(replyLang, () => respond(view, input))
const turn = inLang(replyLang, () => respond(agent, input))
// The question being answered is kept in the history, with the reply after it.
setMessages(ms => [...ms, ...(live.text ? [{ role: 'agent' as const, text: live.text }] : []), { role: 'user', text: input.trim() }])
setText('')
@@ -120,8 +110,8 @@ export function InstructionPage() {
// One at a time so a failure (e.g. not an organization owner) is reported per schedule.
for (const { id, rule } of items) {
try {
await createSchedule(rule.type === 'content' ? { ...rule, language } : rule, chosen)
done.push({ rule: rule.type === 'content' && chosen.length ? { ...rule, chosen: true } : rule, ok: true })
await createSchedule(rule.type === 'content' ? { ...rule, language } : rule)
done.push({ rule, ok: true })
created.add(id)
} catch (e) {
done.push({ rule, ok: false, message: (e as Error).message })
@@ -142,76 +132,73 @@ export function InstructionPage() {
<>
<PageHead title={t('ins.title')} description={t('ins.description')} />
<div className="unified-workspace">
<BrainPanel selected={selected} onSelect={setSelected} />
<div className="ai-column">
<div className="card agent-chat">
<div className="card-head">
<div className="assistant-title">
<span><Icon name="spark" /></span>
<div><h3>{t('agent.name')}</h3><small>{phase === 'thinking' ? t('agent.statusThinking') : t('agent.status')}</small></div>
</div>
<div className="ai-workspace">
<div className="card agent-chat">
<div className="card-head">
<div className="assistant-title">
<span><Icon name="spark" /></span>
<div><h3>{t('agent.name')}</h3><small>{phase === 'thinking' ? t('agent.statusThinking') : t('agent.status')}</small></div>
</div>
<div className="ai-messages" aria-live="polite" ref={chat}>
{messages.map((m, i) => (
<Bubble key={i} message={m} lang={lang} onTyped={i === messages.length - 1 && phase === 'replying' ? replied : undefined} onType={scroll} />
))}
{phase === 'thinking' && <Thinking lang={lang} />}
{phase === 'pausing' && <Typing />}
{idle && live.text && (
<Bubble key={live.text} message={{ role: 'agent', text: live.text, typed: true }} lang={lang} busy={saving}
onTyped={() => setTypedLive(live.text)} onType={scroll} />
)}
</div>
{showOptions && (
<div className="prompt-chips appear">
{live.options.map((o, i) => (
<button key={o} className={i === 0 && !question && agent.items.length ? 'primary' : undefined} onClick={() => send(o)}>{o}</button>
))}
</div>
)}
<form id="ai-request-form" onSubmit={e => { e.preventDefault(); send(text) }}>
<label htmlFor="instruction" className="sr-only">{t('ins.label')}</label>
<textarea id="instruction" rows={2} value={text} placeholder={t('agent.placeholder')} disabled={saving}
onChange={e => setText(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(text) } }} />
<div className="ai-compose-footer">
<small>{t('agent.hint')}</small>
<button className="btn primary" type="submit" disabled={!text.trim() || saving || !idle}><Icon name="arrow-right" />{t('agent.send')}</button>
</div>
</form>
</div>
<div className="ai-messages" aria-live="polite" ref={chat}>
{messages.map((m, i) => (
<Bubble key={i} message={m} lang={lang} onTyped={i === messages.length - 1 && phase === 'replying' ? replied : undefined} onType={scroll} />
))}
{phase === 'thinking' && <Thinking lang={lang} />}
{phase === 'pausing' && <Typing />}
{idle && live.text && (
<Bubble key={live.text} message={{ role: 'agent', text: live.text, typed: true }} lang={lang} busy={saving}
onTyped={() => setTypedLive(live.text)} onType={scroll} />
)}
</div>
{showOptions && (
<div className="prompt-chips appear">
{live.options.map((o, i) => (
<button key={o} className={i === 0 && !question && agent.items.length ? 'primary' : undefined} onClick={() => send(o)}>{o}</button>
))}
</div>
)}
<form id="ai-request-form" onSubmit={e => { e.preventDefault(); send(text) }}>
<label htmlFor="instruction" className="sr-only">{t('ins.label')}</label>
<textarea id="instruction" rows={2} value={text} placeholder={t('agent.placeholder')} disabled={saving}
onChange={e => setText(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(text) } }} />
<div className="ai-compose-footer">
<small>{t('agent.hint')}</small>
<button className="btn primary" type="submit" disabled={!text.trim() || saving || !idle}><Icon name="arrow-right" />{t('agent.send')}</button>
</div>
</form>
</div>
<div className="card">
<div className="card-head"><h3>{t('ins.previewTitle')}</h3><span className="badge neutral">{t('ins.readyCount', { n: ready.length })}</span></div>
{agent.items.length === 0 && <div className="empty"><p>{t('agent.planEmpty')}</p></div>}
{hasContent && (
<label className="plan-language">
<span>{t('ins.language')}</span>
<select value={agent.language ?? ''} onChange={e => setAgent(s => ({ ...s, language: e.target.value as OutputLanguage }))}>
{!agent.language && <option value="" disabled>{t('ins.languagePending')}</option>}
{LANGUAGES.map(l => <option key={l} value={l}>{languageLabel(l)}</option>)}
</select>
</label>
)}
<div className="rule-list">
{agent.items.map(({ id, rule }) => (
<RuleCard key={id} rule={rule} from={sources ?? undefined} onChange={p => setAgent(s => editRule(s, id, p))} onRemove={() => setAgent(s => removeRule(s, id))} />
))}
</div>
{ready.length > 0 && (
<div className="rule-approve">
<small>
<b className="rule-sources"><Icon name="brain" />{chosen.length ? t('ins.sourcesChosen', { n: chosen.length }) : t('ins.sourcesAll')}</b>
{t('ins.approveNote')}
{tooFewSources && <span className="error">{t(chosen.length ? 'ins.tooFewChosen' : 'ins.tooFewInBrain', { n: MIN_PRODUCTION_SOURCES })}</span>}
</small>
<button className="btn primary" disabled={saving || !idle || tooFewSources} onClick={() => approve()}>
<Icon name="check" />{saving ? t('ins.approving') : t('ins.approve', { n: ready.length })}
</button>
</div>
)}
<div className="card">
<div className="card-head"><h3>{t('ins.previewTitle')}</h3><span className="badge neutral">{t('ins.readyCount', { n: ready.length })}</span></div>
{agent.items.length === 0 && <div className="empty"><p>{t('agent.planEmpty')}</p></div>}
{hasContent && (
<label className="plan-language">
<span>{t('ins.language')}</span>
<select value={agent.language ?? ''} onChange={e => setAgent(s => ({ ...s, language: e.target.value as OutputLanguage }))}>
{!agent.language && <option value="" disabled>{t('ins.languagePending')}</option>}
{LANGUAGES.map(l => <option key={l} value={l}>{languageLabel(l)}</option>)}
</select>
</label>
)}
<div className="rule-list">
{agent.items.map(({ id, rule }) => (
<RuleCard key={id} rule={rule} onChange={p => setAgent(s => editRule(s, id, p))} onRemove={() => setAgent(s => removeRule(s, id))} />
))}
</div>
{ready.length > 0 && (
<div className="rule-approve">
<small>
<b className="rule-sources"><Icon name="brain" />{t('ins.sourcesAll')}</b>
{t('ins.approveNote')}
{tooFewSources && <span className="error">{t('ins.tooFewInBrain', { n: MIN_PRODUCTION_SOURCES })}</span>}
</small>
<button className="btn primary" disabled={saving || !idle || tooFewSources} onClick={() => approve()}>
<Icon name="check" />{saving ? t('ins.approving') : t('ins.approve', { n: ready.length })}
</button>
</div>
)}
</div>
</div>
</>