Rename Brain to Upload material and Instruction to Brain and AI

Upload material (/upload) drops the metric cards for a larger upload box
above the material list; Brain and AI moves to /brain. Old paths redirect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-27 22:36:52 +07:00
parent c1063bfa7e
commit ef4fb7d971
10 changed files with 43 additions and 40 deletions

View File

@@ -12,7 +12,7 @@
status = 200 status = 200
force = true force = true
# BrowserRouter: /brain, /results, ... all load index.html. # BrowserRouter: /upload, /results, ... all load index.html.
[[redirects]] [[redirects]]
from = "/*" from = "/*"
to = "/index.html" to = "/index.html"

View File

@@ -13,14 +13,15 @@ export default function App() {
<Routes> <Routes>
<Route element={<AppLayout />}> <Route element={<AppLayout />}>
<Route path="/production" element={<ProductionPage />} /> <Route path="/production" element={<ProductionPage />} />
<Route path="/brain" element={<BrainPage />} /> <Route path="/upload" element={<BrainPage />} />
<Route path="/instruction" element={<InstructionPage />} /> <Route path="/brain" element={<InstructionPage />} />
<Route path="/schedule" element={<SchedulePage />} /> <Route path="/schedule" element={<SchedulePage />} />
<Route path="/results" element={<ResultsPage />} /> <Route path="/results" element={<ResultsPage />} />
</Route> </Route>
{/* Indonesian paths from before the routes were renamed, so old links keep working. */} {/* Paths from before the routes were renamed, so old links keep working. */}
<Route path="/otak" element={<Navigate replace to="/brain" />} /> <Route path="/instruction" element={<Navigate replace to="/brain" />} />
<Route path="/perintah" element={<Navigate replace to="/instruction" />} /> <Route path="/otak" element={<Navigate replace to="/upload" />} />
<Route path="/perintah" element={<Navigate replace to="/brain" />} />
<Route path="/jadwal" element={<Navigate replace to="/schedule" />} /> <Route path="/jadwal" element={<Navigate replace to="/schedule" />} />
<Route path="/hasil" element={<Navigate replace to="/results" />} /> <Route path="/hasil" element={<Navigate replace to="/results" />} />
<Route path="*" element={<Navigate replace to="/production" />} /> <Route path="*" element={<Navigate replace to="/production" />} />

View File

@@ -33,7 +33,7 @@ export function AuthGate({ children }: { children: ReactNode }) {
<div> <div>
<h2>{t('auth.pickOrg')}</h2> <h2>{t('auth.pickOrg')}</h2>
<p>{t('auth.pickOrgCopy')}</p> <p>{t('auth.pickOrgCopy')}</p>
<OrganizationList hidePersonal afterSelectOrganizationUrl="/brain" afterCreateOrganizationUrl="/brain" /> <OrganizationList hidePersonal afterSelectOrganizationUrl="/upload" afterCreateOrganizationUrl="/upload" />
</div> </div>
</AuthScreen> </AuthScreen>
) )

View File

@@ -17,8 +17,8 @@ export const NAV_GROUPS: { label: Key; items: { to: string; label: Key; icon: Ic
{ {
label: 'nav.settings', label: 'nav.settings',
items: [ items: [
{ to: '/brain', label: 'nav.brain', icon: 'brain' }, { to: '/upload', label: 'nav.brain', icon: 'brain' },
{ to: '/instruction', label: 'nav.instruction', icon: 'spark' }, { to: '/brain', label: 'nav.instruction', icon: 'spark' },
{ to: '/schedule', label: 'nav.schedule', icon: 'calendar' }, { to: '/schedule', label: 'nav.schedule', icon: 'calendar' },
{ to: '/results', label: 'nav.results', icon: 'layers' }, { to: '/results', label: 'nav.results', icon: 'layers' },
], ],

View File

@@ -7,8 +7,8 @@ export const LANGS: { code: Lang; label: string }[] = [{ code: 'en', label: 'EN'
const en = { const en = {
// Layout // Layout
'nav.brain': 'Brain', 'nav.brain': 'Upload material',
'nav.instruction': 'Instruction', 'nav.instruction': 'Brain and AI',
'nav.schedule': 'Schedule', 'nav.schedule': 'Schedule',
'nav.results': 'Results', 'nav.results': 'Results',
'nav.dashboard': 'Dashboard', 'nav.dashboard': 'Dashboard',
@@ -81,8 +81,8 @@ const en = {
'prod.more': '+{n} more in Results', 'prod.more': '+{n} more in Results',
// Brain // Brain
'brain.title': 'Brain', 'brain.title': 'Upload material',
'brain.description': 'All team material is processed into one Brain that keeps growing. New content the AI creates goes in here too.', 'brain.description': 'Upload your team’s training material here. Every file is processed into the Brain the AI creates content from.',
'brain.metricMaterial': 'Material', 'brain.metricMaterial': 'Material',
'brain.metricMaterialSub': 'documents in the workspace', 'brain.metricMaterialSub': 'documents in the workspace',
'brain.metricReady': 'In the Brain', 'brain.metricReady': 'In the Brain',
@@ -126,8 +126,8 @@ const en = {
'brain.deleteConfirm': 'Delete “{name}” from the Brain?', 'brain.deleteConfirm': 'Delete “{name}” from the Brain?',
// Instruction // Instruction
'ins.title': 'Instruction', 'ins.title': 'Brain and AI',
'ins.description': 'Write a plain instruction. The AI turns it into research and content schedules that keep running until you stop them.', 'ins.description': 'Tell the AI what to make from the Brain. It turns your instruction into research and content schedules that keep running until you stop them.',
'ins.label': 'Instruction', 'ins.label': 'Instruction',
'ins.ex1': 'Make 2 summaries per day and 1 quiz per week about leadership', 'ins.ex1': 'Make 2 summaries per day and 1 quiz per week about leadership',
'ins.ex2': 'Research the internet every day about leadership', 'ins.ex2': 'Research the internet every day about leadership',
@@ -402,10 +402,10 @@ const en = {
'help.boxBody': 'Four steps from your material to finished content.', 'help.boxBody': 'Four steps from your material to finished content.',
'help.open': 'View the guide', 'help.open': 'View the guide',
'help.title': 'How Learning Lab Express works', 'help.title': 'How Learning Lab Express works',
'help.subtitle': 'Brain → Instruction → Schedule → Results', 'help.subtitle': 'Upload material → Brain and AI → Schedule → Results',
'help.brainTitle': 'Brain', 'help.brainTitle': 'Upload material',
'help.brainBody': 'Upload your training material. Once its knowledge is built, the AI creates everything from it.', 'help.brainBody': 'Upload your training material. Once its knowledge is built, the AI creates everything from it.',
'help.instructionTitle': 'Instruction', 'help.instructionTitle': 'Brain and AI',
'help.instructionBody': 'Say what you need in one sentence, e.g. “3 quizzes per week and 1 video per week about leadership”. Check the schedules, then approve them.', 'help.instructionBody': 'Say what you need in one sentence, e.g. “3 quizzes per week and 1 video per week about leadership”. Check the schedules, then approve them.',
'help.scheduleTitle': 'Schedule', 'help.scheduleTitle': 'Schedule',
'help.scheduleBody': 'Schedules run on their own. Pause them, change the topic, or produce now; each stays within its credit limit.', 'help.scheduleBody': 'Schedules run on their own. Pause them, change the topic, or produce now; each stays within its credit limit.',
@@ -458,8 +458,8 @@ const id: Record<Key, string> = {
'prod.nowTitle': 'Sedang berjalan', 'prod.nowTitle': 'Sedang berjalan',
'prod.nothingNow': 'Tidak ada yang sedang dibuat.', 'prod.nothingNow': 'Tidak ada yang sedang dibuat.',
'prod.more': '+{n} lagi di Hasil', 'prod.more': '+{n} lagi di Hasil',
'nav.brain': 'Otak', 'nav.brain': 'Upload materi',
'nav.instruction': 'Perintah', 'nav.instruction': 'Brain and AI',
'nav.schedule': 'Jadwal', 'nav.schedule': 'Jadwal',
'nav.results': 'Hasil', 'nav.results': 'Hasil',
'nav.dashboard': 'Dasbor', 'nav.dashboard': 'Dasbor',
@@ -496,8 +496,8 @@ const id: Record<Key, string> = {
'common.retry': 'Coba lagi', 'common.retry': 'Coba lagi',
'common.credits': '{n} kredit', 'common.credits': '{n} kredit',
'brain.title': 'Otak', 'brain.title': 'Upload materi',
'brain.description': 'Semua materi tim diproses menjadi satu Otak yang terus bertambah. Konten baru yang dihasilkan juga masuk ke sini.', 'brain.description': 'Unggah materi pelatihan tim di sini. Setiap file diproses ke dalam Otak yang menjadi sumber AI membuat konten.',
'brain.metricMaterial': 'Materi', 'brain.metricMaterial': 'Materi',
'brain.metricMaterialSub': 'dokumen di ruang kerja', 'brain.metricMaterialSub': 'dokumen di ruang kerja',
'brain.metricReady': 'Masuk Otak', 'brain.metricReady': 'Masuk Otak',
@@ -540,8 +540,8 @@ const id: Record<Key, string> = {
'brain.open': 'Buka {name}', 'brain.open': 'Buka {name}',
'brain.deleteConfirm': 'Hapus “{name}” dari Otak?', 'brain.deleteConfirm': 'Hapus “{name}” dari Otak?',
'ins.title': 'Perintah', 'ins.title': 'Brain and AI',
'ins.description': 'Tulis instruksi biasa. AI menyusunnya menjadi jadwal riset dan pembuatan konten, lalu terus berjalan sampai Anda menghentikannya.', 'ins.description': 'Beri tahu AI apa yang perlu dibuat dari Otak. AI menyusun instruksi Anda menjadi jadwal riset dan pembuatan konten yang terus berjalan sampai Anda menghentikannya.',
'ins.label': 'Instruksi', 'ins.label': 'Instruksi',
'ins.ex1': 'Bikin 2 ringkasan per hari dan 1 kuis per minggu soal leadership', 'ins.ex1': 'Bikin 2 ringkasan per hari dan 1 kuis per minggu soal leadership',
'ins.ex2': 'Riset internet tiap hari soal leadership', 'ins.ex2': 'Riset internet tiap hari soal leadership',
@@ -813,10 +813,10 @@ const id: Record<Key, string> = {
'help.boxBody': 'Empat langkah dari materi sampai konten jadi.', 'help.boxBody': 'Empat langkah dari materi sampai konten jadi.',
'help.open': 'Lihat panduan', 'help.open': 'Lihat panduan',
'help.title': 'Cara kerja Learning Lab Express', 'help.title': 'Cara kerja Learning Lab Express',
'help.subtitle': 'Otak → Perintah → Jadwal → Hasil', 'help.subtitle': 'Upload materi → Brain and AI → Jadwal → Hasil',
'help.brainTitle': 'Otak', 'help.brainTitle': 'Upload materi',
'help.brainBody': 'Unggah materi pelatihan. Setelah pengetahuannya dibangun, AI membuat semua konten dari situ.', 'help.brainBody': 'Unggah materi pelatihan. Setelah pengetahuannya dibangun, AI membuat semua konten dari situ.',
'help.instructionTitle': 'Perintah', 'help.instructionTitle': 'Brain and AI',
'help.instructionBody': 'Tulis kebutuhan Anda dalam satu kalimat, misalnya “3 kuis per minggu dan 1 video per minggu soal leadership”. Periksa jadwalnya, lalu setujui.', 'help.instructionBody': 'Tulis kebutuhan Anda dalam satu kalimat, misalnya “3 kuis per minggu dan 1 video per minggu soal leadership”. Periksa jadwalnya, lalu setujui.',
'help.scheduleTitle': 'Jadwal', 'help.scheduleTitle': 'Jadwal',
'help.scheduleBody': 'Jadwal berjalan sendiri. Jeda, ganti topik, atau produksi sekarang; masing-masing tetap dalam batas kreditnya.', 'help.scheduleBody': 'Jadwal berjalan sendiri. Jeda, ganti topik, atau produksi sekarang; masing-masing tetap dalam batas kreditnya.',

View File

@@ -5,7 +5,7 @@ import {
type Source, type Source,
} from '../api/engine' } from '../api/engine'
import { Icon } from '../components/Icon' import { Icon } from '../components/Icon'
import { Empty, PageHead, Skeleton, SkeletonLines, SkeletonRows } from '../components/Page' import { Empty, PageHead, SkeletonLines, SkeletonRows } from '../components/Page'
import { toast } from '../components/Toast' import { toast } from '../components/Toast'
import { locale, t } from '../i18n' import { locale, t } from '../i18n'
@@ -28,17 +28,10 @@ export function BrainPage() {
// Poll quickly while anything is processing or building; slowly otherwise (colleagues may upload too). // Poll quickly while anything is processing or building; slowly otherwise (colleagues may upload too).
refetchInterval: q => (q.state.data?.some(d => phaseOf(d) === 'working') ? 3000 : 20_000), refetchInterval: q => (q.state.data?.some(d => phaseOf(d) === 'working') ? 3000 : 20_000),
}) })
const ready = docs.data?.filter(d => phaseOf(d) === 'ready').length ?? 0
const waiting = docs.data?.filter(d => phaseOf(d) === 'answer').length ?? 0
return ( return (
<> <>
<PageHead title={t('brain.title')} description={t('brain.description')} /> <PageHead title={t('brain.title')} description={t('brain.description')} />
<div className="metrics">
<div className="metric"><span>{t('brain.metricMaterial')}</span>{docs.data ? <strong>{docs.data.length}</strong> : <Skeleton className="skel-num" />}<small>{t('brain.metricMaterialSub')}</small></div>
<div className="metric"><span>{t('brain.metricReady')}</span>{docs.data ? <strong>{ready}</strong> : <Skeleton className="skel-num" />}<small>{t('brain.metricReadySub')}</small></div>
<div className="metric"><span>{t('brain.metricAction')}</span>{docs.data ? <strong>{waiting}</strong> : <Skeleton className="skel-num" />}<small>{t('brain.metricActionSub')}</small></div>
</div>
<UploadBox /> <UploadBox />
<div className="card"> <div className="card">
<div className="card-head"><h3>{t('brain.listTitle')}</h3><Icon name="brain" /></div> <div className="card-head"><h3>{t('brain.listTitle')}</h3><Icon name="brain" /></div>
@@ -116,7 +109,7 @@ function UploadBox() {
const busy = items.some(i => i.state === 'uploading') const busy = items.some(i => i.state === 'uploading')
return ( return (
<div className={`upload-box upload-compact${drag ? ' drag' : ''}`}> <div className={`upload-box upload-large${drag ? ' drag' : ''}`}>
{drag && ( {drag && (
<div className="drop-overlay" aria-hidden="true"> <div className="drop-overlay" aria-hidden="true">
<div><Icon name="upload" /><b>{t('brain.dropHere')}</b></div> <div><Icon name="upload" /><b>{t('brain.dropHere')}</b></div>

View File

@@ -67,7 +67,7 @@ export function ProductionPage() {
<div><small>{t('prod.links')}</small>{graph ? <b>{graph.links.filter(l => !l.hub).length}</b> : <b>-</b>}<span>{t('prod.linksSub')}</span></div> <div><small>{t('prod.links')}</small>{graph ? <b>{graph.links.filter(l => !l.hub).length}</b> : <b>-</b>}<span>{t('prod.linksSub')}</span></div>
</div> </div>
<h3>{t('prod.scheduleTitle')}</h3> <h3>{t('prod.scheduleTitle')}</h3>
{schedules.data?.length === 0 && <p className="graph-empty">{t('prod.noSchedules')} <Link to="/instruction">{t('prod.giveInstruction')}</Link></p>} {schedules.data?.length === 0 && <p className="graph-empty">{t('prod.noSchedules')} <Link to="/brain">{t('prod.giveInstruction')}</Link></p>}
<ul className="graph-list"> <ul className="graph-list">
{schedules.data?.slice(0, 4).map(s => ( {schedules.data?.slice(0, 4).map(s => (
<li key={s.agent.id}> <li key={s.agent.id}>

View File

@@ -105,7 +105,7 @@ function NoResults({ filtered, icon }: { filtered: boolean; icon: IconName }) {
<span className="empty-icon"><Icon name={icon} /></span> <span className="empty-icon"><Icon name={icon} /></span>
<h3>{t('res.empty')}</h3> <h3>{t('res.empty')}</h3>
<p>{t('res.emptyCopy')}</p> <p>{t('res.emptyCopy')}</p>
<Link className="btn light" to="/instruction"><Icon name="plus" />{t('sch.newInstruction')}</Link> <Link className="btn light" to="/brain"><Icon name="plus" />{t('sch.newInstruction')}</Link>
</div> </div>
) )
} }
@@ -427,7 +427,7 @@ function SourceRow({ source: s }: { source: FoundSource }) {
<div> <div>
<span className={`status ${tone}`}><i className="dot"></i>{label}</span> <span className={`status ${tone}`}><i className="dot"></i>{label}</span>
<h3>{s.url ? <a href={s.url} target="_blank" rel="noreferrer">{s.title}</a> : s.title}</h3> <h3>{s.url ? <a href={s.url} target="_blank" rel="noreferrer">{s.title}</a> : s.title}</h3>
<p>{describeRule(s.rule)} · {fmt(s.createdAt)}{s.status === 'accepted' && <> · <Link to="/brain">{t('res.inBrain')}</Link></>}</p> <p>{describeRule(s.rule)} · {fmt(s.createdAt)}{s.status === 'accepted' && <> · <Link to="/upload">{t('res.inBrain')}</Link></>}</p>
{s.preview && <p className="source-preview">{s.preview}</p>} {s.preview && <p className="source-preview">{s.preview}</p>}
{review.error && <span className="error">{review.error.message}</span>} {review.error && <span className="error">{review.error.message}</span>}
</div> </div>

View File

@@ -24,7 +24,7 @@ export function SchedulePage() {
return ( return (
<> <>
<PageHead title={t('sch.title')} description={t('sch.description')} <PageHead title={t('sch.title')} description={t('sch.description')}
actions={<Link className="btn primary" to="/instruction"><Icon name="plus" />{t('sch.newInstruction')}</Link>} /> actions={<Link className="btn primary" to="/brain"><Icon name="plus" />{t('sch.newInstruction')}</Link>} />
<div className="card"> <div className="card">
<div className="card-head"><h3>{t('sch.cardTitle')}</h3><Icon name="calendar" /></div> <div className="card-head"><h3>{t('sch.cardTitle')}</h3><Icon name="calendar" /></div>
{schedules.isPending && <SkeletonRows label={t('sch.loading')} />} {schedules.isPending && <SkeletonRows label={t('sch.loading')} />}

View File

@@ -180,3 +180,12 @@ textarea{resize:vertical;line-height:1.6}
@keyframes blink{50%{opacity:0}} @keyframes blink{50%{opacity:0}}
@keyframes appear{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}} @keyframes appear{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.dots b,.agent-chat .ai-message,.appear,.ai-message .typing:after{animation:none}} @media (prefers-reduced-motion:reduce){.dots b,.agent-chat .ai-message,.appear,.ai-message .typing:after{animation:none}}
.upload-box.upload-large{display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;padding:56px 32px;margin-bottom:24px}
.upload-large .upload-symbol{width:64px;height:64px;justify-content:center;align-items:center;margin-bottom:10px}
.upload-large .upload-symbol svg{width:28px;height:28px}
.upload-large h3{font-size:20px}
.upload-large p{font-size:14px;max-width:520px;margin:6px auto 4px}
.upload-large small{font-size:12px;color:var(--muted)}
.upload-large .upload-progress{text-align:left;max-width:520px;margin:12px auto 0}
.upload-large .btn{margin-top:14px;padding-inline:22px}
@media(max-width:600px){.upload-box.upload-large{padding:32px 20px}.upload-large .btn{width:100%}}