fix(app): harden brief extraction and default to fastest model

- Default chatCollapsed to true so PromptIndex shows on load
- Default AI model to claude-haiku-4.5 for faster responses
- Fix extractMagazineSections to handle numbered/bullet bold lists
  without markdown headings (e.g. "1. **Title** — content")
- Fix "For deeper coverage" stripping for bold-formatted text
- Expand hasMagazine detection with more financial/news keywords

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 15:53:56 +00:00
co-authored by Claude Opus 4.6
parent 721e915a48
commit 10e12a329f
4 changed files with 104 additions and 6 deletions
@@ -161,7 +161,7 @@ export function extractMagazineSections(text: string): MagazineSection[] {
let cleanedText = text
.replace(/\n+(?:Sources|References|Links):?\s*\n[\s\S]*$/i, '')
.replace(/\n+For deeper[^:]*:[\s\S]*$/im, '')
.replace(/\n+\*{0,2}For deeper[^:]*:[\s\S]*$/im, '')
const headingPositions: { title: string; start: number; contentStart: number }[] = []
const headingLineRe = /^#{2,3}\s+(.+)$/gm
@@ -223,6 +223,104 @@ export function extractMagazineSections(text: string): MagazineSection[] {
}
}
// ── Fallback: numbered/bullet lists with bold titles ──────────────
// Handles responses like: "1. **Strong price recovery** — BTC climbed..."
if (sections.length === 0) {
// Detect bold pseudo-headings on their own line: "**Key developments today:**"
const boldHeadingRe = /(?:^|\n)\s*\*\*([^*]+?)\*\*\s*:?\s*(?:\n|$)/g
const boldHeadings: { title: string; start: number; end: number }[] = []
while ((m = boldHeadingRe.exec(cleanedText)) !== null) {
const title = m[1].trim()
// Skip if preceded by a list marker (it's a list item, not a heading)
const before = cleanedText.slice(Math.max(0, m.index - 10), m.index)
if (/\d+\.\s*$/.test(before) || /[-•]\s*$/.test(before)) continue
if (title.length > 2 && title.length < 100) {
boldHeadings.push({ title, start: m.index, end: m.index + m[0].length })
}
}
// Try numbered items: "1. **Title** — content"
const numberedRe = /(?:^|\n)\s*\d+\.\s*\*\*([^*]+)\*\*\s*[-–—:]+\s*/g
const numberedItems: { title: string; start: number; contentStart: number }[] = []
while ((m = numberedRe.exec(cleanedText)) !== null) {
const title = m[1].trim()
if (title.length > 1) {
numberedItems.push({ title, start: m.index, contentStart: m.index + m[0].length })
}
}
// Find group header for numbered items
let group: string | undefined
if (numberedItems.length > 0 && boldHeadings.length > 0) {
const firstItemStart = numberedItems[0].start
const precedingHeading = boldHeadings.filter(h => h.end <= firstItemStart).pop()
if (precedingHeading) group = precedingHeading.title
}
for (let i = 0; i < numberedItems.length; i++) {
const item = numberedItems[i]
const nextStart = i + 1 < numberedItems.length ? numberedItems[i + 1].start : cleanedText.length
const rawContent = cleanMagazineContent(cleanedText.slice(item.contentStart, nextStart))
if (rawContent.length >= 15) {
addSection(sections, item.title, rawContent, seen, group)
}
}
// Also try bullet items: "- **Title** — content"
if (sections.length === 0) {
const bulletRe = /(?:^|\n)\s*[-•]\s*\*\*([^*]+)\*\*\s*[-–—:]+\s*/g
const bulletItems: { title: string; start: number; contentStart: number }[] = []
while ((m = bulletRe.exec(cleanedText)) !== null) {
const title = m[1].trim()
if (title.length > 1) {
bulletItems.push({ title, start: m.index, contentStart: m.index + m[0].length })
}
}
for (let i = 0; i < bulletItems.length; i++) {
const item = bulletItems[i]
const nextStart = i + 1 < bulletItems.length ? bulletItems[i + 1].start : cleanedText.length
const rawContent = cleanMagazineContent(cleanedText.slice(item.contentStart, nextStart))
if (rawContent.length >= 15) {
addSection(sections, item.title, rawContent, seen)
}
}
}
// Also try standalone bold paragraphs: "**Title** — content\n\n**Next** — ..."
if (sections.length === 0) {
const boldParaRe = /(?:^|\n)\s*\*\*([^*]{3,60})\*\*\s*[-–—:]+\s*([\s\S]*?)(?=\n\s*\*\*[^*]{3,60}\*\*\s*[-–—:]|\n{2,}\*\*[^*]+\*\*\s*:|\s*$)/g
while ((m = boldParaRe.exec(cleanedText)) !== null) {
const title = m[1].trim()
const content = cleanMagazineContent(m[2].trim())
if (title.length > 2 && content.length >= 15) {
addSection(sections, title, content, seen)
}
}
}
// Extract intro paragraph as Summary
if (sections.length > 0) {
const firstStructured = numberedItems.length > 0
? numberedItems[0].start
: cleanedText.length
// Bold pseudo-heading before items marks the end of the intro
const precedingBoldHeading = boldHeadings.length > 0 && boldHeadings[0].start < firstStructured
? boldHeadings[0].start
: firstStructured
const fallbackIntroEnd = Math.min(firstStructured, precedingBoldHeading)
if (fallbackIntroEnd > 30) {
const intro = cleanedText.slice(0, fallbackIntroEnd)
.replace(/\*\*/g, '')
.replace(/^[#*_\s-]+/gm, '')
.trim()
if (intro.length > 30 && !seen.has('Summary')) {
addSection(sections, 'Summary', intro, seen)
}
}
}
}
const closingMatch = /(This is being called[^.]+\.[^"]*"[^"]+"[^.]*\.)/i.exec(cleanedText)
if (closingMatch && !seen.has('Key')) {
addSection(sections, 'Key takeaway', closingMatch[1].trim(), seen)