diff --git a/packages/web-ui/example/src/main.ts b/packages/web-ui/example/src/main.ts index 8a8b181..f2c7c6d 100644 --- a/packages/web-ui/example/src/main.ts +++ b/packages/web-ui/example/src/main.ts @@ -290,6 +290,7 @@ chatPanel.setAgent(agent, { case "agent_end": if (typing) typing.hide(); + saveSession(); break; case "turn_start": @@ -379,9 +380,31 @@ async function saveSession() { if (!agent) return; const msgs = agent.getMessages(); const title = msgs.find((m: AgentMessage) => m.role === "user")?.content?.toString().slice(0, 50) || "New chat"; - const state = agent.getState(); - const id = state.sessionId || crypto.randomUUID(); - await sessionsStore.save(id, { state, title }); + const agentState = agent.getState(); + const id = agentState.sessionId || crypto.randomUUID(); + const now = new Date().toISOString(); + try { + const sessionData = { + id, + title, + model: agentState.model, + thinkingLevel: agentState.thinkingLevel, + messages: agentState.messages || [], + createdAt: now, + lastModified: now, + }; + const sessionMeta = { + id, + title, + createdAt: now, + lastModified: now, + messageCount: msgs.length, + usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, + thinkingLevel: agentState.thinkingLevel || "off", + preview: msgs[msgs.length - 1]?.content?.toString().slice(0, 100) || "", + }; + await sessionsStore.save(sessionData as any, sessionMeta as any); + } catch (_e) { /* store may not be ready */ } const sidebar = document.querySelector("jae-session-sidebar") as JaeSessionSidebar; if (sidebar) { sidebar.addSession({ id, title, date: new Date().toLocaleDateString(), pinned: false }); @@ -391,7 +414,19 @@ async function saveSession() { async function refreshSidebar() { const sidebar = document.querySelector("jae-session-sidebar") as JaeSessionSidebar; - if (sidebar) sidebar.refresh(); + if (!sidebar) return; + try { + const allMeta = await sessionsStore.getAllMetadata(); + for (const meta of allMeta) { + sidebar.addSession({ + id: meta.id, + title: meta.title || "New chat", + date: meta.lastModified ? new Date(meta.lastModified).toLocaleDateString() : new Date().toLocaleDateString(), + pinned: false, + }); + } + } catch (_e) { /* fallback if store not ready */ } + sidebar.refresh(); } function newSession() { @@ -586,7 +621,7 @@ function renderApp() { html`
-
+
JAE { (e.currentTarget as HTMLElement).style.transform = "scale(1.15) rotate(5deg)"; }} @mouseleave=${(e: Event) => { (e.currentTarget as HTMLElement).style.transform = ""; }} /> JAE diff --git a/packages/web-ui/src/dialogs/ModelSelector.ts b/packages/web-ui/src/dialogs/ModelSelector.ts index 2d7764e..d8b3adc 100644 --- a/packages/web-ui/src/dialogs/ModelSelector.ts +++ b/packages/web-ui/src/dialogs/ModelSelector.ts @@ -456,10 +456,10 @@ export class ModelSelector extends DialogBase {
- ${icon(Brain, "sm")} - ${icon(ImageIcon, "sm")} - ${icon(Wrench, "sm")} - ${icon(Code, "sm")} + ${icon(Brain, "sm")} + ${icon(ImageIcon, "sm")} + ${icon(Wrench, "sm")} + ${icon(Code, "sm")} ${this.formatTokens(model.contextWindow)}K/${this.formatTokens(model.maxTokens)}K
${formatModelCost(model.cost)} diff --git a/packages/web-ui/src/tools/web-search.ts b/packages/web-ui/src/tools/web-search.ts index 2053126..91cd302 100644 --- a/packages/web-ui/src/tools/web-search.ts +++ b/packages/web-ui/src/tools/web-search.ts @@ -30,31 +30,70 @@ interface WebSearchParams { async function fetchDuckDuckGo(query: string, limit: number): Promise { const encoded = encodeURIComponent(query); - const res = await fetch( - `https://api.duckduckgo.com/?q=${encoded}&format=json&no_redirect=1&no_html=1&skip_disambig=1`, - ); - if (!res.ok) throw new Error(`Search returned ${res.status}`); - const data = (await res.json()) as any; const results: WebSearchResult[] = []; - if (data.AbstractText && data.AbstractURL) { - results.push({ title: data.Heading || query, url: data.AbstractURL, snippet: data.AbstractText }); - } - for (const topic of data.RelatedTopics || []) { - if (results.length >= limit) break; - if (topic.FirstURL && topic.Text) { - results.push({ title: topic.Text.split(" - ")[0], url: topic.FirstURL, snippet: topic.Text }); - } else if (topic.Topics) { - for (const sub of topic.Topics) { + + // Method 1: DuckDuckGo HTML lite (most reliable) + try { + const res = await fetch(`https://lite.duckduckgo.com/lite/?q=${encoded}`, { + headers: { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Accept": "text/html", + }, + }); + if (res.ok) { + const text = await res.text(); + // Parse HTML lite results - links are in with class="result-link" + const linkRegex = /]+rel="nofollow"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi; + const snippetRegex = /]*class="result-snippet"[^>]*>([^<]*(?:<[^>]*>[^<]*)*)<\/td>/gi; + const links: {url: string, title: string}[] = []; + const snippets: string[] = []; + let m; + while ((m = linkRegex.exec(text)) !== null) { + const url = m[1]; + const title = m[2].trim(); + if (url.startsWith("http") && !url.includes("duckduckgo.com")) { + links.push({ url, title }); + } + } + while ((m = snippetRegex.exec(text)) !== null) { + snippets.push(m[1].replace(/<[^>]+>/g, "").trim()); + } + for (let i = 0; i < Math.min(links.length, limit); i++) { + results.push({ + title: links[i].title, + url: links[i].url, + snippet: snippets[i] || "", + }); + } + if (results.length > 0) return results; + } + } catch (_e) { /* fall through to method 2 */ } + + // Method 2: DuckDuckGo Instant Answer API (fallback) + try { + const res = await fetch( + `https://api.duckduckgo.com/?q=${encoded}&format=json&no_redirect=1&no_html=1&skip_disambig=1`, + ); + if (res.ok) { + const data = (await res.json()) as any; + if (data.AbstractText && data.AbstractURL) { + results.push({ title: data.Heading || query, url: data.AbstractURL, snippet: data.AbstractText }); + } + for (const topic of data.RelatedTopics || []) { if (results.length >= limit) break; - if (sub.FirstURL && sub.Text) - results.push({ title: sub.Text.split(" - ")[0], url: sub.FirstURL, snippet: sub.Text }); + if (topic.FirstURL && topic.Text) { + results.push({ title: topic.Text.split(" - ")[0], url: topic.FirstURL, snippet: topic.Text }); + } else if (topic.Topics) { + for (const sub of topic.Topics) { + if (results.length >= limit) break; + if (sub.FirstURL && sub.Text) + results.push({ title: sub.Text.split(" - ")[0], url: sub.FirstURL, snippet: sub.Text }); + } + } } } - } - for (const r of data.Results || []) { - if (results.length >= limit) break; - if (r.FirstURL && r.Text) results.push({ title: r.Title || r.Text, url: r.FirstURL, snippet: r.Text }); - } + } catch (_e) { /* no-op */ } + return results.slice(0, limit); } @@ -89,20 +128,20 @@ class WebSearchRenderer implements ToolRenderer - ${renderHeader(state, Globe, `Web Search: ${details.query}`)} -
- ${details.results.map( - (r) => html` -
- ${r.title} - ${r.url} - ${r.snippet} -
- `, - )} -
-
`, +
+ ${renderHeader(state, Globe, `Web Search: ${details.query}`)} +
+ ${details.results.map( + (r) => html` +
+ ${r.title} + ${r.url} + ${r.snippet} +
+ `, + )} +
+
`, isCustom: false, }; }