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
force = true
# BrowserRouter: /brain, /results, ... all load index.html.
# BrowserRouter: /upload, /results, ... all load index.html.
[[redirects]]
from = "/*"
to = "/index.html"

View File

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

View File

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

View File

@@ -17,8 +17,8 @@ export const NAV_GROUPS: { label: Key; items: { to: string; label: Key; icon: Ic
{
label: 'nav.settings',
items: [
{ to: '/brain', label: 'nav.brain', icon: 'brain' },
{ to: '/instruction', label: 'nav.instruction', icon: 'spark' },
{ to: '/upload', label: 'nav.brain', icon: 'brain' },
{ to: '/brain', label: 'nav.instruction', icon: 'spark' },
{ to: '/schedule', label: 'nav.schedule', icon: 'calendar' },
{ 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 = {
// Layout
'nav.brain': 'Brain',
'nav.instruction': 'Instruction',
'nav.brain': 'Upload material',
'nav.instruction': 'Brain and AI',
'nav.schedule': 'Schedule',
'nav.results': 'Results',
'nav.dashboard': 'Dashboard',
@@ -81,8 +81,8 @@ const en = {
'prod.more': '+{n} more in Results',
// Brain
'brain.title': 'Brain',
'brain.description': 'All team material is processed into one Brain that keeps growing. New content the AI creates goes in here too.',
'brain.title': 'Upload material',
'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.metricMaterialSub': 'documents in the workspace',
'brain.metricReady': 'In the Brain',
@@ -126,8 +126,8 @@ const en = {
'brain.deleteConfirm': 'Delete “{name}” from the Brain?',
// Instruction
'ins.title': 'Instruction',
'ins.description': 'Write a plain instruction. The AI turns it into research and content schedules that keep running until you stop them.',
'ins.title': 'Brain and AI',
'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.ex1': 'Make 2 summaries per day and 1 quiz per week 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.open': 'View the guide',
'help.title': 'How Learning Lab Express works',
'help.subtitle': 'Brain → Instruction → Schedule → Results',
'help.brainTitle': 'Brain',
'help.subtitle': 'Upload material → Brain and AI → Schedule → Results',
'help.brainTitle': 'Upload material',
'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.scheduleTitle': 'Schedule',
'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.nothingNow': 'Tidak ada yang sedang dibuat.',
'prod.more': '+{n} lagi di Hasil',
'nav.brain': 'Otak',
'nav.instruction': 'Perintah',
'nav.brain': 'Upload materi',
'nav.instruction': 'Brain and AI',
'nav.schedule': 'Jadwal',
'nav.results': 'Hasil',
'nav.dashboard': 'Dasbor',
@@ -496,8 +496,8 @@ const id: Record<Key, string> = {
'common.retry': 'Coba lagi',
'common.credits': '{n} kredit',
'brain.title': 'Otak',
'brain.description': 'Semua materi tim diproses menjadi satu Otak yang terus bertambah. Konten baru yang dihasilkan juga masuk ke sini.',
'brain.title': 'Upload materi',
'brain.description': 'Unggah materi pelatihan tim di sini. Setiap file diproses ke dalam Otak yang menjadi sumber AI membuat konten.',
'brain.metricMaterial': 'Materi',
'brain.metricMaterialSub': 'dokumen di ruang kerja',
'brain.metricReady': 'Masuk Otak',
@@ -540,8 +540,8 @@ const id: Record<Key, string> = {
'brain.open': 'Buka {name}',
'brain.deleteConfirm': 'Hapus “{name}” dari Otak?',
'ins.title': 'Perintah',
'ins.description': 'Tulis instruksi biasa. AI menyusunnya menjadi jadwal riset dan pembuatan konten, lalu terus berjalan sampai Anda menghentikannya.',
'ins.title': 'Brain and AI',
'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.ex1': 'Bikin 2 ringkasan per hari dan 1 kuis per minggu 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.open': 'Lihat panduan',
'help.title': 'Cara kerja Learning Lab Express',
'help.subtitle': 'Otak → Perintah → Jadwal → Hasil',
'help.brainTitle': 'Otak',
'help.subtitle': 'Upload materi → Brain and AI → Jadwal → Hasil',
'help.brainTitle': 'Upload materi',
'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.scheduleTitle': 'Jadwal',
'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,
} from '../api/engine'
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 { 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).
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 (
<>
<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 />
<div className="card">
<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')
return (
<div className={`upload-box upload-compact${drag ? ' drag' : ''}`}>
<div className={`upload-box upload-large${drag ? ' drag' : ''}`}>
{drag && (
<div className="drop-overlay" aria-hidden="true">
<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>
<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">
{schedules.data?.slice(0, 4).map(s => (
<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>
<h3>{t('res.empty')}</h3>
<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>
)
}
@@ -427,7 +427,7 @@ function SourceRow({ source: s }: { source: FoundSource }) {
<div>
<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>
<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>}
{review.error && <span className="error">{review.error.message}</span>}
</div>

View File

@@ -24,7 +24,7 @@ export function SchedulePage() {
return (
<>
<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-head"><h3>{t('sch.cardTitle')}</h3><Icon name="calendar" /></div>
{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 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}}
.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%}}