feat: improve app ui and project metadata
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { ArrowLeft, Database, Sparkles } from "lucide-react";
|
||||
|
||||
import { useToast } from "@/components/Toast";
|
||||
import { ConflictAdjudication } from "@/components/generation/ConflictAdjudication";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Field } from "@/components/ui/Field";
|
||||
import { TextArea } from "@/components/ui/TextArea";
|
||||
import { TextInput } from "@/components/ui/TextInput";
|
||||
import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types";
|
||||
import {
|
||||
buildGenerateRequest,
|
||||
@@ -115,25 +121,30 @@ export function GeneratorRunner({
|
||||
|
||||
return (
|
||||
<section
|
||||
className="flex flex-col gap-4 rounded border border-line bg-panel p-5"
|
||||
className="flex flex-col gap-5 rounded border border-line bg-panel p-5 shadow-paper"
|
||||
aria-label={tool.title}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="font-serif text-lg text-ink">{tool.title}</h2>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<h2 className="font-serif text-lg text-ink">{tool.title}</h2>
|
||||
{tool.ingestable ? (
|
||||
<Badge variant="info">
|
||||
<Database className="h-3 w-3" aria-hidden="true" />
|
||||
可入库
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-ink-soft">{tool.subtitle}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded border border-line px-3 py-1 text-sm text-ink-soft hover:text-ink"
|
||||
>
|
||||
<Button onClick={onClose} variant="secondary" size="sm">
|
||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||
返回
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
className="grid gap-3 rounded border border-line bg-bg/50 p-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void onGenerate();
|
||||
@@ -148,13 +159,10 @@ export function GeneratorRunner({
|
||||
onChange={(v) => setField(field.name, v)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={generating}
|
||||
className="self-start rounded bg-cinnabar px-4 py-2 text-sm text-white disabled:opacity-50"
|
||||
>
|
||||
<Button type="submit" disabled={generating} variant="primary">
|
||||
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
||||
{generating ? "生成中…" : "生成"}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{gen.genStatus === "preview" && gen.preview ? (
|
||||
@@ -181,18 +189,18 @@ export function GeneratorRunner({
|
||||
</ul>
|
||||
|
||||
{canIngest && gen.ingestStatus !== "conflict" ? (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
onClick={() => void runIngest(false)}
|
||||
disabled={ingesting || (!singleObject && selected.size === 0)}
|
||||
className="self-start rounded border border-cinnabar px-4 py-2 text-sm text-cinnabar disabled:opacity-50"
|
||||
variant="outline"
|
||||
>
|
||||
<Database className="h-4 w-4" aria-hidden="true" />
|
||||
{ingesting
|
||||
? "入库中…"
|
||||
: singleObject
|
||||
? `入库为规则至 ${table}`
|
||||
: `入库选中(${selected.size})至 ${table}`}
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{gen.ingestStatus === "conflict" && gen.conflicts ? (
|
||||
@@ -223,33 +231,24 @@ interface FormFieldProps {
|
||||
|
||||
// 声明式控件映射:type → text / textarea / number(select 暂同 text,后端未给 options)。
|
||||
function FormField({ field, value, onChange }: FormFieldProps) {
|
||||
const labelText = field.required ? `${field.label} *` : field.label;
|
||||
const common =
|
||||
"mt-1 w-full rounded border border-line bg-bg px-3 py-2 text-sm text-ink";
|
||||
return (
|
||||
<label className="block text-sm text-ink-soft">
|
||||
{labelText}
|
||||
<Field label={field.label} help={field.help} required={field.required}>
|
||||
{field.type === "textarea" ? (
|
||||
<textarea
|
||||
<TextArea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={3}
|
||||
className={`${common} resize-y`}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
<TextInput
|
||||
type={field.type === "number" ? "number" : "text"}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={common}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
)}
|
||||
{field.help ? (
|
||||
<span className="mt-1 block text-xs text-ink-soft/80">{field.help}</span>
|
||||
) : null}
|
||||
</label>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowRight, Database, Sparkles } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import type { ToolDescriptorView } from "@/lib/api/types";
|
||||
import { cardClass } from "@/lib/ui/variants";
|
||||
|
||||
interface ToolCardProps {
|
||||
tool: ToolDescriptorView;
|
||||
@@ -15,27 +19,32 @@ export function ToolCard({ tool, onOpen }: ToolCardProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpen(tool)}
|
||||
className="flex h-full min-h-[140px] flex-col rounded border border-line bg-panel p-5 text-left shadow-paper transition-colors hover:border-cinnabar focus-visible:border-cinnabar focus-visible:outline-none"
|
||||
className={cardClass(
|
||||
"group flex h-full min-h-[164px] flex-col p-5 text-left transition-colors hover:border-cinnabar focus-visible:border-cinnabar focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cinnabar/30",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
<h2 className="font-serif text-xl text-ink">{tool.title}</h2>
|
||||
{tool.genre ? (
|
||||
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
|
||||
{tool.genre}
|
||||
</span>
|
||||
) : null}
|
||||
{tool.is_legacy ? null : (
|
||||
<span className="rounded bg-[var(--color-cinnabar-wash)] px-2 py-0.5 text-xs text-cinnabar">
|
||||
NEW
|
||||
</span>
|
||||
)}
|
||||
{tool.genre ? <Badge>{tool.genre}</Badge> : null}
|
||||
{tool.is_legacy ? null : <Badge variant="accent">新</Badge>}
|
||||
{tool.ingestable ? (
|
||||
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
|
||||
<Badge variant="info">
|
||||
<Database className="h-3 w-3" aria-hidden="true" />
|
||||
可入库
|
||||
</span>
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="line-clamp-2 text-sm text-ink-soft">{tool.subtitle}</p>
|
||||
<div className="mt-auto flex items-center gap-2 pt-4 text-xs text-ink-soft">
|
||||
<Sparkles className="h-3.5 w-3.5 text-cinnabar" aria-hidden="true" />
|
||||
<span className="truncate">
|
||||
{tool.input_fields?.[0]?.label ?? "现有页面"} → 结构化预览
|
||||
</span>
|
||||
<ArrowRight
|
||||
className="ml-auto h-3.5 w-3.5 text-cinnabar opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { PageHeader } from "@/components/ui/PageHeader";
|
||||
import type { ProjectResponse, ToolDescriptorView } from "@/lib/api/types";
|
||||
import { isLegacyTool, resolveLegacyRoute } from "@/lib/toolbox/toolbox";
|
||||
import { ToolCard } from "./ToolCard";
|
||||
@@ -63,13 +66,11 @@ export function ToolboxPage({
|
||||
activeNav="toolbox"
|
||||
>
|
||||
<div className="mx-auto flex max-w-5xl flex-col gap-6 p-6">
|
||||
<header>
|
||||
<h1 className="font-serif text-lg text-ink">创作工具箱</h1>
|
||||
<p className="mt-1 text-xs text-ink-soft">
|
||||
声明驱动的生成器集合:脑洞 / 书名 / 简介 / 名字 / 金手指 / 词条 / 黄金开篇 / 细纲…
|
||||
选一个开始,结构化产物可一键入库(经一致性预检)。
|
||||
</p>
|
||||
</header>
|
||||
<PageHeader
|
||||
title="创作工具箱"
|
||||
eyebrow="generator toolbox"
|
||||
description="脑洞、书名、简介、名字、金手指、词条、黄金开篇与细纲。每个工具都先生成结构化预览,可入库内容会经过一致性预检。"
|
||||
/>
|
||||
|
||||
{active ? (
|
||||
<GeneratorRunner
|
||||
@@ -78,7 +79,11 @@ export function ToolboxPage({
|
||||
onClose={() => setActive(null)}
|
||||
/>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="text-sm text-ink-soft">(暂无可用生成器)</p>
|
||||
<EmptyState
|
||||
icon={Sparkles}
|
||||
title="暂无可用生成器"
|
||||
description="后端暂未返回工具描述符。工具箱会在描述符可用后自动渲染卡片与输入表单。"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sorted.map((tool) => (
|
||||
|
||||
Reference in New Issue
Block a user