Add status progress for batch tool operations
This commit is contained in:
@@ -100,6 +100,24 @@ function addFiles(files) {
|
||||
page = Math.max(0, Math.ceil(rows.length / PAGE_SIZE) - 1);
|
||||
renderRows();
|
||||
}
|
||||
async function readJSONStream(response, handle) {
|
||||
if (!response.ok || !response.body) throw new Error('Request failed.');
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let newline;
|
||||
while ((newline = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, newline);
|
||||
buffer = buffer.slice(newline + 1);
|
||||
if (line.trim()) handle(JSON.parse(line));
|
||||
}
|
||||
}
|
||||
if (buffer.trim()) handle(JSON.parse(buffer));
|
||||
}
|
||||
function renderRows() {
|
||||
const oldPager = document.getElementById('selectedPager');
|
||||
if (oldPager) oldPager.remove();
|
||||
@@ -183,12 +201,31 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
}
|
||||
statusEl.textContent = 'Uploading...';
|
||||
statusEl.classList.add('is-busy');
|
||||
const res = await fetch('/api/plec/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rows }),
|
||||
});
|
||||
const j = await res.json();
|
||||
let j = null;
|
||||
try {
|
||||
const res = await fetch('/api/plec/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rows }),
|
||||
});
|
||||
await readJSONStream(res, (msg) => {
|
||||
if (msg.type === 'status') {
|
||||
statusEl.textContent = msg.message;
|
||||
statusEl.classList.add('is-busy');
|
||||
} else if (msg.type === 'done') {
|
||||
j = msg;
|
||||
} else if (msg.type === 'error') {
|
||||
j = { error: msg.message };
|
||||
} else if (msg.type === 'rowErrors') {
|
||||
j = { rowErrors: msg.rowErrors };
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
|
||||
return;
|
||||
}
|
||||
if (!j) { statusEl.classList.remove('is-busy'); statusEl.innerHTML = '<span class="err">No response.</span>'; return; }
|
||||
if (j.rowErrors) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
for (const re of j.rowErrors) {
|
||||
@@ -260,11 +297,12 @@ type plecRow struct {
|
||||
}
|
||||
|
||||
func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
send := newJSONStream(w)
|
||||
var req struct {
|
||||
Rows []plecRow `json:"rows"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
cfg := LoadConfig().Plec
|
||||
@@ -289,11 +327,11 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if len(rowErrors) > 0 {
|
||||
writeJSON(w, map[string]any{"rowErrors": rowErrors})
|
||||
send(map[string]any{"type": "rowErrors", "rowErrors": rowErrors})
|
||||
return
|
||||
}
|
||||
if len(valid) == 0 {
|
||||
writeJSON(w, map[string]any{"error": "Nothing to upload."})
|
||||
send(map[string]any{"type": "error", "message": "Nothing to upload."})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -303,37 +341,39 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
mw := multipart.NewWriter(&buf)
|
||||
for i, row := range valid {
|
||||
idx := i + 1
|
||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("Preparing %d/%d %s", idx, len(valid), filepath.Base(row.Path))})
|
||||
data, err := os.ReadFile(row.Path)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"error": "Read failed: " + err.Error()})
|
||||
send(map[string]any{"type": "error", "message": "Read failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
stampedName := appendTimestamp(filepath.Base(row.Path), stamp)
|
||||
fw, err := createFormFileWithType(mw, fmt.Sprintf("fileUpload_%d", idx), stampedName, "text/html")
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if _, err := fw.Write(data); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||
// Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars.
|
||||
iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||
if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d file%s...", len(valid), pluralS(len(valid)))})
|
||||
httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
@@ -345,18 +385,18 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"error": "Network error: " + err.Error()})
|
||||
send(map[string]any{"type": "error", "message": "Network error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
writeJSON(w, map[string]any{"error": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))})
|
||||
send(map[string]any{"type": "error", "message": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))})
|
||||
return
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||
writeJSON(w, map[string]any{"error": "Server returned non-JSON:\n" + truncate(string(respBody), 800)})
|
||||
send(map[string]any{"type": "error", "message": "Server returned non-JSON:\n" + truncate(string(respBody), 800)})
|
||||
return
|
||||
}
|
||||
var preview any
|
||||
@@ -365,7 +405,7 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
} else if v, ok := parsed["previewURL"]; ok {
|
||||
preview = v
|
||||
}
|
||||
writeJSON(w, map[string]any{"preview": preview, "raw": parsed, "rawText": string(respBody)})
|
||||
send(map[string]any{"type": "done", "preview": preview, "raw": parsed, "rawText": string(respBody)})
|
||||
}
|
||||
|
||||
func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user