Merge branch 'claude/agitated-hofstadter' into development

This commit is contained in:
Dorian
2026-03-03 18:26:31 +00:00
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)
+2 -2
View File
@@ -62,7 +62,7 @@ ${podcastContext}`
const activeProvider = ref<Provider>('claude')
const activeModel = ref('claude-sonnet-4')
const activeModel = ref('claude-haiku-4.5')
const availableProviders = computed(() => {
const providers: { id: Provider; name: string; models: { id: string; name: string }[] }[] = [
@@ -70,9 +70,9 @@ const availableProviders = computed(() => {
id: 'claude',
name: 'Claude (Max)',
models: [
{ id: 'claude-haiku-4.5', name: 'Claude 4.5 Haiku' },
{ id: 'claude-sonnet-4', name: 'Claude Sonnet 4' },
{ id: 'claude-opus-4', name: 'Claude Opus 4' },
{ id: 'claude-haiku-3.5', name: 'Claude 3.5 Haiku' },
],
},
]
@@ -91,7 +91,7 @@ export function useContentPanel() {
books.length > 0 || tvSeries.length > 0 || images.length > 0 || places.length > 0
const hasMagazine = magazineSections.length >= 1 && (
isNewsQuery(userQuery) || isNewsLikeResponse(text) ||
/sentiment|bearish|bull case|macro|%|BTC|bitcoin|BIP|protocol|debate|what'?s happening/i.test(text) ||
/sentiment|bearish|bull case|macro|%|BTC|bitcoin|BIP|protocol|debate|what'?s happening|ETF|inflow|trading at|key developments|price recovery|institutional|analyst watch|market cap/i.test(text) ||
(!hasAnyOtherContent && !hasWebsites && magazineSections.length >= 2)
)
@@ -239,7 +239,7 @@ export function useContentPanel() {
books.length > 0 || tvSeries.length > 0 || images.length > 0 || places.length > 0
const hasMagazine = magazineSections.length >= 1 && (
isNewsQuery(userQuery) || isNewsLikeResponse(text) ||
/sentiment|bearish|bull case|macro|%|BTC|bitcoin|BIP|protocol|debate|what'?s happening/i.test(text) ||
/sentiment|bearish|bull case|macro|%|BTC|bitcoin|BIP|protocol|debate|what'?s happening|ETF|inflow|trading at|key developments|price recovery|institutional|analyst watch|market cap/i.test(text) ||
(!hasAnyOtherInline && !hasWebsites && magazineSections.length >= 2)
)
const tabs = filterTabsByContext(userQuery, films.length > 0, songs.length > 0, podcasts.length > 0, books.length > 0, tvSeries.length > 0, images.length > 0, places.length > 0, hasNews, hasWebsites, hasMagazine)
+1 -1
View File
@@ -53,7 +53,7 @@ export const useChatStore = defineStore('chat', () => {
const panelSide = ref<'left' | 'right'>(savedSide ?? 'left')
const webSearchEnabled = ref(localStorage.getItem('aiui-web-search') !== 'false')
const chatCollapsed = ref(localStorage.getItem('aiui-chat-collapsed') === 'true')
const chatCollapsed = ref(localStorage.getItem('aiui-chat-collapsed') !== 'false')
// Load chats from server on startup — _loaded gate prevents the watcher
// from overwriting the file with empty data before the load completes