|
@@ -426,11 +426,91 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const handleQuickCommand = (command: string) => {
|
|
|
|
|
- setInput(command);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- handleSubmit();
|
|
|
|
|
- }, 50);
|
|
|
|
|
|
|
+ const handleQuickCommand = async (command: string) => {
|
|
|
|
|
+ if (loading) return;
|
|
|
|
|
+ setInput("");
|
|
|
|
|
+ setMessages((prev) => [...prev, { role: "user", content: command, timestamp: Date.now() }]);
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ const lower = command.toLowerCase();
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (lower.match(/^(写下一章|write next)/)) {
|
|
|
|
|
+ const { books } = await fetchJson<{ books: ReadonlyArray<BookRef> }>("/books");
|
|
|
|
|
+ const target = resolveDirectWriteTarget(activeBookId, books);
|
|
|
|
|
+
|
|
|
|
|
+ if (target.bookId) {
|
|
|
|
|
+ setMessages((prev) => [...prev, {
|
|
|
|
|
+ role: "assistant",
|
|
|
|
|
+ content: isZh ? `⋯ 开始处理《${target.bookId}》...` : `⋯ Starting ${target.bookId}...`,
|
|
|
|
|
+ timestamp: Date.now(),
|
|
|
|
|
+ }]);
|
|
|
|
|
+ await postApi(`/books/${target.bookId}/write-next`, {});
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ setMessages((prev) => [...prev, {
|
|
|
|
|
+ role: "assistant",
|
|
|
|
|
+ content:
|
|
|
|
|
+ target.reason === "missing"
|
|
|
|
|
+ ? (isZh ? "✗ 还没有书,先创建一本再写。" : "✗ No books yet. Create one first.")
|
|
|
|
|
+ : (isZh ? "✗ 当前有多本书,请先打开目标书籍后再执行\u201C写下一章\u201D。" : '✗ Multiple books found. Open the target book first, then run "write next".'),
|
|
|
|
|
+ timestamp: Date.now(),
|
|
|
|
|
+ }]);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await fetchJson<{
|
|
|
|
|
+ response?: string;
|
|
|
|
|
+ error?: string;
|
|
|
|
|
+ session?: {
|
|
|
|
|
+ activeBookId?: string;
|
|
|
|
|
+ automationMode?: string;
|
|
|
|
|
+ creationDraft?: {
|
|
|
|
|
+ title?: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ currentExecution?: {
|
|
|
|
|
+ status?: string;
|
|
|
|
|
+ stageLabel?: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ pendingDecision?: {
|
|
|
|
|
+ summary?: string;
|
|
|
|
|
+ };
|
|
|
|
|
+ messages?: ReadonlyArray<{ role: "user" | "assistant" | "system"; content: string; timestamp: number }>;
|
|
|
|
|
+ };
|
|
|
|
|
+ }>("/agent", {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
|
|
+ body: JSON.stringify({ instruction: command, activeBookId }),
|
|
|
|
|
+ });
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ if (data.session) {
|
|
|
|
|
+ setSessionMeta({
|
|
|
|
|
+ activeBookId: data.session.activeBookId ?? activeBookId,
|
|
|
|
|
+ draftTitle: data.session.creationDraft?.title,
|
|
|
|
|
+ automationMode: data.session.automationMode,
|
|
|
|
|
+ currentStage: data.session.currentExecution?.stageLabel ?? data.session.currentExecution?.status,
|
|
|
|
|
+ pendingSummary: data.session.pendingDecision?.summary,
|
|
|
|
|
+ });
|
|
|
|
|
+ const restored = coerceSharedSessionMessages(data.session.messages ?? []);
|
|
|
|
|
+ if (restored.length > 0) {
|
|
|
|
|
+ setMessages(restored);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ setMessages((prev) => [...prev, {
|
|
|
|
|
+ role: "assistant",
|
|
|
|
|
+ content: data.response ?? data.error ?? "Acknowledged.",
|
|
|
|
|
+ timestamp: Date.now(),
|
|
|
|
|
+ }]);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ setMessages((prev) => [...prev, {
|
|
|
|
|
+ role: "assistant",
|
|
|
|
|
+ content: `✗ ${e instanceof Error ? e.message : String(e)}`,
|
|
|
|
|
+ timestamp: Date.now(),
|
|
|
|
|
+ }]);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const isZh = t("nav.connected") === "已连接";
|
|
const isZh = t("nav.connected") === "已连接";
|
|
@@ -537,8 +617,8 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
|
|
|
>
|
|
>
|
|
|
{messages.length === 0 && !loading && <EmptyState />}
|
|
{messages.length === 0 && !loading && <EmptyState />}
|
|
|
|
|
|
|
|
- {messages.map((msg) => (
|
|
|
|
|
- <MessageBubble key={msg.timestamp} msg={msg} />
|
|
|
|
|
|
|
+ {messages.map((msg, i) => (
|
|
|
|
|
+ <MessageBubble key={`${msg.timestamp}-${i}`} msg={msg} />
|
|
|
))}
|
|
))}
|
|
|
|
|
|
|
|
{loading && !messages.some((m) => m.content.startsWith("⋯")) && (
|
|
{loading && !messages.some((m) => m.content.startsWith("⋯")) && (
|
|
@@ -551,22 +631,22 @@ export function ChatPanel({ open, onClose, t, sse, activeBookId }: {
|
|
|
<QuickChip
|
|
<QuickChip
|
|
|
icon={<Zap size={11} />}
|
|
icon={<Zap size={11} />}
|
|
|
label={t("dash.writeNext")}
|
|
label={t("dash.writeNext")}
|
|
|
- onClick={() => handleQuickCommand(isZh ? "写下一章" : "write next")}
|
|
|
|
|
|
|
+ onClick={() => void handleQuickCommand(isZh ? "写下一章" : "write next")}
|
|
|
/>
|
|
/>
|
|
|
<QuickChip
|
|
<QuickChip
|
|
|
icon={<Search size={11} />}
|
|
icon={<Search size={11} />}
|
|
|
label={t("book.audit")}
|
|
label={t("book.audit")}
|
|
|
- onClick={() => handleQuickCommand(isZh ? "审计第1章" : "audit chapter 1")}
|
|
|
|
|
|
|
+ onClick={() => void handleQuickCommand(isZh ? "审计第1章" : "audit chapter 1")}
|
|
|
/>
|
|
/>
|
|
|
<QuickChip
|
|
<QuickChip
|
|
|
icon={<FileOutput size={11} />}
|
|
icon={<FileOutput size={11} />}
|
|
|
label={t("book.export")}
|
|
label={t("book.export")}
|
|
|
- onClick={() => handleQuickCommand(isZh ? "导出全书" : "export book as epub")}
|
|
|
|
|
|
|
+ onClick={() => void handleQuickCommand(isZh ? "导出全书" : "export book as epub")}
|
|
|
/>
|
|
/>
|
|
|
<QuickChip
|
|
<QuickChip
|
|
|
icon={<TrendingUp size={11} />}
|
|
icon={<TrendingUp size={11} />}
|
|
|
label={t("nav.radar")}
|
|
label={t("nav.radar")}
|
|
|
- onClick={() => handleQuickCommand(isZh ? "扫描市场趋势" : "scan market trends")}
|
|
|
|
|
|
|
+ onClick={() => void handleQuickCommand(isZh ? "扫描市场趋势" : "scan market trends")}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|