import React, { useMemo, useState, useEffect } from "react";
// Keep animations minimal to avoid sandbox issues.
import { Search, Filter, Copy, Play, Download, Tag, X, Sparkles, Save, Trash2, Upload, FileSpreadsheet, Layers, BookMarked, FileText, FileUp } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
// XLSX for Excel read/write in-browser
import * as XLSX from "xlsx";
// PDF text extraction (no web worker to fit sandbox)
import * as pdfjsLib from "pdfjs-dist";
// DOCX → text (browser build)
import * as mammoth from "mammoth";
/**
* Interactive AI Prompt Library (Searchable, Click-to-Start)
* + Quick Export to Excel/PDF
* + Saved Prompts
* + 📎 Document-to-Variable uploads (PDF/DOCX/TXT) for prompts
*
* Fix in this revision:
* - Remove pdf.js GlobalWorkerOptions.workerSrc assignment. In sandboxed environments, setting it to `undefined` causes
* "Invalid `workerSrc` type". We instead rely solely on `{ disableWorker: true }` in all `getDocument` calls.
*
* Existing features:
* - No framer-motion (sandbox-safe transitions only)
* - Upload documents into variables (SOPs, policies, etc.)
* - Quick Export supports CSV/XLSX/PDF; PDF uses rules to structure lines → rows
* - Self-tests for helpers
*/
// NOTE: Do NOT set pdfjsLib.GlobalWorkerOptions.workerSrc here. We operate with disableWorker: true per call.
// ---------- Types ----------
export type Prompt = {
id: string;
title: string;
function: "Operations" | "Customer Service" | "Finance" | "Sales" | "HR" | "General";
stage: "Lightbulb" | "Everyday" | "Strategy" | "Advanced";
tags: string[];
description?: string;
template: string; // may include {{variables}}
};
export type SavedPrompt = {
id: string; // unique id
promptId: string; // original template id
title: string; // user-provided title
filledText: string; // fully materialized prompt text
savedAt: number; // epoch ms
};
// ---------- Seed Data (edit/expand freely) ----------
const PROMPTS: Prompt[] = [
// Lightbulb (General)
{
id: "lb-email",
title: "Draft or respond to an email",
function: "General",
stage: "Lightbulb",
tags: ["email", "communication", "fast"],
description: "Paste an email and get a clear, empathetic reply with next steps.",
template:
"I just received this email: \n\n---\n{{email_text}}\n---\n\nPlease draft a clear, professional, and empathetic response that addresses the sender’s concerns and proposes specific next steps. Keep it under {{max_words}} words and include a short subject line.",
},
{
id: "lb-meeting-summary",
title: "Summarize a meeting or call",
function: "General",
stage: "Lightbulb",
tags: ["summary", "notes", "action items"],
description: "Turn raw notes into takeaways and owner-assigned action items.",
template:
"Here are my meeting notes/transcript:\n\n---\n{{meeting_notes}}\n---\n\nSummarize into: 1) key decisions, 2) action items with owners and due dates, 3) risks/dependencies, 4) 3-sentence executive summary.",
},
{
id: "lb-compare-reports",
title: "Compare two reports",
function: "General",
stage: "Lightbulb",
tags: ["analysis", "variance", "reports"],
description: "Paste two datasets or summaries—get differences, trends, issues.",
template:
"Compare the following two reports and highlight: major differences, emerging trends, anomalies, and 3 recommendations.\n\nReport A:\n---\n{{report_a}}\n---\nReport B:\n---\n{{report_b}}\n---\n\nReturn a concise table of key deltas and a short narrative.",
},
{
id: "lb-idea-to-plan",
title: "Turn an idea into a plan",
function: "General",
stage: "Lightbulb",
tags: ["planning", "execution"],
description: "Convert a rough idea into goals, steps, timeline, risks.",
template:
"Here’s my rough idea:\n\n{{idea_text}}\n\nTurn this into a one-page plan with: objective, scope, success metrics, milestones timeline (with owners), risks/mitigations, and next 3 actions for this week.",
},
// Operations
{
id: "ops-wms-entry",
title: "Prepare order entry for WMS",
function: "Operations",
stage: "Everyday",
tags: ["extensiv", "orders", "warehouse"],
description:
"Review paperwork and output clean fields for WMS import. Follows SKU composition and quantity rules.",
template:
"You are an experienced CSR entering Outbound Orders into Extensiv WMS. Review the document and produce a structured table for import.\n\nRules:\n- SKU = STYLE + '-' + COLOR + '-' + SCALE (e.g., 23159-DBAB-LTIND-07).\n- If Quantity is not an integer, assume 0 and exclude the line.\n- Map fields: Customer Order Number → Reference Number; Purchase Order Number → PO Number; Ship Carrier → Ship Via; SKU per logic; Quantity → Scale column; Lot Code/# → Load Number; Batch ID/Number → Notes.\n\nDocument:\n---\n{{order_paperwork}}\n---\n\nReturn a CSV with the mapped columns only.",
},
{
id: "ops-inventory-snapshot",
title: "Inventory snapshot & alerts",
function: "Operations",
stage: "Everyday",
tags: ["inventory", "exceptions", "warehouse"],
description: "Highlight shortages, overstocks, and at-risk SKUs from a report.",
template:
"From this inventory export, provide: 1) current on-hand and available by SKU, 2) under min/over max flags, 3) the top 10 at-risk SKUs.\n\nInventory Export:\n---\n{{inventory_csv_or_text}}\n---\n\nReturn a markdown table (SKU, On-Hand, Available, Min, Max, Flag, Note).",
},
{
id: "ops-sop",
title: "Create or improve an SOP",
function: "Operations",
stage: "Everyday",
tags: ["SOP", "training", "quality"],
description: "Step-by-step SOP with a checklist and training notes.",
template:
"Create a concise SOP for process: {{process_name}}.\nInclude: Purpose, Preconditions, Step-by-step procedure (numbered), Quality checks, Safety notes, Common errors, and a one-page checklist version for training.\nIf an existing SOP is pasted below, first identify 5 improvements.\n\nExisting SOP (optional):\n{{existing_sop}}",
},
// Customer Service
{
id: "cs-respond",
title: "Customer inquiry response",
function: "Customer Service",
stage: "Everyday",
tags: ["customer", "email", "empathy"],
description: "Draft a helpful, reassuring reply with next steps.",
template:
"Customer message:\n---\n{{customer_message}}\n---\nDraft a helpful, empathetic response that answers questions, sets expectations, and lists next steps with timelines. Include a friendly subject line.",
},
{
id: "cs-proactive-update",
title: "Proactive delay update",
function: "Customer Service",
stage: "Everyday",
tags: ["delay", "communication"],
description: "Explain a delay with clarity and options.",
template:
"We have a delay affecting shipment(s): {{shipment_refs}}. Root cause: {{root_cause}}. New ETA: {{new_eta}}. Draft a proactive customer update with options, escalation path, and reassurance. Keep it concise and professional.",
},
// Finance
{
id: "fin-invoice-recon",
title: "Invoice reconciliation by shipment",
function: "Finance",
stage: "Everyday",
tags: ["AP", "invoices", "audit"],
description: "Compare carrier invoices to AP ledger and flag issues.",
template:
"Compare these carrier invoices to the AP ledger. Break out discrepancies by shipment number and classify them (rate, fuel, accessorial, duplicate, missing).\n\nInvoices:\n{{invoices_text_or_csv}}\n\nAP Ledger:\n{{ap_ledger_text_or_csv}}\n\nReturn a table with: Shipment #, Issue Type, Amount, Notes, Action.",
},
{
id: "fin-qb-import",
title: "QuickBooks import builder",
function: "Finance",
stage: "Everyday",
tags: ["QuickBooks", "billing"],
description: "Create a QB bills/import file with markup.",
template:
"From this invoice data, generate a QuickBooks bills import (CSV) for Vendor {{vendor_name}}. Apply {{markup_percent}}% markup as a service fee line.\n\nInvoice Data:\n{{invoice_data}}\n\nReturn CSV columns: Vendor, Bill Date, Due Date, Reference No, Item/Account, Description, Amount, Customer/Project (optional).",
},
// Sales
{
id: "sales-proposal",
title: "Draft a customer proposal",
function: "Sales",
stage: "Everyday",
tags: ["proposal", "sales"],
description: "Problem → Solution → Benefits → Next Steps.",
template:
"Draft a short proposal for {{customer_name}} (industry: {{industry}}). Include: 1) problem statement, 2) our solution, 3) 5 quantified benefits, 4) timeline & next steps, 5) assumptions. Tone: consultative, concise.",
},
{
id: "sales-linkedin",
title: "LinkedIn outreach message",
function: "Sales",
stage: "Everyday",
tags: ["linkedin", "outreach"],
description: "Polite, relevant, non-salesy connect message.",
template:
"Write a 280-character LinkedIn message to a {{title}} at {{company}} about {{relevant_trigger}}. Goal: start a conversation, not pitch. Offer a useful resource and ask a low-friction question.",
},
// HR
{
id: "hr-job-post",
title: "Draft a job posting",
function: "HR",
stage: "Everyday",
tags: ["hiring", "job post"],
description: "Clear role, responsibilities, must-haves, why join.",
template:
"Create a job posting for role: {{role_title}}. Include: mission, responsibilities, must-have skills, nice-to-haves, success metrics (first 90 days), and a short blurb on why join us (culture + impact).",
},
{
id: "hr-feedback",
title: "Rewrite performance feedback",
function: "HR",
stage: "Everyday",
tags: ["coaching", "communication"],
description: "Maintain standards while staying supportive.",
template:
"Rewrite this feedback to be clear, supportive, and actionable, while maintaining firm standards.\n\nFeedback:\n{{feedback_text}}",
},
// Strategy
{
id: "strat-plan-review",
title: "Strategic plan review",
function: "General",
stage: "Strategy",
tags: ["strategy", "review", "coach"],
description: "Identify strengths, risks, and improvements.",
template:
"Act as an executive coach. Review this strategic plan and identify strengths, weaknesses, key risks, and the top improvements.\n\nPlan:\n{{plan_text_or_link}}\n\nReturn: 10-bullet executive summary and a 90-day focus list.",
},
{
id: "strat-risk",
title: "Risk assessment & second-order effects",
function: "General",
stage: "Strategy",
tags: ["risk", "decision"],
description: "Surface risks and mitigations before decisions.",
template:
"Situation:\n{{situation}}\n\nList potential risks and second-order effects. Rate likelihood and impact (Low/Med/High), propose mitigations, and list trigger signals to monitor.",
},
// Advanced
{
id: "adv-roleplay-board",
title: "Role-play: board member Q&A",
function: "General",
stage: "Advanced",
tags: ["role-play", "board", "challenge"],
description: "Stress-test your plan with tough questions.",
template:
"Act as a growth-minded board member reviewing our plan. Ask the 7 toughest questions you’d raise, then evaluate my drafted answers (I will reply) and score clarity, evidence, and feasibility.",
},
{
id: "adv-negotiate",
title: "Role-play: vendor negotiation",
function: "General",
stage: "Advanced",
tags: ["negotiation", "practice"],
description: "Practice countering pushback and improve offers.",
template:
"Role-play a vendor negotiating price and terms for {{item_or_service}}. Push back realistically. After the exercise, critique my approach and suggest 3 improvements.",
},
];
// ---------- Helpers ----------
const FUNCTIONS = ["All", "Operations", "Customer Service", "Finance", "Sales", "HR", "General"] as const;
const STAGES = ["All", "Lightbulb", "Everyday", "Strategy", "Advanced"] as const;
const SAVED_KEY = "briefli_saved_prompts_v1";
type FnFilter = typeof FUNCTIONS[number];
type StageFilter = typeof STAGES[number];
const escapeForUrl = (s: string) => encodeURIComponent(s);
const CHATGPT_NEW_CHAT = "https://chat.openai.com/"; // new chat; user pastes or uses prefill param below
const composePrefillUrl = (text: string) => `${CHATGPT_NEW_CHAT}?temporary-chat=true&input=${escapeForUrl(text)}`;
export function extractVariables(template: string): string[] {
const vars = new Set
();
const re = /\{\{\s*([a-zA-Z0-9_\-]+)\s*\}\}/g;
let m;
while ((m = re.exec(template))) vars.add(m[1]);
return Array.from(vars);
}
export function applyTemplate(template: string, values: Record): string {
return template.replace(/\{\{\s*([a-zA-Z0-9_\-]+)\s*\}\}/g, (_: unknown, k: string) => (values[k] ?? `{{${k}}}`));
}
// ---------- Document reading helpers (TXT/PDF/DOCX) ----------
function readFileAsArrayBuffer(file: File): Promise {
return new Promise((res, rej) => {
const fr = new FileReader();
fr.onload = () => res(fr.result as ArrayBuffer);
fr.onerror = rej;
fr.readAsArrayBuffer(file);
});
}
function readFileAsText(file: File): Promise {
return new Promise((res, rej) => {
const fr = new FileReader();
fr.onload = () => res(fr.result as string);
fr.onerror = rej;
fr.readAsText(file);
});
}
async function pdfToText(file: File): Promise {
const buf = await readFileAsArrayBuffer(file);
// IMPORTANT: operate without a worker in sandbox
const pdf = await pdfjsLib.getDocument({ data: buf, disableWorker: true }).promise;
let out: string[] = [];
for (let p = 1; p <= pdf.numPages; p++) {
const page = await pdf.getPage(p);
const content = await page.getTextContent();
let line = "";
for (const item of content.items as any[]) {
const str = (item.str ?? "").toString();
line += (line ? " " : "") + str;
if (item.hasEOL) {
out.push(line.trim());
line = "";
}
}
if (line.trim()) out.push(line.trim());
}
return out.join("\n");
}
async function docxToText(file: File): Promise {
const arrayBuffer = await readFileAsArrayBuffer(file);
const result = await mammoth.extractRawText({ arrayBuffer });
return (result.value || "").trim();
}
async function fileToText(file: File): Promise {
const name = file.name.toLowerCase();
if (name.endsWith(".pdf")) return pdfToText(file);
if (name.endsWith(".docx")) return docxToText(file);
return readFileAsText(file); // .txt, .csv etc.
}
// ---------- Quick Export rules ----------
export type PdfRules = {
delimiter?: string;
columns?: string[];
skipHeaderRows?: number;
regex?: string;
};
export type RuleSet = {
select?: string[];
rename?: Record;
filter?: { column: string; op: "=="|"!="|">"|">="|"<"|"<="|"contains"; value: any }[];
add?: { column: string; formula: string }[];
pdf?: PdfRules;
};
async function parsePdfToLines(file: File): Promise {
const buf = await readFileAsArrayBuffer(file);
const pdf = await pdfjsLib.getDocument({ data: buf, disableWorker: true }).promise;
const lines: string[] = [];
for (let p = 1; p <= pdf.numPages; p++) {
const page = await pdf.getPage(p);
const content = await page.getTextContent();
let line = "";
for (const item of content.items as any[]) {
const str = (item.str ?? "").toString();
line += (line ? " " : "") + str;
if (item.hasEOL) {
lines.push(line.trim());
line = "";
}
}
if (line.trim()) lines.push(line.trim());
}
return lines.filter(Boolean);
}
function linesToRows(lines: string[], pdfRules?: PdfRules): Record[] {
const rules = pdfRules ?? {};
const out: Record[] = [];
const start = Math.max(0, rules.skipHeaderRows ?? 0);
const useRegex = rules.regex ? new RegExp(rules.regex) : null;
const splitter = (s: string) => {
if (rules.delimiter) return s.split(rules.delimiter);
return s.trim().split(/\s{2,}|\t|\|/);
};
let columns = rules.columns?.slice();
if (!columns || columns.length === 0) {
const trial = splitter(lines[start] || "");
columns = trial.map((_, i) => `c${i+1}`);
}
for (let i = start; i < lines.length; i++) {
const L = lines[i];
if (!L) continue;
if (useRegex) {
const m = L.match(useRegex);
if (m && (m.groups || Object.keys(m).length)) {
out.push({ ...(m.groups || {}) });
}
continue;
}
const parts = splitter(L);
const row: Record = {};
for (let c = 0; c < columns.length; c++) row[columns[c]] = parts[c] ?? "";
out.push(row);
}
return out;
}
function parseDataFileBasic(file: File): Promise[]> {
const name = file.name.toLowerCase();
if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
return readFileAsArrayBuffer(file).then((buf) => {
const wb = XLSX.read(buf, { type: "array" });
const ws = wb.Sheets[wb.SheetNames[0]];
return XLSX.utils.sheet_to_json(ws, { defval: "" }) as Record[];
});
}
if (name.endsWith(".pdf")) {
return parsePdfToLines(file).then((ls) => ls.map((t) => ({ text: t })));
}
return readFileAsText(file).then((text) => {
const rows = text.split(/\r?\n/).filter(Boolean).map((r) => r.split(","));
if (rows.length === 0) return [];
const headers = rows[0];
return rows.slice(1).map((r) => {
const o: Record = {};
headers.forEach((h, i) => (o[h] = r[i] ?? ""));
return o;
});
});
}
function deriveColumnsFromTemplate(file: File): Promise {
return parseDataFileBasic(file).then((rows) => (rows[0] ? Object.keys(rows[0]) : []));
}
function applyRules(rows: Record[], rules: RuleSet): Record[] {
let out = [...rows];
if (rules.filter?.length) {
out = out.filter((row) => {
return rules.filter!.every((f) => {
const v = row[f.column];
const t = typeof v === "string" ? v.toLowerCase() : v;
const cmp = typeof f.value === "string" ? (f.value as string).toLowerCase() : f.value;
switch (f.op) {
case "==": return v == f.value;
case "!=": return v != f.value;
case ">": return Number(v) > Number(f.value);
case ">=": return Number(v) >= Number(f.value);
case "<": return Number(v) < Number(f.value);
case "<=": return Number(v) <= Number(f.value);
case "contains": return typeof t === "string" && typeof cmp === "string" && t.includes(cmp);
default: return true;
}
});
});
}
if (rules.add?.length) {
out = out.map((row, index) => {
const next = { ...row } as Record;
for (const add of rules.add!) {
try {
// eslint-disable-next-line no-new-func
const fn = new Function("row", "index", add.formula) as (row: any, index: number) => any;
next[add.column] = fn(row, index);
} catch (e) {
next[add.column] = "";
}
}
return next;
});
}
if (rules.rename) {
out = out.map((row) => {
const next: Record = {};
Object.keys(row).forEach((k) => {
const nk = rules.rename![k] ?? k;
next[nk] = row[k];
});
return next;
});
}
if (rules.select?.length) {
out = out.map((row) => {
const next: Record = {};
rules.select!.forEach((k) => (next[k] = row[k] ?? ""));
return next;
});
}
return out;
}
function toWorkbook(rows: Record[], templateHeaders?: string[]) {
let headers: string[];
if (templateHeaders?.length) {
headers = templateHeaders;
} else if (rows[0]) {
headers = Object.keys(rows[0]);
} else {
headers = [];
}
const data = [headers, ...rows.map((r) => headers.map((h) => r[h] ?? ""))];
const ws = XLSX.utils.aoa_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Export");
return wb;
}
// ---------- UI Components ----------
const Chip: React.FC<{ label: string; active?: boolean; onClick?: () => void }>=({ label, active, onClick }) => (
);
const SectionHeader: React.FC<{ title: string; subtitle?: string }>=({ title, subtitle }) => (
{title}
{subtitle &&
{subtitle}
}
);
// ---------- Self Tests (helpers) ----------
function runSelfTests() {
try {
const t1 = "Hello {{name}} your {{item-1}} is ready";
const vars1 = extractVariables(t1).sort();
console.assert(JSON.stringify(vars1) === JSON.stringify(["item-1", "name"].sort()), "extractVariables captures hyphenated vars");
const t2 = "No variables here";
const vars2 = extractVariables(t2);
console.assert(vars2.length === 0, "extractVariables returns empty if none");
const out1 = applyTemplate("Hi {{name}}", { name: "Mike" });
console.assert(out1 === "Hi Mike", "applyTemplate replaces tokens");
const out2 = applyTemplate("Hi {{name}} {{missing}}", { name: "Sam" });
console.assert(out2.includes("{{missing}}"), "applyTemplate preserves unknown tokens");
// Quick Export rules tests (non-PDF)
const rows = [ { A:1, B:2, Text:"abc" }, { A:3, B:4, Text:"def" } ];
const rules = { select:["A","Sum"], add:[{column:"Sum", formula:"return (Number(row.A)||0)+(Number(row.B)||0);"}], filter:[{column:"Text", op:"contains", value:"a"}] } as RuleSet;
const transformed = applyRules(rows, rules);
console.assert(transformed.length === 1 && transformed[0].Sum === 3, "applyRules add/filter/select works");
// Lines → Rows tests (PDF-style parsing logic)
const pdfLines = ["2024-01-01 ABC123 10.50", "2024-01-02 XYZ555 0.00"];
const parsedPdf = linesToRows(pdfLines, { delimiter: undefined, columns:["Date","BOL","Total"] });
console.assert(parsedPdf[0].Date === "2024-01-01" && parsedPdf[0].Total === "10.50", "linesToRows default splitter works");
// Variable fill workflow (doc upload simulation)
const mockText = "Line1\nLine2";
const filled = applyTemplate("Doc: {{existing_sop}}", { existing_sop: mockText });
console.assert(filled.includes("Line2"), "uploaded text flows into variables");
// New: ensure we didn't set workerSrc improperly
try {
// @ts-ignore
const hasWorkerSrc = pdfjsLib?.GlobalWorkerOptions?.workerSrc;
console.assert(hasWorkerSrc === undefined || typeof hasWorkerSrc === "string", "workerSrc should be undefined or string (not object/other)");
} catch {}
if (typeof console !== "undefined") console.log("✅ Self-tests passed");
} catch (err) {
if (typeof console !== "undefined") console.error("❌ Self-tests failed", err);
}
}
// ---------- Main Component ----------
export default function PromptLibrary() {
const [tab, setTab] = useState<"library"|"export"|"saved">("library");
// Library state
const [q, setQ] = useState("");
const [fnFilter, setFnFilter] = useState("All");
const [stageFilter, setStageFilter] = useState("All");
const [selected, setSelected] = useState(null);
const [values, setValues] = useState>({});
// Saved prompts state
const [saved, setSaved] = useState([]);
const [savedQ, setSavedQ] = useState("");
// Export state
const [dataFile, setDataFile] = useState(null);
const [templateFile, setTemplateFile] = useState(null);
const [templateHeaders, setTemplateHeaders] = useState();
const [rulesText, setRulesText] = useState(`{\n \"select\": [],\n \"rename\": {},\n \"filter\": [],\n \"add\": [],\n \"pdf\": {\n \"delimiter\": \"\",\n \"columns\": [],\n \"skipHeaderRows\": 0,\n \"regex\": \"\"\n }\n}`);
const [dataPreview, setDataPreview] = useState[]>([]);
const [exportName, setExportName] = useState("export.xlsx");
// run tests on mount in non-production environments
useEffect(() => {
const dev = typeof process !== "undefined" && process?.env?.NODE_ENV !== "production";
if (dev) runSelfTests();
try {
const raw = localStorage.getItem(SAVED_KEY);
if (raw) setSaved(JSON.parse(raw));
} catch {}
}, []);
useEffect(() => {
if (!selected) return;
const vars = extractVariables(selected.template);
const init: Record = {};
vars.forEach(v => (init[v] = values[v] ?? ""));
setValues(init);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected?.id]);
const filtered = useMemo(() => {
const needle = q.trim().toLowerCase();
return PROMPTS.filter(p => {
const matchesText = !needle || [p.title, p.description ?? "", p.template, ...p.tags].join("\n").toLowerCase().includes(needle);
const matchesFn = fnFilter === "All" || p.function === fnFilter;
const matchesStage = stageFilter === "All" || p.stage === stageFilter;
return matchesText && matchesFn && matchesStage;
});
}, [q, fnFilter, stageFilter]);
// ----- Saved prompts helpers -----
const persistSaved = (next: SavedPrompt[]) => {
setSaved(next);
try { localStorage.setItem(SAVED_KEY, JSON.stringify(next)); } catch {}
};
const onSavePrompt = () => {
if (!selected) return alert("Open a prompt first");
const text = applyTemplate(selected.template, values);
const title = prompt("Name this saved prompt", selected.title) || selected.title;
const entry: SavedPrompt = {
id: `${Date.now()}-${Math.random().toString(36).slice(2,8)}`,
promptId: selected.id,
title,
filledText: text,
savedAt: Date.now()
};
persistSaved([entry, ...saved]);
alert("Saved ✅");
};
const onDeleteSaved = (id: string) => {
persistSaved(saved.filter(s => s.id !== id));
};
const filteredSaved = useMemo(() => {
const needle = savedQ.trim().toLowerCase();
return saved.filter(s => !needle || s.title.toLowerCase().includes(needle) || s.filledText.toLowerCase().includes(needle));
}, [saved, savedQ]);
// ----- Quick Export handlers -----
const onLoadData = async () => {
if (!dataFile) return alert("Upload a data file first");
const rows = await parseDataFileBasic(dataFile);
setDataPreview(rows.slice(0, 100));
};
const onLoadTemplate = async () => {
if (!templateFile) return alert("Upload a template file first");
const headers = await deriveColumnsFromTemplate(templateFile);
if (!headers.length) return alert("No headers detected in template file");
setTemplateHeaders(headers);
alert(`Template headers loaded: ${headers.join(", ")}`);
};
const onExportExcel = async () => {
if (!dataFile) return alert("Upload a data file first");
let rules: RuleSet = {};
try { rules = JSON.parse(rulesText); } catch { return alert("Rules must be valid JSON"); }
const name = dataFile.name.toLowerCase();
let rows: Record[] = [];
if (name.endsWith(".pdf")) {
const lines = await parsePdfToLines(dataFile);
rows = linesToRows(lines, rules.pdf);
} else {
rows = await parseDataFileBasic(dataFile);
}
rows = applyRules(rows, rules);
const wb = toWorkbook(rows, templateHeaders);
XLSX.writeFile(wb, exportName || "export.xlsx");
};
const onCopy = async () => {
if (!selected) return;
const text = applyTemplate(selected.template, values);
try {
await navigator.clipboard.writeText(text);
alert("Prompt copied to clipboard ✨");
} catch {
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
alert("Prompt copied to clipboard ✨");
}
};
const onStart = () => {
if (!selected) return;
const text = applyTemplate(selected.template, values);
const url = composePrefillUrl(text);
window.open(url, "_blank");
};
const exportJson = () => {
const data = JSON.stringify(filtered, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "prompt-library.json";
a.click();
URL.revokeObjectURL(url);
};
// ----- Document upload into variable -----
const handleVarFile = async (vKey: string, file?: File | null) => {
if (!file) return;
try {
const text = await fileToText(file);
setValues(prev => ({ ...prev, [vKey]: (prev[vKey] ? (prev[vKey] + "\n\n" + text) : text) }));
} catch (e) {
alert("Could not read file. Please ensure it is PDF, DOCX, or TXT.");
}
};
return (
{/* Header */}
AI Prompt Library
Search • Filter • Customize • Start
{/* Tabs */}
{/* ----- LIBRARY TAB ----- */}
{tab === "library" && (
<>
{/* Search & Filters */}
setQ(e.target.value)}
/>
{FUNCTIONS.map(f => (
setFnFilter(f)} />
))}
{STAGES.map(s => (
setStageFilter(s)} />
))}
{/* Results */}
{filtered.map(p => (
setSelected(p)}>
{p.title}
{p.function}
{p.stage}
{p.description}
{p.tags.map(t => #{t})}
))}
{/* Drawer / Modal substitute */}
{selected && (
setSelected(null)}>
e.stopPropagation()}>
{selected.title}
{selected.function} • {selected.stage}
{selected.description &&
{selected.description}
}
{/* Variables */}
{extractVariables(selected.template).map(v => (
))}
{extractVariables(selected.template).length===0 && (
This prompt has no variables. You can copy or start as-is.
)}
{applyTemplate(selected.template, values)}
)}
>
)}
{/* ----- QUICK EXPORT TAB ----- */}
{tab === "export" && (
<>
Quick Export to Excel
Preview (first 100 rows/lines)
{dataPreview.length === 0 ? (
No data loaded yet.
) : (
{Object.keys(dataPreview[0]).map(h => {h} | )}
{dataPreview.map((r,i)=> (
{Object.keys(dataPreview[0]).map(h => {String(r[h]??"")} | )}
))}
)}
>
)}
{/* ----- SAVED TAB ----- */}
{tab === "saved" && (
<>
setSavedQ(e.target.value)} />
{filteredSaved.length === 0 ? (
No saved prompts yet. Open a prompt in the Library tab and click Save.
) : (
{filteredSaved.map(sp => (
{sp.title}
{new Date(sp.savedAt).toLocaleString()}
{sp.filledText}
))}
)}
>
)}
Built for Briefli users • Add more prompts in PROMPTS[] • Variables are {"{{like_this}}"}
);
}