production-dashboard #1

Merged
iqbal merged 13 commits from production-dashboard into main 2026-10-27 14:58:53 +00:00
4 changed files with 19 additions and 7 deletions
Showing only changes of commit 9f9fc93dfe - Show all commits

View File

@@ -5,7 +5,7 @@
import { request } from '../api/client' import { request } from '../api/client'
import { t } from '../i18n' import { t } from '../i18n'
import { kindLabel, type ContentKind, type Rule } from './parse' import { kindLabel, type ContentKind, type Rule } from './parse'
import { expressAgents, MAX_ATTEMPTS, PLAN_ACTIVE, productionStatus, retryItem, type PlanItem, type Schedule } from './schedules' import { expressAgents, MAX_ATTEMPTS, PLAN_ACTIVE, productionStatus, retryItem, runNow, type PlanItem, type Schedule } from './schedules'
const AGENTS = '/organizations/current/agents' const AGENTS = '/organizations/current/agents'
@@ -39,8 +39,8 @@ export interface Material {
sources: string[] sources: string[]
createdAt: string createdAt: string
rule: Rule rule: Rule
/** The plan item to produce again, when the engine allows another attempt. */ /** How to produce it again, when the engine allows another attempt: the plan item, or (video) a new run. */
retry?: { planId: number; itemId: number } retry?: { planId: number; itemId: number } | { run: true }
video?: { url: string | null; progress: number; credits: number | null } video?: { url: string | null; progress: number; credits: number | null }
} }
@@ -147,11 +147,15 @@ function videoStatus(s: string): MaterialStatus {
return 'generating' return 'generating'
} }
async function videosOf({ agent, rule }: Schedule): Promise<Material[]> { async function videosOf({ agent, rule, active }: Schedule): Promise<Material[]> {
const drafts = await request<RawVideo[]>(`${AGENTS}/${agent.id}/video-drafts`) const drafts = await request<RawVideo[]>(`${AGENTS}/${agent.id}/video-drafts`)
// A video draft cannot be retried itself; a new run of its schedule makes it again. Offered on the
// newest draft only, so older failures don't each start another (paid) run.
const newest = Math.max(...drafts.map(d => d.id))
return drafts.map(d => { return drafts.map(d => {
const status = videoStatus(d.status) const status = videoStatus(d.status)
return { return {
retry: status === 'failed' && active && d.id === newest ? { run: true } as const : undefined,
id: d.id, agentId: agent.id, kind: 'video', status, revision: 0, id: d.id, agentId: agent.id, kind: 'video', status, revision: 0,
parts: [{ id: d.id, revision: 0, kind: 'video', status }], parts: [{ id: d.id, revision: 0, kind: 'video', status }],
planTitle: d.title, composeSetId: null, sources: d.sources.map(s => s.title), createdAt: d.created_at, rule, planTitle: d.title, composeSetId: null, sources: d.sources.map(s => s.title), createdAt: d.created_at, rule,
@@ -195,9 +199,10 @@ export async function materialContent(m: Material): Promise<{ content: MaterialC
// The engine renders slide decks to PPTX itself; other kinds have no file export there. // The engine renders slide decks to PPTX itself; other kinds have no file export there.
export const exportPptx = (deckId: number) => request<Blob>(`/slide-deck/${deckId}/export`, { method: 'POST', as: 'blob' }) export const exportPptx = (deckId: number) => request<Blob>(`/slide-deck/${deckId}/export`, { method: 'POST', as: 'blob' })
// Every part waiting for review gets the same decision, so a SCORM module is approved as one. export const retryMaterial = ({ agentId, retry }: Material) =>
export const retryMaterial = (m: Material) => retryItem(m.agentId, m.retry!.planId, m.retry!.itemId) retry && 'run' in retry ? runNow(agentId) : retryItem(agentId, retry!.planId, retry!.itemId)
// Every part waiting for review gets the same decision, so a SCORM module is approved as one.
export async function decideMaterial(m: Material, decision: 'accept' | 'reject') { export async function decideMaterial(m: Material, decision: 'accept' | 'reject') {
for (const p of m.parts.filter(p => p.status === 'ready')) { for (const p of m.parts.filter(p => p.status === 'ready')) {
if (m.kind === 'video') await request(`${AGENTS}/${m.agentId}/video-drafts/${p.id}/review`, { method: 'POST', json: { decision } }) if (m.kind === 'video') await request(`${AGENTS}/${m.agentId}/video-drafts/${p.id}/review`, { method: 'POST', json: { decision } })

View File

@@ -117,6 +117,7 @@ const en = {
'brain.needsCreditsNeed': ' (needs {credits})', 'brain.needsCreditsNeed': ' (needs {credits})',
'brain.failed': 'Could not add this to the Brain.', 'brain.failed': 'Could not add this to the Brain.',
'brain.retryWith': 'Try again ({credits})', 'brain.retryWith': 'Try again ({credits})',
'brain.failedAfterBuild': 'Processing this file failed after its knowledge was built. Schedules that use the whole Brain stop until it is removed: delete it and upload it again.',
'brain.confirmCopy': 'Adding this document to the Brain uses {credits} from the workspace.', 'brain.confirmCopy': 'Adding this document to the Brain uses {credits} from the workspace.',
'brain.confirmCreditsFallback': 'credits', 'brain.confirmCreditsFallback': 'credits',
'brain.addToBrain': 'Add to Brain', 'brain.addToBrain': 'Add to Brain',
@@ -531,6 +532,7 @@ const id: Record<Key, string> = {
'brain.needsCreditsNeed': ' (butuh {credits})', 'brain.needsCreditsNeed': ' (butuh {credits})',
'brain.failed': 'Gagal memasukkan ke Otak.', 'brain.failed': 'Gagal memasukkan ke Otak.',
'brain.retryWith': 'Coba lagi ({credits})', 'brain.retryWith': 'Coba lagi ({credits})',
'brain.failedAfterBuild': 'Pemrosesan file ini gagal setelah pengetahuannya dibangun. Jadwal yang memakai seluruh Otak berhenti sampai file ini dihapus: hapus lalu unggah ulang.',
'brain.confirmCopy': 'Memasukkan dokumen ini ke Otak memakai {credits} dari ruang kerja.', 'brain.confirmCopy': 'Memasukkan dokumen ini ke Otak memakai {credits} dari ruang kerja.',
'brain.confirmCreditsFallback': 'kredit', 'brain.confirmCreditsFallback': 'kredit',
'brain.addToBrain': 'Masukkan ke Otak', 'brain.addToBrain': 'Masukkan ke Otak',

View File

@@ -12,6 +12,7 @@ import { locale, t } from '../i18n'
type Phase = 'ready' | 'working' | 'answer' | 'failed' type Phase = 'ready' | 'working' | 'answer' | 'failed'
function phaseOf(s: Source): Phase { function phaseOf(s: Source): Phase {
// A failed document blocks every whole-Brain schedule in the engine, even with a built knowledge base.
if (s.status === 'failed' || s.knowledge === 'failed') return 'failed' if (s.status === 'failed' || s.knowledge === 'failed') return 'failed'
if (s.knowledge === 'ready') return 'ready' if (s.knowledge === 'ready') return 'ready'
if (s.knowledge === 'building' || s.status !== 'completed') return 'working' if (s.knowledge === 'building' || s.status !== 'completed') return 'working'
@@ -246,6 +247,10 @@ function KnowledgeAction({ doc }: { doc: Source }) {
text = t('brain.needsCredits', { need: credits ? t('brain.needsCreditsNeed', { credits }) : '' }) text = t('brain.needsCredits', { need: credits ? t('brain.needsCreditsNeed', { credits }) : '' })
actions = <button className="btn small" disabled={busy} onClick={() => build.mutate()}>{t('common.retry')}</button> actions = <button className="btn small" disabled={busy} onClick={() => build.mutate()}>{t('common.retry')}</button>
break break
case 'ready':
// Processing failed after the build: building again is refused, only removing it unblocks schedules.
text = t('brain.failedAfterBuild')
break
case 'failed': case 'failed':
text = d.error || t('brain.failed') text = d.error || t('brain.failed')
if (d.quote) actions = <button className="btn small" disabled={busy} onClick={() => build.mutate()}>{t('brain.retryWith', { credits: credits ?? '' })}</button> if (d.quote) actions = <button className="btn small" disabled={busy} onClick={() => build.mutate()}>{t('brain.retryWith', { credits: credits ?? '' })}</button>

View File

@@ -198,7 +198,7 @@ dialog::backdrop{background:rgba(16,24,40,.45);backdrop-filter:blur(4px)}
.help-step h3{font-size:14.5px} .help-step h3{font-size:14.5px}
.help-step p{font-size:13px} .help-step p{font-size:13px}
.toast{background:var(--ink);border-radius:14px;font-size:13.5px;box-shadow:0 18px 40px -16px rgba(16,24,40,.6)} .toast{background:var(--ink);border-radius:14px;font-size:13.5px;box-shadow:0 18px 40px -16px rgba(16,24,40,.6)}
.toast.error{background:#991b1b} .toast.error{background:#991b1b;color:#fff}
.progress-orb{background:var(--light);color:var(--blue);border-radius:20px} .progress-orb{background:var(--light);color:var(--blue);border-radius:20px}
/* Sign in: bright blue story panel with dot grid and light glow */ /* Sign in: bright blue story panel with dot grid and light glow */