fix(aiui): node content outranks the prose surface, and web search resolves
Browser-verified on archi-dev-box; all three were only visible by driving the real UI. The tab bar. `setArchyContent` put the node's grids up, then `updatePanelFromText` replaced the bar with tabs inferred from the reply text. "show me my own shared content" therefore landed on an "AI Brief" — a prose restatement of the answer already on the left — with the populated image grid no longer reachable. Guarding the panel arrays was not enough: they held the right data while the tab bar had discarded the way to see it. Archy tabs now lead, and the title follows the leading tab. The prose stays; it just is not the only thing shown. Tab order follows bucket size. A node with 13 photos and 2 tracks opened on Songs and titled itself "2 Songs" for a 15-item answer. Web search never worked embedded. `searchWeb` hardcoded `/api/web-search` while every other call is built from BASE_URL. Under `/aiui/` that asked the HOST for a path only the AIUI-scoped nginx location serves, so it hit the node's API gate for a 403 and the CSP refused the connection on top. Now BASE-relative. Additionally, the embedded path skips the client-side search entirely: `streamViaArchy` sends only the user's text, so the system prompt those results were folded into is never transmitted — it was a round trip and a console error per turn whose output provably reached no model. Web search for the embedded path belongs node-side, with the other tools. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c810b514ed
commit
b1c5d13850
@@ -671,7 +671,13 @@ export function useAI() {
|
||||
|
||||
let systemPrompt = buildSystemPrompt(chatStore)
|
||||
let clientSearchSucceeded = false
|
||||
if (chatStore.webSearchEnabled && userText.trim()) {
|
||||
// Embedded in Archy, the NODE owns the tool loop and `streamViaArchy`
|
||||
// sends only the user's text — this system prompt, and anything folded
|
||||
// into it, is never transmitted. So a client-side search here cost a
|
||||
// round trip and a CSP console error on every single turn while its
|
||||
// results provably reached no model. Web search for the embedded path
|
||||
// belongs node-side, next to the other tools.
|
||||
if (chatStore.webSearchEnabled && userText.trim() && !archyBridge.isInArchy()) {
|
||||
const results = await searchWeb(userText)
|
||||
if (results.length > 0) {
|
||||
systemPrompt += formatWebSearchContext(results)
|
||||
@@ -794,7 +800,7 @@ export function useAI() {
|
||||
chatStore.isStreaming = true
|
||||
|
||||
let systemPrompt = buildSystemPrompt(chatStore)
|
||||
if (chatStore.webSearchEnabled && lastUserMsg.content.trim()) {
|
||||
if (chatStore.webSearchEnabled && lastUserMsg.content.trim() && !archyBridge.isInArchy()) {
|
||||
const results = await searchWeb(lastUserMsg.content)
|
||||
if (results.length > 0) {
|
||||
systemPrompt += formatWebSearchContext(results)
|
||||
|
||||
@@ -168,10 +168,36 @@ export function useContentPanel() {
|
||||
}
|
||||
|
||||
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, hasNostr, hasApps, hasCode, hasRecipes)
|
||||
// Real node content outranks anything inferred from the reply text.
|
||||
//
|
||||
// `setArchyContent` runs first and puts the node's films/songs/podcasts/
|
||||
// images on the tab bar; this function then ran and REPLACED the bar with
|
||||
// its own regex-derived tabs. Asking "show me my own shared content"
|
||||
// therefore ended on an "AI Brief" — a prose restatement — while the
|
||||
// populated image grid was no longer reachable. Guarding the panel*
|
||||
// arrays (above) was not enough: the arrays held the right data and the
|
||||
// tab bar had thrown away the way to see it.
|
||||
// Ordered by how much the node actually returned, not by a fixed type
|
||||
// preference: "show me my own shared content" on a node with 13 photos
|
||||
// and 2 tracks opened on Songs, so the answer's own subject was one
|
||||
// click away and the heading read "2 Songs" for a 15-item reply.
|
||||
const archyTabs: ContentTab[] = archyContentActive.value
|
||||
? ([
|
||||
['film', panelFilms.value.length],
|
||||
['song', panelSongs.value.length],
|
||||
['podcast', panelPodcasts.value.length],
|
||||
['image', panelImages.value.length],
|
||||
] as [ContentTab, number][])
|
||||
.filter(([, n]) => n > 0)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([t]) => t)
|
||||
: []
|
||||
const inferredTabs = tabs.filter((t) => !archyTabs.includes(t))
|
||||
const orderedTabs = [...archyTabs, ...inferredTabs]
|
||||
// Always append Prompt tab as the rightmost tab
|
||||
const tabsWithPrompt = tabs.length > 0 ? [...tabs, 'prompt' as const] : ['prompt' as const]
|
||||
const tabsWithPrompt = orderedTabs.length > 0 ? [...orderedTabs, 'prompt' as const] : ['prompt' as const]
|
||||
availableTabs.value = tabsWithPrompt
|
||||
activeTab.value = tabs[0] ?? 'prompt'
|
||||
activeTab.value = orderedTabs[0] ?? 'prompt'
|
||||
|
||||
const showFilms = tabs.includes('film')
|
||||
const showBooks = tabs.includes('book')
|
||||
@@ -246,9 +272,26 @@ export function useContentPanel() {
|
||||
else if (visibleNews.length > 0) contentType.value = 'film'
|
||||
else contentType.value = 'film'
|
||||
|
||||
// Title follows the primary (first) tab
|
||||
const primary = tabs[0]
|
||||
if (primary === 'place' && visiblePlaces.length > 0) {
|
||||
// Title follows the primary (first) tab — which, when the node supplied
|
||||
// content, is an Archy tab. Titling from `tabs[0]` (the regex path's
|
||||
// own first tab) would name a grid that is no longer the one on screen.
|
||||
const primary = orderedTabs[0]
|
||||
if (archyTabs.includes(primary as ContentTab)) {
|
||||
if (primary === 'image') panelTitle.value = `${panelImages.value.length} Images`
|
||||
else if (primary === 'film') {
|
||||
panelTitle.value = panelFilms.value.length === 1
|
||||
? panelFilms.value[0]!.title
|
||||
: `${panelFilms.value.length} Films`
|
||||
} else if (primary === 'song') {
|
||||
panelTitle.value = panelSongs.value.length === 1
|
||||
? panelSongs.value[0]!.title
|
||||
: `${panelSongs.value.length} Songs`
|
||||
} else {
|
||||
panelTitle.value = panelPodcasts.value.length === 1
|
||||
? panelPodcasts.value[0]!.title
|
||||
: `${panelPodcasts.value.length} Podcasts`
|
||||
}
|
||||
} else if (primary === 'place' && visiblePlaces.length > 0) {
|
||||
panelTitle.value = visiblePlaces.length === 1 ? visiblePlaces[0].name : `${visiblePlaces.length} Places`
|
||||
} else if (primary === 'tvshow' && visibleTVSeries.length > 0) {
|
||||
panelTitle.value = visibleTVSeries.length === 1 ? visibleTVSeries[0].title : `${visibleTVSeries.length} TV Series`
|
||||
@@ -329,21 +372,32 @@ export function useContentPanel() {
|
||||
// GAP-FOUND). Only touches these three refs when Archy actually
|
||||
// supplied non-empty content — an empty/never-granted library must not
|
||||
// force the panel open on every mount.
|
||||
const archyTabs: ContentTab[] = []
|
||||
if (panelFilms.value.length > 0) archyTabs.push('film')
|
||||
if (panelSongs.value.length > 0) archyTabs.push('song')
|
||||
if (panelPodcasts.value.length > 0) archyTabs.push('podcast')
|
||||
if (panelImages.value.length > 0) archyTabs.push('image')
|
||||
// Largest bucket first — same reasoning as updatePanelFromText's copy:
|
||||
// the tab that opens should be the one holding most of the answer.
|
||||
const archyTabs: ContentTab[] = ([
|
||||
['film', panelFilms.value.length],
|
||||
['song', panelSongs.value.length],
|
||||
['podcast', panelPodcasts.value.length],
|
||||
['image', panelImages.value.length],
|
||||
] as [ContentTab, number][])
|
||||
.filter(([, n]) => n > 0)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([t]) => t)
|
||||
if (archyTabs.length > 0) {
|
||||
availableTabs.value = [...archyTabs, 'prompt']
|
||||
if (!archyTabs.includes(activeTab.value)) activeTab.value = archyTabs[0]!
|
||||
if (panelFilms.value.length === 1) panelTitle.value = panelFilms.value[0]!.title
|
||||
else if (panelFilms.value.length > 1) panelTitle.value = `${panelFilms.value.length} Films`
|
||||
else if (panelSongs.value.length === 1) panelTitle.value = panelSongs.value[0]!.title
|
||||
else if (panelSongs.value.length > 1) panelTitle.value = `${panelSongs.value.length} Songs`
|
||||
else if (panelPodcasts.value.length === 1) panelTitle.value = panelPodcasts.value[0]!.title
|
||||
else if (panelPodcasts.value.length > 1) panelTitle.value = `${panelPodcasts.value.length} Podcasts`
|
||||
else if (panelImages.value.length > 0) panelTitle.value = `${panelImages.value.length} Images`
|
||||
const lead = archyTabs[0]!
|
||||
if (lead === 'image') panelTitle.value = `${panelImages.value.length} Images`
|
||||
else if (lead === 'film') {
|
||||
panelTitle.value = panelFilms.value.length === 1
|
||||
? panelFilms.value[0]!.title : `${panelFilms.value.length} Films`
|
||||
} else if (lead === 'song') {
|
||||
panelTitle.value = panelSongs.value.length === 1
|
||||
? panelSongs.value[0]!.title : `${panelSongs.value.length} Songs`
|
||||
} else {
|
||||
panelTitle.value = panelPodcasts.value.length === 1
|
||||
? panelPodcasts.value[0]!.title : `${panelPodcasts.value.length} Podcasts`
|
||||
}
|
||||
panelOpen.value = true
|
||||
} else if (archyContentLoading.value) {
|
||||
// The turn asked the node for content and got none back. Leaving the
|
||||
|
||||
@@ -7,11 +7,20 @@ export interface WebSearchResult {
|
||||
imgSrc?: string
|
||||
}
|
||||
|
||||
// Every other endpoint in this app is built from BASE_URL; this one was
|
||||
// hardcoded to a leading slash, so embedded (BASE_URL `/aiui/`) it asked
|
||||
// the HOST for `/api/web-search` instead of `/aiui/api/web-search`. Only
|
||||
// the `/aiui/`-scoped location proxies SearXNG, so the request hit the
|
||||
// node's own API gate and came back 403 — and the CSP, which allows the
|
||||
// AIUI-scoped path, refused the connection on top. Web search could never
|
||||
// have worked embedded, no matter what SearXNG was doing.
|
||||
const BASE = import.meta.env.BASE_URL || '/'
|
||||
|
||||
export async function searchWeb(query: string): Promise<WebSearchResult[]> {
|
||||
if (!query.trim()) return []
|
||||
try {
|
||||
const params = new URLSearchParams({ q: query.trim() })
|
||||
const res = await apiFetch(`/api/web-search?${params}`, { signal: AbortSignal.timeout(10000) })
|
||||
const res = await apiFetch(`${BASE}api/web-search?${params}`, { signal: AbortSignal.timeout(10000) })
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '')
|
||||
console.warn('[AIUI web-search]', res.status, body)
|
||||
|
||||
Reference in New Issue
Block a user