initial: novel-app snapshot
This commit is contained in:
77
static/js/components/sse-client.js
Normal file
77
static/js/components/sse-client.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/* Generic SSE client wrapper for novel-app streams.
|
||||
*
|
||||
* 后端事件契约:
|
||||
* token: {text:string}
|
||||
* section_start: {section:string} (拆书)
|
||||
* section_end: {section:string, content:string}
|
||||
* context: {summary:string} (写章节)
|
||||
* done: {content:string, word_count:number}
|
||||
* error: {error:string}
|
||||
*
|
||||
* handlers 全部可选:
|
||||
* {onToken, onSectionStart, onSectionEnd, onContext, onDone, onError}
|
||||
* 返回句柄 {close()}, close() 幂等. done / error 帧后自动 close.
|
||||
*/
|
||||
|
||||
const EVENTS = ["token", "section_start", "section_end", "context", "done", "error"];
|
||||
|
||||
export function createSSEStream(url, handlers = {}) {
|
||||
let closed = false;
|
||||
const es = new EventSource(url);
|
||||
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
es.close();
|
||||
}
|
||||
|
||||
function parse(e) {
|
||||
try {
|
||||
return JSON.parse(e.data);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
es.addEventListener("token", (e) => {
|
||||
const d = parse(e);
|
||||
if (d && handlers.onToken) handlers.onToken(d.text || "");
|
||||
});
|
||||
|
||||
es.addEventListener("section_start", (e) => {
|
||||
const d = parse(e);
|
||||
if (d && handlers.onSectionStart) handlers.onSectionStart(d.section);
|
||||
});
|
||||
|
||||
es.addEventListener("section_end", (e) => {
|
||||
const d = parse(e);
|
||||
if (d && handlers.onSectionEnd) handlers.onSectionEnd(d.section, d.content || "");
|
||||
close();
|
||||
});
|
||||
|
||||
es.addEventListener("context", (e) => {
|
||||
const d = parse(e);
|
||||
if (d && handlers.onContext) handlers.onContext(d.summary || "");
|
||||
});
|
||||
|
||||
es.addEventListener("done", (e) => {
|
||||
const d = parse(e);
|
||||
if (d && handlers.onDone) handlers.onDone(d.content || "", d.word_count || 0);
|
||||
close();
|
||||
});
|
||||
|
||||
es.addEventListener("error", (e) => {
|
||||
const d = parse(e);
|
||||
if (handlers.onError) handlers.onError(d && d.error ? d.error : "未知错误", "server");
|
||||
close();
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
if (!closed) {
|
||||
if (handlers.onError) handlers.onError("连接错误, 请检查后端日志", "connection");
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
return { close };
|
||||
}
|
||||
55
static/js/components/status-badge.js
Normal file
55
static/js/components/status-badge.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* Status badge renderer (Bootstrap pill). */
|
||||
|
||||
import { escapeHtml } from "../utils.js";
|
||||
|
||||
const COLOR_MAP = {
|
||||
// chapter / job statuses
|
||||
pending: "secondary",
|
||||
outlined: "info",
|
||||
drafting: "warning",
|
||||
done: "success",
|
||||
reviewed: "primary",
|
||||
running: "warning",
|
||||
failed: "danger",
|
||||
// novel statuses
|
||||
planning: "secondary",
|
||||
outlining: "info",
|
||||
reviewing: "primary",
|
||||
completed: "success",
|
||||
// character statuses
|
||||
alive: "success",
|
||||
dead: "dark",
|
||||
unknown: "secondary",
|
||||
// foreshadowing statuses
|
||||
planted: "info",
|
||||
resolved: "success",
|
||||
missed: "danger",
|
||||
};
|
||||
|
||||
const LABEL_MAP = {
|
||||
pending: "待处理",
|
||||
outlined: "已列纲",
|
||||
drafting: "写作中",
|
||||
done: "已完成",
|
||||
reviewed: "已审阅",
|
||||
running: "运行中",
|
||||
failed: "失败",
|
||||
planning: "筹划中",
|
||||
outlining: "大纲中",
|
||||
reviewing: "审阅中",
|
||||
completed: "完本",
|
||||
alive: "存活",
|
||||
dead: "死亡",
|
||||
unknown: "未知",
|
||||
planted: "已埋设",
|
||||
resolved: "已回收",
|
||||
missed: "已遗漏",
|
||||
};
|
||||
|
||||
/** Return a badge HTML string for a status. */
|
||||
export function statusBadge(status) {
|
||||
const key = String(status || "pending");
|
||||
const color = COLOR_MAP[key] || "secondary";
|
||||
const label = LABEL_MAP[key] || key;
|
||||
return `<span class="badge text-bg-${color}">${escapeHtml(label)}</span>`;
|
||||
}
|
||||
Reference in New Issue
Block a user