|
|
@@ -1,5 +1,6 @@
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
import type { BookCreationDraft } from "@actalk/inkos-core";
|
|
|
+import { BookPlus, CheckCircle2, RotateCcw, Sparkles } from "lucide-react";
|
|
|
import { fetchJson, useApi } from "../hooks/use-api";
|
|
|
import type { Theme } from "../hooks/use-theme";
|
|
|
import type { TFunction } from "../hooks/use-i18n";
|
|
|
@@ -15,18 +16,30 @@ interface Nav {
|
|
|
toBook: (id: string) => void;
|
|
|
}
|
|
|
|
|
|
-interface GenreInfo {
|
|
|
- readonly id: string;
|
|
|
- readonly name: string;
|
|
|
- readonly source: "project" | "builtin";
|
|
|
- readonly language: "zh" | "en";
|
|
|
-}
|
|
|
-
|
|
|
interface PlatformOption {
|
|
|
readonly value: string;
|
|
|
readonly label: string;
|
|
|
}
|
|
|
|
|
|
+export interface BookCreateFormState {
|
|
|
+ readonly title: string;
|
|
|
+ readonly genre: string;
|
|
|
+ readonly platform: string;
|
|
|
+ readonly targetChapters: string;
|
|
|
+ readonly chapterWordCount: string;
|
|
|
+ readonly brief: string;
|
|
|
+}
|
|
|
+
|
|
|
+export interface BookCreatePayload {
|
|
|
+ readonly title: string;
|
|
|
+ readonly genre: string;
|
|
|
+ readonly platform: string;
|
|
|
+ readonly language: "zh" | "en";
|
|
|
+ readonly targetChapters: number;
|
|
|
+ readonly chapterWordCount: number;
|
|
|
+ readonly blurb: string;
|
|
|
+}
|
|
|
+
|
|
|
export interface DraftSummaryRow {
|
|
|
readonly key: string;
|
|
|
readonly label: string;
|
|
|
@@ -61,6 +74,24 @@ interface SessionResponse {
|
|
|
interface PlatformCopy {
|
|
|
readonly idleTitle: string;
|
|
|
readonly idleBody: string;
|
|
|
+ readonly formHeading: string;
|
|
|
+ readonly formHint: string;
|
|
|
+ readonly titleLabel: string;
|
|
|
+ readonly titlePlaceholder: string;
|
|
|
+ readonly genreLabel: string;
|
|
|
+ readonly genrePlaceholder: string;
|
|
|
+ readonly platformLabel: string;
|
|
|
+ readonly targetChaptersLabel: string;
|
|
|
+ readonly chapterWordCountLabel: string;
|
|
|
+ readonly briefLabel: string;
|
|
|
+ readonly briefPlaceholder: string;
|
|
|
+ readonly createBook: string;
|
|
|
+ readonly creatingBook: string;
|
|
|
+ readonly creationStatus: string;
|
|
|
+ readonly creationSteps: ReadonlyArray<string>;
|
|
|
+ readonly assistantHeading: string;
|
|
|
+ readonly assistantHint: string;
|
|
|
+ readonly applyDraft: string;
|
|
|
readonly promptLabel: string;
|
|
|
readonly promptPlaceholder: string;
|
|
|
readonly promptPlaceholderFollowup: string;
|
|
|
@@ -94,7 +125,25 @@ const PLATFORMS_EN: ReadonlyArray<PlatformOption> = [
|
|
|
const PAGE_COPY: Record<"zh" | "en", PlatformCopy> = {
|
|
|
zh: {
|
|
|
idleTitle: "从一句模糊想法开始",
|
|
|
- idleBody: "直接描述题材、世界观、主角、核心冲突,或告诉我你想先改哪一块。共享草案会在 TUI 和 Studio Chat 之间同步。",
|
|
|
+ idleBody: "先填清楚书名、题材和故事核心,系统会生成基础设定并进入新书工作台。",
|
|
|
+ formHeading: "书籍基础信息",
|
|
|
+ formHint: "这些字段会直接进入建书流程。简介写得越具体,后续基础设定越稳定。",
|
|
|
+ titleLabel: "书名",
|
|
|
+ titlePlaceholder: "例如:夜港账本",
|
|
|
+ genreLabel: "题材 / 类型",
|
|
|
+ genrePlaceholder: "例如:都市悬疑、玄幻、科幻、女频情感",
|
|
|
+ platformLabel: "目标平台",
|
|
|
+ targetChaptersLabel: "目标章数",
|
|
|
+ chapterWordCountLabel: "每章字数",
|
|
|
+ briefLabel: "故事简介 / 核心设定",
|
|
|
+ briefPlaceholder: "写清世界观、主角、目标、核心冲突和第一阶段方向。例如:近未来港口城,主角是水货账房,想洗白却被旧账拖回港口旧案。",
|
|
|
+ createBook: "创建书籍",
|
|
|
+ creatingBook: "创建中…",
|
|
|
+ creationStatus: "正在创建书籍,完成后会自动进入工作台。",
|
|
|
+ creationSteps: ["写入书籍配置", "生成基础设定", "准备工作台"],
|
|
|
+ assistantHeading: "需要先让 AI 帮你补设定?",
|
|
|
+ assistantHint: "这块是辅助草案,不是必须步骤。已有草案可以一键套用到左侧表单。",
|
|
|
+ applyDraft: "套用草案",
|
|
|
promptLabel: "继续打磨这本书",
|
|
|
promptPlaceholder: "例如:我想写个港风商战悬疑,主角先做灰产再洗白。",
|
|
|
promptPlaceholderFollowup: "例如:世界观改成近未来港口城;女主不要太早出场;卷一先查账再砸场。",
|
|
|
@@ -103,7 +152,7 @@ const PAGE_COPY: Record<"zh" | "en", PlatformCopy> = {
|
|
|
create: "按当前草案建书",
|
|
|
creating: "创建中…",
|
|
|
discard: "丢弃草案",
|
|
|
- draftHeading: "当前 foundation 草案",
|
|
|
+ draftHeading: "当前基础设定草案",
|
|
|
missingHeading: "还缺这些关键信息",
|
|
|
missingHint: "这些字段未必都要一次填满,但缺得太多时不要急着建书。",
|
|
|
syncedHint: "这份草案和 TUI / Studio Chat 共享。",
|
|
|
@@ -112,7 +161,25 @@ const PAGE_COPY: Record<"zh" | "en", PlatformCopy> = {
|
|
|
},
|
|
|
en: {
|
|
|
idleTitle: "Start from a rough idea",
|
|
|
- idleBody: "Describe the genre, world, protagonist, and core conflict. The shared draft stays in sync across TUI and Studio Chat.",
|
|
|
+ idleBody: "Fill in the title, genre, and story core first. InkOS will generate the foundation and open the new workspace.",
|
|
|
+ formHeading: "Book basics",
|
|
|
+ formHint: "These fields go straight into creation. A concrete brief gives the foundation generator better material.",
|
|
|
+ titleLabel: "Title",
|
|
|
+ titlePlaceholder: "Example: Ledger of the Night Port",
|
|
|
+ genreLabel: "Genre",
|
|
|
+ genrePlaceholder: "Example: mystery, urban fantasy, sci-fi, romance",
|
|
|
+ platformLabel: "Target platform",
|
|
|
+ targetChaptersLabel: "Target chapters",
|
|
|
+ chapterWordCountLabel: "Words per chapter",
|
|
|
+ briefLabel: "Story brief / core premise",
|
|
|
+ briefPlaceholder: "Include the world, protagonist, goal, core conflict, and first arc direction.",
|
|
|
+ createBook: "Create book",
|
|
|
+ creatingBook: "Creating…",
|
|
|
+ creationStatus: "Creating the book. The workspace will open automatically when it is ready.",
|
|
|
+ creationSteps: ["Saving config", "Generating foundation", "Preparing workspace"],
|
|
|
+ assistantHeading: "Want AI to shape the idea first?",
|
|
|
+ assistantHint: "This draft area is optional. If a draft looks useful, apply it to the form.",
|
|
|
+ applyDraft: "Apply draft",
|
|
|
promptLabel: "Refine this book",
|
|
|
promptPlaceholder: "Example: I want a harbor-noir business thriller about a fixer trying to go legit.",
|
|
|
promptPlaceholderFollowup: "Example: move the world to a near-future port city; delay the heroine; make volume one about chasing ledgers first.",
|
|
|
@@ -141,10 +208,56 @@ export function defaultChapterWordsForLanguage(language: "zh" | "en"): string {
|
|
|
return language === "en" ? "2000" : "3000";
|
|
|
}
|
|
|
|
|
|
+export function defaultBookCreateForm(language: "zh" | "en"): BookCreateFormState {
|
|
|
+ return {
|
|
|
+ title: "",
|
|
|
+ genre: "",
|
|
|
+ platform: platformOptionsForLanguage(language)[0]?.value ?? "other",
|
|
|
+ targetChapters: "200",
|
|
|
+ chapterWordCount: defaultChapterWordsForLanguage(language),
|
|
|
+ brief: "",
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
export function platformOptionsForLanguage(language: "zh" | "en"): ReadonlyArray<PlatformOption> {
|
|
|
return language === "en" ? PLATFORMS_EN : PLATFORMS_ZH;
|
|
|
}
|
|
|
|
|
|
+function parsePositiveInteger(value: string): number | null {
|
|
|
+ const parsed = Number.parseInt(value.trim(), 10);
|
|
|
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
|
+}
|
|
|
+
|
|
|
+export function isBookCreateFormReady(form: BookCreateFormState): boolean {
|
|
|
+ return Boolean(
|
|
|
+ form.title.trim()
|
|
|
+ && form.genre.trim()
|
|
|
+ && form.brief.trim()
|
|
|
+ && parsePositiveInteger(form.targetChapters)
|
|
|
+ && parsePositiveInteger(form.chapterWordCount),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export function buildBookCreatePayload(
|
|
|
+ form: BookCreateFormState,
|
|
|
+ language: "zh" | "en",
|
|
|
+): BookCreatePayload {
|
|
|
+ const targetChapters = parsePositiveInteger(form.targetChapters);
|
|
|
+ const chapterWordCount = parsePositiveInteger(form.chapterWordCount);
|
|
|
+ if (!targetChapters || !chapterWordCount || !isBookCreateFormReady(form)) {
|
|
|
+ throw new Error(language === "zh" ? "请先补齐建书表单。" : "Complete the book creation form first.");
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ title: form.title.trim(),
|
|
|
+ genre: form.genre.trim(),
|
|
|
+ platform: form.platform,
|
|
|
+ language,
|
|
|
+ targetChapters,
|
|
|
+ chapterWordCount,
|
|
|
+ blurb: form.brief.trim(),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
export function resolveDraftInstruction(input: string, hasDraft: boolean): string {
|
|
|
const trimmed = input.trim();
|
|
|
if (!trimmed) {
|
|
|
@@ -339,8 +452,10 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
const { data: project } = useApi<{ language: string }>("/project");
|
|
|
const projectLang = (project?.language ?? "zh") as "zh" | "en";
|
|
|
const copy = PAGE_COPY[projectLang];
|
|
|
+ const platformChoices = platformOptionsForLanguage(projectLang);
|
|
|
|
|
|
const [draft, setDraft] = useState<BookCreationDraft | undefined>();
|
|
|
+ const [form, setForm] = useState<BookCreateFormState>(() => defaultBookCreateForm(projectLang));
|
|
|
const [input, setInput] = useState("");
|
|
|
const [loadingDraft, setLoadingDraft] = useState(true);
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
@@ -353,6 +468,45 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
() => (draft ? buildCreationDraftSummary(draft, projectLang) : []),
|
|
|
[draft, projectLang],
|
|
|
);
|
|
|
+ const canSubmitForm = isBookCreateFormReady(form);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ setForm((current) => ({
|
|
|
+ ...current,
|
|
|
+ platform: pickValidValue(
|
|
|
+ current.platform,
|
|
|
+ platformOptionsForLanguage(projectLang).map((option) => option.value),
|
|
|
+ ),
|
|
|
+ chapterWordCount: current.chapterWordCount || defaultChapterWordsForLanguage(projectLang),
|
|
|
+ targetChapters: current.targetChapters || "200",
|
|
|
+ }));
|
|
|
+ }, [projectLang]);
|
|
|
+
|
|
|
+ const updateForm = (patch: Partial<BookCreateFormState>) => {
|
|
|
+ setForm((current) => ({ ...current, ...patch }));
|
|
|
+ };
|
|
|
+
|
|
|
+ const applyDraftToForm = () => {
|
|
|
+ if (!draft) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const draftBrief = [
|
|
|
+ draft.blurb,
|
|
|
+ draft.worldPremise,
|
|
|
+ draft.protagonist,
|
|
|
+ draft.conflictCore,
|
|
|
+ draft.volumeOutline,
|
|
|
+ ].filter((part): part is string => Boolean(part?.trim())).join("\n\n");
|
|
|
+ const platformValues = platformChoices.map((option) => option.value);
|
|
|
+ setForm((current) => ({
|
|
|
+ title: draft.title?.trim() || current.title,
|
|
|
+ genre: draft.genre?.trim() || current.genre,
|
|
|
+ platform: pickValidValue(draft.platform ?? current.platform, platformValues),
|
|
|
+ targetChapters: draft.targetChapters ? String(draft.targetChapters) : current.targetChapters,
|
|
|
+ chapterWordCount: draft.chapterWordCount ? String(draft.chapterWordCount) : current.chapterWordCount,
|
|
|
+ brief: draftBrief || current.brief,
|
|
|
+ }));
|
|
|
+ };
|
|
|
|
|
|
const refreshDraft = async (): Promise<BookCreationDraft | undefined> => {
|
|
|
const data = await fetchJson<InteractionSessionResponse>("/interaction/session");
|
|
|
@@ -442,6 +596,34 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleFormCreate = async () => {
|
|
|
+ if (!canSubmitForm) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setCreating(true);
|
|
|
+ setError(null);
|
|
|
+ setStatus(copy.creationStatus);
|
|
|
+ try {
|
|
|
+ const payload = buildBookCreatePayload(form, projectLang);
|
|
|
+ const data = await fetchJson<{ status?: string; bookId?: string }>("/books/create", {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
+ body: JSON.stringify(payload),
|
|
|
+ });
|
|
|
+ if (!data.bookId) {
|
|
|
+ throw new Error(projectLang === "zh" ? "创建请求没有返回书籍 ID。" : "Create request did not return a book id.");
|
|
|
+ }
|
|
|
+ await waitForBookReady(data.bookId);
|
|
|
+ nav.toBook(data.bookId);
|
|
|
+ } catch (cause) {
|
|
|
+ setError(cause instanceof Error ? cause.message : String(cause));
|
|
|
+ setStatus(null);
|
|
|
+ } finally {
|
|
|
+ setCreating(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const handleCreate = async () => {
|
|
|
if (!canCreateFromDraft(draft)) {
|
|
|
return;
|
|
|
@@ -483,7 +665,7 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <div className="max-w-4xl mx-auto space-y-8">
|
|
|
+ <div className="max-w-5xl mx-auto space-y-8">
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
<button onClick={nav.toDashboard} className={c.link}>{t("bread.books")}</button>
|
|
|
<span className="text-border">/</span>
|
|
|
@@ -491,8 +673,8 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
- <h1 className="font-serif text-3xl">{t("create.title")}</h1>
|
|
|
- <p className="text-sm text-muted-foreground leading-7">{copy.idleBody}</p>
|
|
|
+ <h1 className="font-serif text-4xl">{t("create.title")}</h1>
|
|
|
+ <p className="text-sm text-muted-foreground leading-7 max-w-2xl">{copy.idleBody}</p>
|
|
|
</div>
|
|
|
|
|
|
{error && (
|
|
|
@@ -507,28 +689,158 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- <div className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
|
|
- <section className="space-y-4">
|
|
|
- <div className="rounded-2xl border border-border/60 bg-card/70 p-5 space-y-4">
|
|
|
+ <div className="grid gap-6 lg:grid-cols-[minmax(0,1.25fr)_minmax(300px,0.75fr)]">
|
|
|
+ <section className="rounded-lg border border-border/60 bg-card/80 p-5 space-y-5">
|
|
|
+ <div className="space-y-1">
|
|
|
+ <div className="text-[11px] uppercase text-muted-foreground font-bold">
|
|
|
+ {copy.formHeading}
|
|
|
+ </div>
|
|
|
+ <p className="text-xs text-muted-foreground leading-6">{copy.formHint}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-2">
|
|
|
+ <label className="space-y-2">
|
|
|
+ <span className="text-xs font-medium text-muted-foreground">{copy.titleLabel}</span>
|
|
|
+ <input
|
|
|
+ value={form.title}
|
|
|
+ onChange={(event) => updateForm({ title: event.target.value })}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-2.5 focus:outline-none text-sm`}
|
|
|
+ placeholder={copy.titlePlaceholder}
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ <label className="space-y-2">
|
|
|
+ <span className="text-xs font-medium text-muted-foreground">{copy.genreLabel}</span>
|
|
|
+ <input
|
|
|
+ value={form.genre}
|
|
|
+ onChange={(event) => updateForm({ genre: event.target.value })}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-2.5 focus:outline-none text-sm`}
|
|
|
+ placeholder={copy.genrePlaceholder}
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid gap-4 sm:grid-cols-3">
|
|
|
+ <label className="space-y-2">
|
|
|
+ <span className="text-xs font-medium text-muted-foreground">{copy.platformLabel}</span>
|
|
|
+ <select
|
|
|
+ value={form.platform}
|
|
|
+ onChange={(event) => updateForm({ platform: event.target.value })}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-2.5 focus:outline-none text-sm bg-background`}
|
|
|
+ >
|
|
|
+ {platformChoices.map((option) => (
|
|
|
+ <option key={option.value} value={option.value}>{option.label}</option>
|
|
|
+ ))}
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ <label className="space-y-2">
|
|
|
+ <span className="text-xs font-medium text-muted-foreground">{copy.targetChaptersLabel}</span>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ min={1}
|
|
|
+ value={form.targetChapters}
|
|
|
+ onChange={(event) => updateForm({ targetChapters: event.target.value })}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-2.5 focus:outline-none text-sm`}
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ <label className="space-y-2">
|
|
|
+ <span className="text-xs font-medium text-muted-foreground">{copy.chapterWordCountLabel}</span>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ min={1000}
|
|
|
+ value={form.chapterWordCount}
|
|
|
+ onChange={(event) => updateForm({ chapterWordCount: event.target.value })}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-2.5 focus:outline-none text-sm`}
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <label className="space-y-2 block">
|
|
|
+ <span className="text-xs font-medium text-muted-foreground">{copy.briefLabel}</span>
|
|
|
+ <textarea
|
|
|
+ value={form.brief}
|
|
|
+ onChange={(event) => updateForm({ brief: event.target.value })}
|
|
|
+ rows={9}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-3 focus:outline-none text-sm leading-7 resize-y`}
|
|
|
+ placeholder={copy.briefPlaceholder}
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+
|
|
|
+ {creating && (
|
|
|
+ <div className="grid gap-2 sm:grid-cols-3">
|
|
|
+ {copy.creationSteps.map((step) => (
|
|
|
+ <div key={step} className="flex items-center gap-2 rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-xs text-primary">
|
|
|
+ <CheckCircle2 size={14} />
|
|
|
+ <span>{step}</span>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <button
|
|
|
+ onClick={handleFormCreate}
|
|
|
+ disabled={!canSubmitForm || creating || submitting}
|
|
|
+ className={`inline-flex items-center gap-2 px-5 py-3 ${c.btnPrimary} rounded-md disabled:opacity-50 font-medium text-sm`}
|
|
|
+ >
|
|
|
+ <BookPlus size={16} />
|
|
|
+ {creating ? copy.creatingBook : copy.createBook}
|
|
|
+ </button>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <aside className="space-y-4">
|
|
|
+ <section className="rounded-lg border border-border/60 bg-card/80 p-5 space-y-4">
|
|
|
<div className="space-y-1">
|
|
|
- <div className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-bold">
|
|
|
- {copy.draftHeading}
|
|
|
+ <div className="text-[11px] uppercase text-muted-foreground font-bold">
|
|
|
+ {copy.assistantHeading}
|
|
|
</div>
|
|
|
- <div className="text-xs text-muted-foreground">
|
|
|
- {copy.syncedHint}
|
|
|
+ <p className="text-xs text-muted-foreground leading-6">{copy.assistantHint}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <textarea
|
|
|
+ value={input}
|
|
|
+ onChange={(event) => setInput(event.target.value)}
|
|
|
+ rows={7}
|
|
|
+ className={`w-full ${c.input} rounded-md px-3 py-3 focus:outline-none text-sm leading-7 resize-y`}
|
|
|
+ placeholder={draft ? copy.promptPlaceholderFollowup : copy.promptPlaceholder}
|
|
|
+ />
|
|
|
+
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ <button
|
|
|
+ onClick={handleDraftSubmit}
|
|
|
+ disabled={submitting || creating || !input.trim()}
|
|
|
+ className={`inline-flex items-center gap-2 px-3 py-2 ${c.btnPrimary} rounded-md disabled:opacity-50 font-medium text-xs`}
|
|
|
+ >
|
|
|
+ <Sparkles size={14} />
|
|
|
+ {submitting ? copy.submitting : copy.submit}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={handleDiscard}
|
|
|
+ disabled={!draft || submitting || creating}
|
|
|
+ className="inline-flex items-center gap-2 px-3 py-2 rounded-md border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 disabled:opacity-50 font-medium text-xs"
|
|
|
+ >
|
|
|
+ <RotateCcw size={14} />
|
|
|
+ {copy.discard}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section className="rounded-lg border border-border/60 bg-card/80 p-5 space-y-4">
|
|
|
+ <div className="space-y-1">
|
|
|
+ <div className="text-[11px] uppercase text-muted-foreground font-bold">
|
|
|
+ {copy.draftHeading}
|
|
|
</div>
|
|
|
+ <p className="text-xs text-muted-foreground leading-6">{copy.syncedHint}</p>
|
|
|
</div>
|
|
|
|
|
|
{loadingDraft ? (
|
|
|
- <div className="text-sm text-muted-foreground">{projectLang === "zh" ? "读取共享草案中…" : "Loading shared draft…"}</div>
|
|
|
+ <div className="text-sm text-muted-foreground">{projectLang === "zh" ? "读取草案中…" : "Loading draft…"}</div>
|
|
|
) : draft ? (
|
|
|
<div className="space-y-4">
|
|
|
{summaryRows.length > 0 ? (
|
|
|
- <div className="space-y-3">
|
|
|
+ <div className="space-y-2">
|
|
|
{summaryRows.map((row) => (
|
|
|
- <div key={row.key} className="rounded-xl border border-border/50 bg-background/70 px-4 py-3">
|
|
|
- <div className="text-[10px] uppercase tracking-[0.18em] text-muted-foreground font-semibold">{row.label}</div>
|
|
|
- <div className="mt-1 text-sm leading-7 whitespace-pre-wrap">{row.value}</div>
|
|
|
+ <div key={row.key} className="rounded-md border border-border/50 bg-background/70 px-3 py-2">
|
|
|
+ <div className="text-[10px] uppercase text-muted-foreground font-semibold">{row.label}</div>
|
|
|
+ <div className="mt-1 text-sm leading-6 whitespace-pre-wrap">{row.value}</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
@@ -541,71 +853,42 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
{draft.missingFields.map((field) => (
|
|
|
<span
|
|
|
key={field}
|
|
|
- className="rounded-full border border-border/70 bg-secondary/50 px-3 py-1 text-xs text-muted-foreground"
|
|
|
+ className="rounded-md border border-border/70 bg-secondary/50 px-2 py-1 text-xs text-muted-foreground"
|
|
|
>
|
|
|
{field}
|
|
|
</span>
|
|
|
))}
|
|
|
</div>
|
|
|
- <p className="text-xs text-muted-foreground">{copy.missingHint}</p>
|
|
|
+ <p className="text-xs text-muted-foreground leading-6">{copy.missingHint}</p>
|
|
|
</div>
|
|
|
) : null}
|
|
|
+
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ <button
|
|
|
+ onClick={applyDraftToForm}
|
|
|
+ className="px-3 py-2 rounded-md border border-border bg-secondary text-secondary-foreground font-medium text-xs"
|
|
|
+ >
|
|
|
+ {copy.applyDraft}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={handleCreate}
|
|
|
+ disabled={!canCreateFromDraft(draft) || creating || submitting}
|
|
|
+ className="px-3 py-2 rounded-md border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 disabled:opacity-50 font-medium text-xs"
|
|
|
+ >
|
|
|
+ {creating ? copy.creating : copy.create}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
) : (
|
|
|
- <div className="rounded-xl border border-dashed border-border/70 bg-background/50 px-5 py-6">
|
|
|
+ <div className="rounded-md border border-dashed border-border/70 bg-background/50 px-4 py-5">
|
|
|
<div className="font-medium">{copy.idleTitle}</div>
|
|
|
<p className="mt-2 text-sm text-muted-foreground leading-7">
|
|
|
{copy.helperBody}
|
|
|
</p>
|
|
|
</div>
|
|
|
)}
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section className="space-y-4">
|
|
|
- <div className="rounded-2xl border border-border/60 bg-card/70 p-5 space-y-4">
|
|
|
- <div className="space-y-1">
|
|
|
- <div className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground font-bold">
|
|
|
- {copy.promptLabel}
|
|
|
- </div>
|
|
|
- <div className="text-xs text-muted-foreground">
|
|
|
- {copy.helperTitle}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <textarea
|
|
|
- value={input}
|
|
|
- onChange={(event) => setInput(event.target.value)}
|
|
|
- rows={10}
|
|
|
- className={`w-full ${c.input} rounded-xl px-4 py-3 focus:outline-none text-sm leading-7 resize-y`}
|
|
|
- placeholder={draft ? copy.promptPlaceholderFollowup : copy.promptPlaceholder}
|
|
|
- />
|
|
|
-
|
|
|
- <div className="flex flex-wrap gap-3">
|
|
|
- <button
|
|
|
- onClick={handleDraftSubmit}
|
|
|
- disabled={submitting || creating || !input.trim()}
|
|
|
- className={`px-4 py-3 ${c.btnPrimary} rounded-md disabled:opacity-50 font-medium text-sm`}
|
|
|
- >
|
|
|
- {submitting ? copy.submitting : copy.submit}
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={handleCreate}
|
|
|
- disabled={!canCreateFromDraft(draft) || creating || submitting}
|
|
|
- className={`px-4 py-3 rounded-md border border-border bg-secondary text-secondary-foreground disabled:opacity-50 font-medium text-sm`}
|
|
|
- >
|
|
|
- {creating ? copy.creating : copy.create}
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={handleDiscard}
|
|
|
- disabled={!draft || submitting || creating}
|
|
|
- className="px-4 py-3 rounded-md border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 disabled:opacity-50 font-medium text-sm"
|
|
|
- >
|
|
|
- {copy.discard}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
+ </section>
|
|
|
+ </aside>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|