Merge branch 'claude/agitated-hofstadter' into development
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user