fix: model icons use inline opacity, DDG search upgrade, session save/load/sidebar persistence, header z-index
Some checks are pending
CI / build-check-test (push) Waiting to run
Some checks are pending
CI / build-check-test (push) Waiting to run
- ModelSelector: inline style opacity:0.3 instead of Tailwind class (fixes icons always lit) - web-search: scrape lite.duckduckgo.com HTML with API fallback (fixes empty results) - main.ts: auto-save session on agent_end event - main.ts: header z-index:50 + overflow:visible (fixes View dropdown clipping) - main.ts: refreshSidebar uses getAllMetadata() from IndexedDB - main.ts: saveSession uses correct SessionsStore.save(data, metadata) API
This commit is contained in:
parent
aca28ad0ec
commit
db79dec9e1
3 changed files with 118 additions and 44 deletions
|
|
@ -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`
|
||||
<div class="flex flex-col h-screen bg-background text-foreground dark">
|
||||
<!-- HEADER -->
|
||||
<div class="flex items-center gap-2 px-3 shrink-0 border-b border-border/40 jae-glass" style="height:48px">
|
||||
<div class="flex items-center gap-2 px-3 shrink-0 border-b border-border/40 jae-glass" style="height:48px;position:relative;z-index:50;overflow:visible">
|
||||
<img src="/mascot/jae-default.png" class="w-7 h-7 rounded-full" alt="JAE" style="transition: transform 0.3s" @mouseenter=${(e: Event) => { (e.currentTarget as HTMLElement).style.transform = "scale(1.15) rotate(5deg)"; }} @mouseleave=${(e: Event) => { (e.currentTarget as HTMLElement).style.transform = ""; }} />
|
||||
<span class="text-sm font-bold text-foreground tracking-tight">JAE</span>
|
||||
<jae-mood-indicator class="ml-1"></jae-mood-indicator>
|
||||
|
|
|
|||
|
|
@ -456,10 +456,10 @@ export class ModelSelector extends DialogBase {
|
|||
</div>
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="${model.reasoning ? "" : "opacity-30"}" title="Reasoning">${icon(Brain, "sm")}</span>
|
||||
<span class="${model.input.includes("image") ? "" : "opacity-30"}" title="Vision">${icon(ImageIcon, "sm")}</span>
|
||||
<span class="${(model as any).tags?.includes("tools") ? "" : "opacity-30"}" title="Tool Use">${icon(Wrench, "sm")}</span>
|
||||
<span class="${(model as any).tags?.includes("code") || (model as any).tags?.includes("optimized-for-code") ? "" : "opacity-30"}" title="Code">${icon(Code, "sm")}</span>
|
||||
<span style="${model.reasoning ? '' : 'opacity:0.3'}" title="Reasoning">${icon(Brain, "sm")}</span>
|
||||
<span style="${model.input.includes("image") ? '' : 'opacity:0.3'}" title="Vision">${icon(ImageIcon, "sm")}</span>
|
||||
<span style="${(model as any).tags?.includes("tools") ? '' : 'opacity:0.3'}" title="Tool Use">${icon(Wrench, "sm")}</span>
|
||||
<span style="${(model as any).tags?.includes("code") || (model as any).tags?.includes("optimized-for-code") ? '' : 'opacity:0.3'}" title="Code">${icon(Code, "sm")}</span>
|
||||
<span>${this.formatTokens(model.contextWindow)}K/${this.formatTokens(model.maxTokens)}K</span>
|
||||
</div>
|
||||
<span>${formatModelCost(model.cost)}</span>
|
||||
|
|
|
|||
|
|
@ -30,31 +30,70 @@ interface WebSearchParams {
|
|||
|
||||
async function fetchDuckDuckGo(query: string, limit: number): Promise<WebSearchResult[]> {
|
||||
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 <a> with class="result-link"
|
||||
const linkRegex = /<a[^>]+rel="nofollow"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
|
||||
const snippetRegex = /<td[^>]*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<WebSearchParams, WebSearchDetail
|
|||
const details = result.details;
|
||||
return {
|
||||
content: html`
|
||||
<div class="flex flex-col gap-3">
|
||||
${renderHeader(state, Globe, `Web Search: ${details.query}`)}
|
||||
<div class="flex flex-col gap-2">
|
||||
${details.results.map(
|
||||
(r) => html`
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded border border-border bg-background">
|
||||
<a href=${r.url} target="_blank" rel="noopener" class="text-sm font-medium text-primary hover:underline">${r.title}</a>
|
||||
<span class="text-xs text-muted-foreground truncate">${r.url}</span>
|
||||
<span class="text-xs text-foreground mt-1">${r.snippet}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>`,
|
||||
<div class="flex flex-col gap-3">
|
||||
${renderHeader(state, Globe, `Web Search: ${details.query}`)}
|
||||
<div class="flex flex-col gap-2">
|
||||
${details.results.map(
|
||||
(r) => html`
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded border border-border bg-background">
|
||||
<a href=${r.url} target="_blank" rel="noopener" class="text-sm font-medium text-primary hover:underline">${r.title}</a>
|
||||
<span class="text-xs text-muted-foreground truncate">${r.url}</span>
|
||||
<span class="text-xs text-foreground mt-1">${r.snippet}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue