/**
* The clickable keyword cloud for the single-image view. Prefers a saved keyword list on the
* sidecar (`meta.keywords`); otherwise it parses clean tags from the sent prompt. "Rebuild with AI"
* asks the rewrite provider for a tidy tag list and saves it over the sidecar's set.
* @module gui/components/single/KeywordsCard
*/
import { useMemo, useState } from "react";
import { useIntl } from "react-intl";
import { isOutputFile, updateImageMeta } from "../../lib/output.js";
import { parseKeywords, normalizeKeywordList } from "../../lib/keywords.js";
import { rewritePrompt } from "../../lib/rewrite.js";
import { effectiveKey } from "../../lib/sessionKeys.js";
import { getProvider } from "../../lib/providers/index.js";
import { msgs } from "./messages.js";
/**
* The clickable keyword cloud.
* @param {object} props
* @param {string} props.text The sent-to-model prompt text.
* @param {string[]|null} props.saved A saved keyword list from the sidecar, or null.
* @param {object} props.item The gallery item (for its served path / on-disk check).
* @param {object} props.settings App settings (rewrite provider + key).
* @param {Function} props.onSearch `(term)` — search the gallery for a keyword.
* @param {Function} props.onSaved `(meta)` — a fresh sidecar after a save.
* @returns {JSX.Element|null}
*/
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
export default function KeywordsCard({
text,
saved,
item,
settings,
onSearch,
onSaved
}) {
const intl = useIntl();
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const tags = useMemo(() => {
if (Array.isArray(saved) && saved.length) return saved.slice(0, 80);
return parseKeywords(text).map(k => k.display);
}, [saved, text]);
const rewriteId = settings?.rewriteProvider;
const hasRewrite = rewriteId && rewriteId !== "none";
// The image supports an AI keyword rebuild (it's a saved local file with prompt text). Whether a
// Text provider is actually chosen is a *separate* gate — so the button can stay visible but locked.
const rebuildable = isOutputFile(item?.path) && Boolean(text && text.trim());
async function rebuild() {
setError("");
const key = effectiveKey(rewriteId, settings);
if (!key) {
setError(intl.formatMessage(msgs.noKey, {
provider: getProvider(rewriteId)?.label || intl.formatMessage(msgs.providerFallback)
}));
return;
}
setBusy(true);
try {
const reply = await rewritePrompt({
providerId: rewriteId,
prompt: text,
key,
mode: "keyword"
});
const keywords = normalizeKeywordList((reply || "").split(/[,\n]+/), {
sort: true
});
if (!keywords.length) {
setError(intl.formatMessage(msgs.noKeywords));
return;
}
const meta = await updateImageMeta(item.path, {
keywords
});
if (meta) onSaved?.(meta);else setError(intl.formatMessage(msgs.saveFailed));
} catch (e) {
setError(intl.formatMessage(msgs.rebuildFailed, {
error: e.message || String(e)
}));
} finally {
setBusy(false);
}
}
if (tags.length < 2 && !rebuildable) return null;
return /*#__PURE__*/_jsxDEV("section", {
className: "g-card",
children: [/*#__PURE__*/_jsxDEV("div", {
className: "g-card-head",
children: [/*#__PURE__*/_jsxDEV("h3", {
className: "g-card-title",
children: intl.formatMessage(Array.isArray(saved) && saved.length ? msgs.keywordsEdited : msgs.keywords)
}, void 0, false), rebuildable && /*#__PURE__*/_jsxDEV("button", {
className: "g-card-action",
onClick: rebuild,
disabled: busy || !hasRewrite,
title: intl.formatMessage(hasRewrite ? msgs.rebuildTitle : msgs.rebuildLocked),
children: intl.formatMessage(busy ? msgs.rebuilding : msgs.rebuild)
}, void 0, false)]
}, void 0, true), error && /*#__PURE__*/_jsxDEV("p", {
className: "g-card-err",
children: error
}, void 0, false), tags.length > 0 && /*#__PURE__*/_jsxDEV("div", {
className: "g-cloud",
children: tags.map((t, i) => /*#__PURE__*/_jsxDEV("button", {
className: "g-cloud-chip",
onClick: () => onSearch(t),
title: intl.formatMessage(msgs.find, {
term: t
}),
children: t
}, `${t}-${i}`, false))
}, void 0, false)]
}, void 0, true);
}