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:
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Icon, ruleIcon } from '../components/Icon'
|
import { Icon, ruleIcon } from '../components/Icon'
|
||||||
import { PageHead } from '../components/Page'
|
import { PageHead } from '../components/Page'
|
||||||
import { BrainPanel, choosable, useDocuments } from './Brain'
|
import { choosable, useDocuments } from './Brain'
|
||||||
import {
|
import {
|
||||||
dayName, describeRule, kindLabel, KINDS, LANGUAGES, languageLabel, monthNote, videoCadence, weeklyTarget,
|
dayName, describeRule, kindLabel, KINDS, LANGUAGES, languageLabel, monthNote, videoCadence, weeklyTarget,
|
||||||
type ContentKind, type ContentRule, type OutputLanguage, type ResearchRule, type Rule,
|
type ContentKind, type ContentRule, type OutputLanguage, type ResearchRule, type Rule,
|
||||||
@@ -36,14 +36,7 @@ export function InstructionPage() {
|
|||||||
const [agent, setAgent] = useState<AgentState>(EMPTY)
|
const [agent, setAgent] = useState<AgentState>(EMPTY)
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
const [saving, setSaving] = useState(false)
|
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()
|
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')
|
const [phase, setPhase] = useState<Phase>('idle')
|
||||||
// The live question whose typing has finished, so its reply options can show.
|
// The live question whose typing has finished, so its reply options can show.
|
||||||
const [typedLive, setTypedLive] = useState('')
|
const [typedLive, setTypedLive] = useState('')
|
||||||
@@ -55,19 +48,16 @@ export function InstructionPage() {
|
|||||||
const later = (fn: () => void, ms: number) => { timers.current.push(window.setTimeout(fn, ms)) }
|
const later = (fn: () => void, ms: number) => { timers.current.push(window.setTimeout(fn, ms)) }
|
||||||
useEffect(() => () => timers.current.forEach(clearTimeout), [])
|
useEffect(() => () => timers.current.forEach(clearTimeout), [])
|
||||||
|
|
||||||
// The agent sees the current choice, so ticking material in the Brain panel updates its question and plan.
|
const question = nextQuestion(agent)
|
||||||
const view: AgentState = { ...agent, sources }
|
|
||||||
const question = nextQuestion(view)
|
|
||||||
const ready = agent.items.filter(i => canSchedule(i.rule))
|
const ready = agent.items.filter(i => canSchedule(i.rule))
|
||||||
// Without a choice the whole Brain is attached, so every ready document counts.
|
// The whole Brain is attached, so every ready document counts.
|
||||||
const sourceCount = chosen.length || choosable(docs.data).length
|
const tooFewSources = choosable(docs.data).length < MIN_PRODUCTION_SOURCES && ready.some(i => needsTwoSources(i.rule))
|
||||||
const tooFewSources = sourceCount < MIN_PRODUCTION_SOURCES && ready.some(i => needsTwoSources(i.rule))
|
|
||||||
const hasContent = agent.items.some(i => i.rule.type === 'content')
|
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.
|
// 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
|
const live = inLang(lang, () => saving
|
||||||
? { text: t('agent.approving'), options: [] as string[] }
|
? { text: t('agent.approving'), options: [] as string[] }
|
||||||
: question
|
: question
|
||||||
? ask(question, view)
|
? ask(question, agent)
|
||||||
: agent.items.length
|
: agent.items.length
|
||||||
? {
|
? {
|
||||||
text: hasContent && agent.language
|
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.
|
// 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
|
const replyLang = live.options.includes(input) ? lang : chatLangOf(input) ?? lang
|
||||||
setChatLang(replyLang)
|
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.
|
// 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() }])
|
setMessages(ms => [...ms, ...(live.text ? [{ role: 'agent' as const, text: live.text }] : []), { role: 'user', text: input.trim() }])
|
||||||
setText('')
|
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.
|
// 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 { id, rule } of items) {
|
||||||
try {
|
try {
|
||||||
await createSchedule(rule.type === 'content' ? { ...rule, language } : rule, chosen)
|
await createSchedule(rule.type === 'content' ? { ...rule, language } : rule)
|
||||||
done.push({ rule: rule.type === 'content' && chosen.length ? { ...rule, chosen: true } : rule, ok: true })
|
done.push({ rule, ok: true })
|
||||||
created.add(id)
|
created.add(id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
done.push({ rule, ok: false, message: (e as Error).message })
|
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')} />
|
<PageHead title={t('ins.title')} description={t('ins.description')} />
|
||||||
|
|
||||||
<div className="unified-workspace">
|
<div className="ai-workspace">
|
||||||
<BrainPanel selected={selected} onSelect={setSelected} />
|
<div className="card agent-chat">
|
||||||
<div className="ai-column">
|
<div className="card-head">
|
||||||
<div className="card agent-chat">
|
<div className="assistant-title">
|
||||||
<div className="card-head">
|
<span><Icon name="spark" /></span>
|
||||||
<div className="assistant-title">
|
<div><h3>{t('agent.name')}</h3><small>{phase === 'thinking' ? t('agent.statusThinking') : t('agent.status')}</small></div>
|
||||||
<span><Icon name="spark" /></span>
|
|
||||||
<div><h3>{t('agent.name')}</h3><small>{phase === 'thinking' ? t('agent.statusThinking') : t('agent.status')}</small></div>
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
<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">
|
||||||
<div className="card-head"><h3>{t('ins.previewTitle')}</h3><span className="badge neutral">{t('ins.readyCount', { n: ready.length })}</span></div>
|
<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>}
|
{agent.items.length === 0 && <div className="empty"><p>{t('agent.planEmpty')}</p></div>}
|
||||||
{hasContent && (
|
{hasContent && (
|
||||||
<label className="plan-language">
|
<label className="plan-language">
|
||||||
<span>{t('ins.language')}</span>
|
<span>{t('ins.language')}</span>
|
||||||
<select value={agent.language ?? ''} onChange={e => setAgent(s => ({ ...s, language: e.target.value as OutputLanguage }))}>
|
<select value={agent.language ?? ''} onChange={e => setAgent(s => ({ ...s, language: e.target.value as OutputLanguage }))}>
|
||||||
{!agent.language && <option value="" disabled>{t('ins.languagePending')}</option>}
|
{!agent.language && <option value="" disabled>{t('ins.languagePending')}</option>}
|
||||||
{LANGUAGES.map(l => <option key={l} value={l}>{languageLabel(l)}</option>)}
|
{LANGUAGES.map(l => <option key={l} value={l}>{languageLabel(l)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<div className="rule-list">
|
<div className="rule-list">
|
||||||
{agent.items.map(({ id, rule }) => (
|
{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))} />
|
<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" />{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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user