Spaces:
Sleeping
Sleeping
| /** | |
| * Browser-side Markdown export utilities. | |
| * | |
| * Generates downloadable `.md` files from conversations and saved responses. | |
| * All file generation and download triggering happens entirely in the browser | |
| * without a server round-trip (Requirement 11.4). | |
| * | |
| * Output format: | |
| * --- | |
| * date: YYYY-MM-DD | |
| * model: claude-3-5-sonnet | |
| * type: conversation | response | |
| * --- | |
| * | |
| * # Question text | |
| * | |
| * Answer body | |
| * | |
| * ## Sources | |
| * | |
| * 1. [Title](url) — domain | |
| * 2. Title — domain | |
| */ | |
| import type { Conversation, QueryResult, SavedResponse } from "../types"; | |
| // --------------------------------------------------------------------------- | |
| // Public API | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Generates a Markdown export of a full conversation and triggers a browser | |
| * file download. | |
| * | |
| * The filename follows the pattern: `health-marketing-{YYYYMMDD}-{slug}.md` | |
| * where `slug` is derived from the first user question. | |
| * | |
| * Requirements: 11.1, 11.3 | |
| */ | |
| export function exportConversation(conversation: Conversation): void { | |
| const today = formatDate(new Date()); | |
| const frontMatter = buildFrontMatter(today, "conversation"); | |
| const sections: string[] = [frontMatter]; | |
| // Iterate over message pairs: user message followed by assistant message. | |
| const { messages, results } = conversation; | |
| for (let i = 0; i < messages.length; i++) { | |
| const msg = messages[i]; | |
| if (msg.role !== "user") continue; | |
| const question = msg.content; | |
| const assistantMsg = messages[i + 1]; | |
| const answer = | |
| assistantMsg && assistantMsg.role === "assistant" | |
| ? assistantMsg.content | |
| : ""; | |
| // results are keyed by the assistant message index (as a string). | |
| const resultKey = String(i + 1); | |
| const result = results[resultKey] ?? null; | |
| sections.push(buildQASection(question, answer, result)); | |
| } | |
| const markdown = sections.join("\n\n"); | |
| const firstQuestion = messages.find((m) => m.role === "user")?.content ?? ""; | |
| const slug = slugify(firstQuestion); | |
| const filename = `health-marketing-${today.replace(/-/g, "")}-${slug}.md`; | |
| triggerDownload(markdown, filename); | |
| } | |
| /** | |
| * Generates a Markdown export of a single Q&A pair and triggers a browser | |
| * file download. | |
| * | |
| * Requirements: 11.2, 11.3 | |
| */ | |
| export function exportSingleResponse( | |
| question: string, | |
| answer: string, | |
| result: QueryResult | |
| ): void { | |
| const today = formatDate(new Date()); | |
| const frontMatter = buildFrontMatter(today, "response"); | |
| const qaSection = buildQASection(question, answer, result); | |
| const markdown = `${frontMatter}\n\n${qaSection}`; | |
| const slug = slugify(question); | |
| const filename = `health-marketing-${today.replace(/-/g, "")}-${slug}.md`; | |
| triggerDownload(markdown, filename); | |
| } | |
| /** | |
| * Generates a Markdown export of all saved responses concatenated into a | |
| * single file, separated by `---` dividers, and triggers a browser file | |
| * download. | |
| * | |
| * Requirements: 11.1, 11.3 | |
| */ | |
| export function exportAllSavedResponses(saved: SavedResponse[]): void { | |
| const today = formatDate(new Date()); | |
| const frontMatter = buildFrontMatter(today, "saved-responses"); | |
| const responseSections = saved.map((sr) => | |
| buildQASection(sr.question, sr.answer, sr.result) | |
| ); | |
| const markdown = | |
| frontMatter + "\n\n" + responseSections.join("\n\n---\n\n"); | |
| const filename = `health-marketing-${today.replace(/-/g, "")}-saved-responses.md`; | |
| triggerDownload(markdown, filename); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Internal helpers | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Formats a Date as `YYYY-MM-DD` (local date, not UTC). | |
| */ | |
| function formatDate(date: Date): string { | |
| const year = date.getFullYear(); | |
| const month = String(date.getMonth() + 1).padStart(2, "0"); | |
| const day = String(date.getDate()).padStart(2, "0"); | |
| return `${year}-${month}-${day}`; | |
| } | |
| /** | |
| * Builds the YAML front-matter block. | |
| * | |
| * Example: | |
| * --- | |
| * date: 2024-01-15 | |
| * model: claude-3-5-sonnet | |
| * type: conversation | |
| * --- | |
| */ | |
| function buildFrontMatter(date: string, type: string): string { | |
| return `---\ndate: ${date}\nmodel: claude-3-5-sonnet\ntype: ${type}\n---`; | |
| } | |
| /** | |
| * Builds a single Q&A section: H1 question, answer body, and Sources section. | |
| * | |
| * Example: | |
| * # What are the advertising rules for chiropractors? | |
| * | |
| * [answer body here] | |
| * | |
| * ## Sources | |
| * | |
| * 1. [Section Title](https://example.com) — domain | |
| * 2. Section Title — domain | |
| */ | |
| function buildQASection( | |
| question: string, | |
| answer: string, | |
| result: QueryResult | null | |
| ): string { | |
| const parts: string[] = []; | |
| parts.push(`# ${question}`); | |
| parts.push(answer); | |
| if (result && result.citations.length > 0) { | |
| const sourceLines = result.citations.map((citation) => { | |
| const prefix = `${citation.index}.`; | |
| const titlePart = citation.source_url | |
| ? `[${citation.title}](${citation.source_url})` | |
| : citation.title; | |
| return `${prefix} ${titlePart} — ${citation.domain}`; | |
| }); | |
| parts.push(`## Sources\n\n${sourceLines.join("\n")}`); | |
| } | |
| return parts.join("\n\n"); | |
| } | |
| /** | |
| * Converts a question string into a URL-safe slug for use in filenames. | |
| * | |
| * - Lowercases the string | |
| * - Replaces spaces with hyphens | |
| * - Removes non-alphanumeric characters (except hyphens) | |
| * - Truncates to 40 characters | |
| * - Trims trailing hyphens | |
| */ | |
| function slugify(text: string): string { | |
| return text | |
| .toLowerCase() | |
| .replace(/\s+/g, "-") | |
| .replace(/[^a-z0-9-]/g, "") | |
| .slice(0, 40) | |
| .replace(/-+$/, ""); | |
| } | |
| /** | |
| * Triggers a browser file download using the Blob + URL.createObjectURL | |
| * + anchor click pattern. | |
| */ | |
| function triggerDownload(content: string, filename: string): void { | |
| const blob = new Blob([content], { type: "text/markdown;charset=utf-8" }); | |
| const url = URL.createObjectURL(blob); | |
| const anchor = document.createElement("a"); | |
| anchor.href = url; | |
| anchor.download = filename; | |
| anchor.style.display = "none"; | |
| document.body.appendChild(anchor); | |
| anchor.click(); | |
| document.body.removeChild(anchor); | |
| // Release the object URL to free memory. | |
| URL.revokeObjectURL(url); | |
| } | |