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 38 additions and 2 deletions
Showing only changes of commit 7c17191b8b - Show all commits

View File

@@ -49,6 +49,7 @@ function asSource(d: EngineDocument): Source {
export const listDocuments = async () => (await request<EngineDocument[]>('/upload/list')).map(asSource) export const listDocuments = async () => (await request<EngineDocument[]>('/upload/list')).map(asSource)
export const documentStatus = (id: number) => request<EngineDocument>(`/upload/${id}`) export const documentStatus = (id: number) => request<EngineDocument>(`/upload/${id}`)
export const deleteDocument = (id: number) => request<null>(`/upload/${id}`, { method: 'DELETE' }) export const deleteDocument = (id: number) => request<null>(`/upload/${id}`, { method: 'DELETE' })
export const documentFile = (id: number) => request<Blob>(`/upload/${id}/file`, { as: 'blob' })
export async function supportedExtensions(): Promise<Set<string>> { export async function supportedExtensions(): Promise<Set<string>> {
const f = await request<{ extensions: string[] }>('/upload/supported-formats') const f = await request<{ extensions: string[] }>('/upload/supported-formats')

View File

@@ -83,6 +83,7 @@ const en = {
'brain.confirmCreditsFallback': 'credits', 'brain.confirmCreditsFallback': 'credits',
'brain.addToBrain': 'Add to Brain', 'brain.addToBrain': 'Add to Brain',
'brain.delete': 'Delete {name}', 'brain.delete': 'Delete {name}',
'brain.open': 'Open {name}',
'brain.deleteConfirm': 'Delete “{name}” from the Brain?', 'brain.deleteConfirm': 'Delete “{name}” from the Brain?',
// Instruction // Instruction
@@ -460,6 +461,7 @@ const id: Record<Key, string> = {
'brain.confirmCreditsFallback': 'kredit', 'brain.confirmCreditsFallback': 'kredit',
'brain.addToBrain': 'Masukkan ke Otak', 'brain.addToBrain': 'Masukkan ke Otak',
'brain.delete': 'Hapus {name}', 'brain.delete': 'Hapus {name}',
'brain.open': 'Buka {name}',
'brain.deleteConfirm': 'Hapus “{name}” dari Otak?', 'brain.deleteConfirm': 'Hapus “{name}” dari Otak?',
'ins.title': 'Perintah', 'ins.title': 'Perintah',

View File

@@ -1,7 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react' import { useEffect, useRef, useState, type ReactNode } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { import {
answerVersion, buildKnowledge, deleteDocument, knowledgeDetail, listDocuments, NEEDS_ANSWER, supportedExtensions, uploadDocument, answerVersion, buildKnowledge, deleteDocument, documentFile, knowledgeDetail, listDocuments, NEEDS_ANSWER, supportedExtensions, uploadDocument,
type Source, type Source,
} from '../api/engine' } from '../api/engine'
import { Icon } from '../components/Icon' import { Icon } from '../components/Icon'
@@ -154,7 +154,7 @@ function DocumentRow({ doc }: { doc: Source }) {
<div className="result-row"> <div className="result-row">
<span className={`file-icon ${doc.format.toLowerCase()}`}>{doc.format.slice(0, 4)}</span> <span className={`file-icon ${doc.format.toLowerCase()}`}>{doc.format.slice(0, 4)}</span>
<div style={{ minWidth: 0, flex: 1 }}> <div style={{ minWidth: 0, flex: 1 }}>
<b>{doc.title}</b> <OpenButton doc={doc} />
<div><small>{new Date(doc.createdAt).toLocaleDateString(locale())}</small></div> <div><small>{new Date(doc.createdAt).toLocaleDateString(locale())}</small></div>
{phase === 'working' && <Progress doc={doc} />} {phase === 'working' && <Progress doc={doc} />}
{(phase === 'answer' || phase === 'failed') && <KnowledgeAction doc={doc} />} {(phase === 'answer' || phase === 'failed') && <KnowledgeAction doc={doc} />}
@@ -165,6 +165,36 @@ function DocumentRow({ doc }: { doc: Source }) {
) )
} }
// Formats a browser can show in a tab; anything else is saved under its original name.
const VIEWABLE = new Set(['pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'txt', 'md', 'csv', 'mp3', 'wav', 'm4a', 'ogg', 'mp4', 'webm', 'mov'])
function OpenButton({ doc }: { doc: Source }) {
const [busy, setBusy] = useState(false)
async function open() {
const inTab = VIEWABLE.has(doc.format.toLowerCase())
// Opened before the fetch: a tab opened after an await counts as a popup and is blocked.
const tab = inTab ? window.open('', '_blank') : null
setBusy(true)
try {
const url = URL.createObjectURL(await documentFile(doc.docId))
if (tab) tab.location.href = url
// Not viewable, or the tab was blocked: save it instead of doing nothing.
else Object.assign(document.createElement('a'), { href: url, download: doc.originalName }).click()
setTimeout(() => URL.revokeObjectURL(url), 60_000)
} catch (e) {
tab?.close()
toast((e as Error).message, { error: true })
} finally {
setBusy(false)
}
}
return (
<button className="doc-title" disabled={busy} title={t('brain.open', { name: doc.originalName })} onClick={open}>
{doc.title}
</button>
)
}
function StatusBadge({ doc, phase }: { doc: Source; phase: Phase }) { function StatusBadge({ doc, phase }: { doc: Source; phase: Phase }) {
const label = { const label = {
ready: t('brain.statusReady'), ready: t('brain.statusReady'),

View File

@@ -10,6 +10,9 @@
.doc-action-buttons{display:flex;gap:7px;flex-wrap:wrap} .doc-action-buttons{display:flex;gap:7px;flex-wrap:wrap}
.doc-action .error{margin:0;font-size:11px} .doc-action .error{margin:0;font-size:11px}
.result-row b{overflow-wrap:anywhere} .result-row b{overflow-wrap:anywhere}
.doc-title{display:block;max-width:100%;border:0;padding:0;background:none;text-align:left;font:inherit;font-weight:700;color:var(--ink);overflow-wrap:anywhere;cursor:pointer}
.doc-title:hover{color:var(--blue);text-decoration:underline}
.doc-title:disabled{cursor:progress;color:#8b97a9}
.drop-overlay{position:fixed;inset:0;z-index:50;display:grid;place-items:center;background:#142d5059;backdrop-filter:blur(2px);pointer-events:none} .drop-overlay{position:fixed;inset:0;z-index:50;display:grid;place-items:center;background:#142d5059;backdrop-filter:blur(2px);pointer-events:none}
.drop-overlay>div{display:flex;flex-direction:column;align-items:center;gap:12px;padding:34px 46px;border:2px dashed var(--blue);border-radius:16px;background:white;color:var(--blue);font-size:14px} .drop-overlay>div{display:flex;flex-direction:column;align-items:center;gap:12px;padding:34px 46px;border:2px dashed var(--blue);border-radius:16px;background:white;color:var(--blue);font-size:14px}
.drop-overlay svg{width:34px;height:34px} .drop-overlay svg{width:34px;height:34px}