scripts/vendor.sh clones upstream, rewrites @/ imports to relative paths, and applies scripts/apply-host.py. web/host.ts is ours and gets placed into the vendored tree, because upstream declined it. a script rather than a .patch, and that is not a preference. between cfde698 and 5c83ac6 — one day — upstream changed an import line, re-indented the component from 6 spaces to 8, and added an editAccess guard. a context diff fails on all three, none of which concern us. so every anchor matches structure with flexible whitespace and re-indents to whatever it finds, and a missing anchor stops the run loudly. all three prompt-export paths are wired, which is what upstream said a complete version needs. the other two clipboard writes are ShareMenu's share-link and ask-an-AI blurb, and the reason they are excluded is written down so the count does not read as the same oversight twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { buildPrompt } from "../lib/prompt";
|
|
import { Doc, Palette, Platform, defaultPlatformOf } from "../lib/tokens";
|
|
import { Icon } from "./M3Node";
|
|
import { Field, IconBtn, Segmented } from "./ui";
|
|
import { t, useLang } from "../lib/i18n";
|
|
import { useHost } from "../lib/host";
|
|
|
|
export function PromptPanel({
|
|
doc,
|
|
widths,
|
|
palette: p,
|
|
onDoc,
|
|
}: {
|
|
doc: Doc;
|
|
widths: Record<string, number>;
|
|
palette: Palette;
|
|
onDoc: (patch: Partial<Doc>) => void;
|
|
}) {
|
|
const lang = useLang();
|
|
const generated = useMemo(() => buildPrompt(doc, widths, undefined, lang), [doc, widths, lang]);
|
|
const edited = doc.promptEdit !== undefined;
|
|
const text = edited ? doc.promptEdit! : generated;
|
|
const host = useHost();
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!copied) return;
|
|
const t = setTimeout(() => setCopied(false), 1400);
|
|
return () => clearTimeout(t);
|
|
}, [copied]);
|
|
|
|
const copy = async () => {
|
|
try {
|
|
let handled = false;
|
|
try { handled = host.onExport?.(text, null) === true; } catch {}
|
|
if (handled) { setCopied(true); return; }
|
|
await navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
} catch {}
|
|
};
|
|
|
|
const projectButton = (icon: string, label: string, onClick: () => void) => (
|
|
<button
|
|
onClick={onClick}
|
|
className="m3-press"
|
|
style={{
|
|
flex: 1,
|
|
height: 42,
|
|
borderRadius: 21,
|
|
border: "none",
|
|
background: p.secondaryContainer,
|
|
color: p.onSecondaryContainer,
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
cursor: "pointer",
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 7,
|
|
}}
|
|
>
|
|
<Icon name={icon} size={19} />
|
|
{label}
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<div style={{ display: "flex", flexDirection: "column", height: "100%", padding: 12, gap: 10 }}>
|
|
<Field
|
|
value={doc.title}
|
|
onChange={(title) => onDoc({ title })}
|
|
placeholder={t("appName", lang)}
|
|
p={p}
|
|
icon="smartphone"
|
|
/>
|
|
<Field
|
|
value={doc.brief}
|
|
onChange={(brief) => onDoc({ brief })}
|
|
placeholder={t("brief", lang)}
|
|
p={p}
|
|
icon="lightbulb"
|
|
multiline
|
|
rows={3}
|
|
/>
|
|
<Segmented<Platform>
|
|
options={[
|
|
{ key: "android", icon: "android", label: "Android", title: t("targetAndroid", lang) },
|
|
{ key: "web", icon: "language", label: "Web", title: t("targetWeb", lang) },
|
|
]}
|
|
value={doc.platform ?? defaultPlatformOf(doc.frames, doc.frame)}
|
|
onChange={(platform) => onDoc({ platform })}
|
|
p={p}
|
|
height={40}
|
|
/>
|
|
<div style={{ position: "relative", flex: 1, minHeight: 0, display: "flex" }}>
|
|
<textarea
|
|
className="no-scrollbar"
|
|
value={text}
|
|
onChange={(e) => onDoc({ promptEdit: e.target.value })}
|
|
spellCheck={false}
|
|
aria-label={t("prompt", lang)}
|
|
style={{
|
|
flex: 1,
|
|
minHeight: 0,
|
|
width: "100%",
|
|
borderRadius: 18,
|
|
border: "none",
|
|
background: p.surfaceContainerLow,
|
|
padding: edited ? "14px 14px 48px" : 14,
|
|
fontSize: 13,
|
|
lineHeight: 1.75,
|
|
color: p.onSurface,
|
|
fontFamily: "inherit",
|
|
resize: "none",
|
|
outline: "none",
|
|
boxSizing: "border-box",
|
|
}}
|
|
/>
|
|
{edited && (
|
|
<div style={{ position: "absolute", right: 8, bottom: 8 }}>
|
|
<IconBtn icon="undo" p={p} size={32} onClick={() => onDoc({ promptEdit: undefined })} title={t("promptReset", lang)} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={copy}
|
|
className="m3-press"
|
|
style={{
|
|
height: 48,
|
|
borderRadius: 24,
|
|
border: "none",
|
|
background: copied ? p.tertiaryContainer : p.primary,
|
|
color: copied ? p.onTertiaryContainer : p.onPrimary,
|
|
fontSize: 14,
|
|
fontWeight: 600,
|
|
cursor: "pointer",
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 8,
|
|
transition: "background 160ms, color 160ms",
|
|
}}
|
|
>
|
|
<Icon name={copied ? "check" : "content_copy"} size={20} />
|
|
{copied ? t("copied", lang) : t("copyPrompt", lang)}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|