fix: provider filter tabs, model badge always visible, empty-state 4% fade, server deps (ws/concurrently)
Some checks are pending
CI / build-check-test (push) Waiting to run
Some checks are pending
CI / build-check-test (push) Waiting to run
This commit is contained in:
parent
1514fabd50
commit
63a773184c
30 changed files with 2271 additions and 1651 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -10011,8 +10011,10 @@
|
|||
"playwright": "^1.58.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^7.1.6"
|
||||
"vite": "^7.1.6",
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/@xterm/xterm": {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
|
||||
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
|
||||
const browserSchema = Type.Object({
|
||||
action: Type.Union([
|
||||
action: Type.Union(
|
||||
[
|
||||
Type.Literal("navigate"),
|
||||
Type.Literal("screenshot"),
|
||||
Type.Literal("click"),
|
||||
Type.Literal("type"),
|
||||
Type.Literal("content"),
|
||||
Type.Literal("close"),
|
||||
], { description: "navigate: go to URL | screenshot: capture page | click: click element | type: type into element | content: get page text | close: close browser" }),
|
||||
],
|
||||
{
|
||||
description:
|
||||
"navigate: go to URL | screenshot: capture page | click: click element | type: type into element | content: get page text | close: close browser",
|
||||
},
|
||||
),
|
||||
url: Type.Optional(Type.String({ description: "URL to navigate to (for navigate action)" })),
|
||||
selector: Type.Optional(Type.String({ description: "CSS selector (for click/type actions)" })),
|
||||
text: Type.Optional(Type.String({ description: "Text to type (for type action)" })),
|
||||
|
|
@ -55,21 +60,27 @@ async function getPage() {
|
|||
export const browserTool: AgentTool<typeof browserSchema, BrowserToolDetails> = {
|
||||
name: "browser",
|
||||
label: "Browser",
|
||||
description: "Control a headless Chromium browser. Navigate pages, take screenshots, click elements, type text, and extract page content. Requires playwright.",
|
||||
description:
|
||||
"Control a headless Chromium browser. Navigate pages, take screenshots, click elements, type text, and extract page content. Requires playwright.",
|
||||
parameters: browserSchema,
|
||||
async execute(toolCallId, params, signal) {
|
||||
const { action, url, selector, text, wait = 1000 } = params;
|
||||
|
||||
try {
|
||||
if (action === "close") {
|
||||
if (_browser) { await _browser.close(); _browser = null; _page = null; }
|
||||
if (_browser) {
|
||||
await _browser.close();
|
||||
_browser = null;
|
||||
_page = null;
|
||||
}
|
||||
return { content: [{ type: "text", text: "Browser closed." }], details: { action } };
|
||||
}
|
||||
|
||||
const page = await getPage();
|
||||
|
||||
if (action === "navigate") {
|
||||
if (!url) return { content: [{ type: "text", text: "Error: url required for navigate" }], details: { action } };
|
||||
if (!url)
|
||||
return { content: [{ type: "text", text: "Error: url required for navigate" }], details: { action } };
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
await page.waitForTimeout(wait);
|
||||
const title = await page.title();
|
||||
|
|
@ -101,15 +112,18 @@ export const browserTool: AgentTool<typeof browserSchema, BrowserToolDetails> =
|
|||
}
|
||||
|
||||
if (action === "click") {
|
||||
if (!selector) return { content: [{ type: "text", text: "Error: selector required for click" }], details: { action } };
|
||||
if (!selector)
|
||||
return { content: [{ type: "text", text: "Error: selector required for click" }], details: { action } };
|
||||
await page.click(selector, { timeout: 10000 });
|
||||
await page.waitForTimeout(wait);
|
||||
return { content: [{ type: "text", text: `Clicked: ${selector}` }], details: { action } };
|
||||
}
|
||||
|
||||
if (action === "type") {
|
||||
if (!selector) return { content: [{ type: "text", text: "Error: selector required for type" }], details: { action } };
|
||||
if (!text) return { content: [{ type: "text", text: "Error: text required for type" }], details: { action } };
|
||||
if (!selector)
|
||||
return { content: [{ type: "text", text: "Error: selector required for type" }], details: { action } };
|
||||
if (!text)
|
||||
return { content: [{ type: "text", text: "Error: text required for type" }], details: { action } };
|
||||
await page.fill(selector, text);
|
||||
await page.waitForTimeout(wait);
|
||||
return { content: [{ type: "text", text: `Typed into: ${selector}` }], details: { action } };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
|
|
@ -26,7 +25,8 @@ export interface ImageGenToolDetails {
|
|||
export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenToolDetails> = {
|
||||
name: "generate_image",
|
||||
label: "Generate Image",
|
||||
description: "Generate an image using Venice AI image models and save it to disk. Uses VENICE_API_KEY or OPENAI_API_KEY env var.",
|
||||
description:
|
||||
"Generate an image using Venice AI image models and save it to disk. Uses VENICE_API_KEY or OPENAI_API_KEY env var.",
|
||||
parameters: imageGenSchema,
|
||||
async execute(toolCallId, params, signal) {
|
||||
const {
|
||||
|
|
@ -60,7 +60,7 @@ export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenToolDetails>
|
|||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body,
|
||||
signal: signal ?? AbortSignal.timeout(60000),
|
||||
|
|
@ -74,7 +74,7 @@ export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenToolDetails>
|
|||
};
|
||||
}
|
||||
|
||||
const data = await res.json() as any;
|
||||
const data = (await res.json()) as any;
|
||||
const b64 = data?.images?.[0];
|
||||
if (!b64) {
|
||||
return {
|
||||
|
|
@ -89,7 +89,12 @@ export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenToolDetails>
|
|||
writeFileSync(filepath, Buffer.from(b64, "base64"));
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Image saved to: ${filepath}\nModel: ${model}\nPrompt: ${prompt}\nSize: ${width}x${height}` }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Image saved to: ${filepath}\nModel: ${model}\nPrompt: ${prompt}\nSize: ${width}x${height}`,
|
||||
},
|
||||
],
|
||||
details: { path: filepath, model, prompt, width, height },
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -107,13 +107,22 @@ import { createWriteTool, createWriteToolDefinition, writeTool, writeToolDefinit
|
|||
export type Tool = AgentTool<any>;
|
||||
export type ToolDef = ToolDefinition<any, any>;
|
||||
|
||||
|
||||
import { webSearchTool } from "./web-search.js";
|
||||
import { webFetchTool } from "./web-fetch.js";
|
||||
import { browserTool } from "./browser.js";
|
||||
import { imageGenTool } from "./image-gen.js";
|
||||
import { memoryTool } from "./memory.js";
|
||||
import { browserTool } from "./browser.js";
|
||||
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool, webSearchTool, webFetchTool, imageGenTool, memoryTool, browserTool];
|
||||
import { webFetchTool } from "./web-fetch.js";
|
||||
import { webSearchTool } from "./web-search.js";
|
||||
export const codingTools: Tool[] = [
|
||||
readTool,
|
||||
bashTool,
|
||||
editTool,
|
||||
writeTool,
|
||||
webSearchTool,
|
||||
webFetchTool,
|
||||
imageGenTool,
|
||||
memoryTool,
|
||||
browserTool,
|
||||
];
|
||||
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];
|
||||
|
||||
export const allTools = {
|
||||
|
|
@ -198,9 +207,14 @@ export function createAllTools(cwd: string, options?: ToolsOptions): Record<Tool
|
|||
};
|
||||
}
|
||||
|
||||
export { type BrowserToolDetails, type BrowserToolInput, browserTool } from "./browser.js";
|
||||
export { type ImageGenToolDetails, type ImageGenToolInput, imageGenTool } from "./image-gen.js";
|
||||
export { type MemoryToolDetails, type MemoryToolInput, memoryTool } from "./memory.js";
|
||||
export { type WebFetchToolDetails, type WebFetchToolInput, webFetchTool } from "./web-fetch.js";
|
||||
// ── New tools ────────────────────────────────────────────────────────────────
|
||||
export { webSearchTool, type WebSearchToolInput, type WebSearchToolDetails, type WebSearchResult } from "./web-search.js";
|
||||
export { webFetchTool, type WebFetchToolInput, type WebFetchToolDetails } from "./web-fetch.js";
|
||||
export { imageGenTool, type ImageGenToolInput, type ImageGenToolDetails } from "./image-gen.js";
|
||||
export { memoryTool, type MemoryToolInput, type MemoryToolDetails } from "./memory.js";
|
||||
export { browserTool, type BrowserToolInput, type BrowserToolDetails } from "./browser.js";
|
||||
export {
|
||||
type WebSearchResult,
|
||||
type WebSearchToolDetails,
|
||||
type WebSearchToolInput,
|
||||
webSearchTool,
|
||||
} from "./web-search.js";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
|
@ -19,7 +18,9 @@ function loadMemories(): MemoryEntry[] {
|
|||
if (!existsSync(MEMORY_FILE)) return [];
|
||||
try {
|
||||
return JSON.parse(readFileSync(MEMORY_FILE, "utf-8")) as MemoryEntry[];
|
||||
} catch { return []; }
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveMemories(entries: MemoryEntry[]): void {
|
||||
|
|
@ -31,16 +32,13 @@ function scoreMatch(entry: MemoryEntry, query: string): number {
|
|||
const q = query.toLowerCase();
|
||||
const text = (entry.content + " " + entry.tags.join(" ")).toLowerCase();
|
||||
const words = q.split(/\s+/);
|
||||
return words.filter(w => text.includes(w)).length / Math.max(words.length, 1);
|
||||
return words.filter((w) => text.includes(w)).length / Math.max(words.length, 1);
|
||||
}
|
||||
|
||||
const memorySchema = Type.Object({
|
||||
action: Type.Union([
|
||||
Type.Literal("save"),
|
||||
Type.Literal("recall"),
|
||||
Type.Literal("list"),
|
||||
Type.Literal("delete"),
|
||||
], { description: "save: store info | recall: search by query | list: show all | delete: remove by id" }),
|
||||
action: Type.Union([Type.Literal("save"), Type.Literal("recall"), Type.Literal("list"), Type.Literal("delete")], {
|
||||
description: "save: store info | recall: search by query | list: show all | delete: remove by id",
|
||||
}),
|
||||
content: Type.Optional(Type.String({ description: "Content to save (required for save action)" })),
|
||||
query: Type.Optional(Type.String({ description: "Search query (required for recall action)" })),
|
||||
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorisation (optional for save)" })),
|
||||
|
|
@ -59,14 +57,19 @@ export interface MemoryToolDetails {
|
|||
export const memoryTool: AgentTool<typeof memorySchema, MemoryToolDetails> = {
|
||||
name: "memory",
|
||||
label: "Memory",
|
||||
description: "Persistent memory across sessions. Save facts, recall by query, list all memories, or delete by ID. Stored in ~/.jae/memory/.",
|
||||
description:
|
||||
"Persistent memory across sessions. Save facts, recall by query, list all memories, or delete by ID. Stored in ~/.jae/memory/.",
|
||||
parameters: memorySchema,
|
||||
async execute(toolCallId, params, signal) {
|
||||
const { action, content, query, tags = [], id, limit = 5 } = params;
|
||||
const memories = loadMemories();
|
||||
|
||||
if (action === "save") {
|
||||
if (!content) return { content: [{ type: "text", text: "Error: content is required for save action" }], details: { action } };
|
||||
if (!content)
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: content is required for save action" }],
|
||||
details: { action },
|
||||
};
|
||||
const entry: MemoryEntry = {
|
||||
id: `mem-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
content,
|
||||
|
|
@ -75,34 +78,52 @@ export const memoryTool: AgentTool<typeof memorySchema, MemoryToolDetails> = {
|
|||
};
|
||||
memories.push(entry);
|
||||
saveMemories(memories);
|
||||
return { content: [{ type: "text", text: `Memory saved. ID: ${entry.id}` }], details: { action, id: entry.id } };
|
||||
return {
|
||||
content: [{ type: "text", text: `Memory saved. ID: ${entry.id}` }],
|
||||
details: { action, id: entry.id },
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "recall") {
|
||||
if (!query) return { content: [{ type: "text", text: "Error: query is required for recall action" }], details: { action } };
|
||||
if (!query)
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: query is required for recall action" }],
|
||||
details: { action },
|
||||
};
|
||||
const scored = memories
|
||||
.map(m => ({ m, score: scoreMatch(m, query) }))
|
||||
.filter(x => x.score > 0)
|
||||
.map((m) => ({ m, score: scoreMatch(m, query) }))
|
||||
.filter((x) => x.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit)
|
||||
.map(x => x.m);
|
||||
const text = scored.length === 0
|
||||
.map((x) => x.m);
|
||||
const text =
|
||||
scored.length === 0
|
||||
? `No memories found for: ${query}`
|
||||
: scored.map(m => `[${m.id}] ${m.content}${m.tags.length ? ` (tags: ${m.tags.join(", ")})` : ""}`).join("\n\n");
|
||||
: scored
|
||||
.map((m) => `[${m.id}] ${m.content}${m.tags.length ? ` (tags: ${m.tags.join(", ")})` : ""}`)
|
||||
.join("\n\n");
|
||||
return { content: [{ type: "text", text }], details: { action, count: scored.length } };
|
||||
}
|
||||
|
||||
if (action === "list") {
|
||||
const text = memories.length === 0
|
||||
const text =
|
||||
memories.length === 0
|
||||
? "No memories stored."
|
||||
: memories.map(m => `[${m.id}] ${m.content.slice(0, 100)}${m.content.length > 100 ? "..." : ""}`).join("\n");
|
||||
: memories
|
||||
.map((m) => `[${m.id}] ${m.content.slice(0, 100)}${m.content.length > 100 ? "..." : ""}`)
|
||||
.join("\n");
|
||||
return { content: [{ type: "text", text }], details: { action, count: memories.length } };
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
if (!id) return { content: [{ type: "text", text: "Error: id is required for delete action" }], details: { action } };
|
||||
const filtered = memories.filter(m => m.id !== id);
|
||||
if (filtered.length === memories.length) return { content: [{ type: "text", text: `No memory found with ID: ${id}` }], details: { action } };
|
||||
if (!id)
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: id is required for delete action" }],
|
||||
details: { action },
|
||||
};
|
||||
const filtered = memories.filter((m) => m.id !== id);
|
||||
if (filtered.length === memories.length)
|
||||
return { content: [{ type: "text", text: `No memory found with ID: ${id}` }], details: { action } };
|
||||
saveMemories(filtered);
|
||||
return { content: [{ type: "text", text: `Memory ${id} deleted.` }], details: { action, id } };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
|
||||
|
|
@ -23,8 +22,12 @@ function htmlToText(html: string): string {
|
|||
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
.replace(/"/g, '"').replace(/ /g, " ").replace(/'/g, "'")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/ /g, " ")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/[ \t]+/g, " ")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
|
|
@ -39,12 +42,12 @@ export const webFetchTool: AgentTool<typeof webFetchSchema, WebFetchToolDetails>
|
|||
const { url } = params;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": "JAE-Agent/1.0", "Accept": "text/html,application/xhtml+xml,text/plain,*/*" },
|
||||
headers: { "User-Agent": "JAE-Agent/1.0", Accept: "text/html,application/xhtml+xml,text/plain,*/*" },
|
||||
signal: signal ?? AbortSignal.timeout(15000),
|
||||
redirect: "follow",
|
||||
});
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
let body = await res.text();
|
||||
const body = await res.text();
|
||||
let text: string;
|
||||
if (contentType.includes("html")) {
|
||||
text = htmlToText(body);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^7.1.6"
|
||||
"vite": "^7.1.6",
|
||||
"ws": "*",
|
||||
"concurrently": "^9.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,28 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
@customElement('jae-browser-panel')
|
||||
@customElement("jae-browser-panel")
|
||||
export class JaeBrowserPanel extends LitElement {
|
||||
@state() private url = '';
|
||||
@state() private inputUrl = '';
|
||||
@state() private screenshot = '';
|
||||
@state() private url = "";
|
||||
@state() private inputUrl = "";
|
||||
@state() private screenshot = "";
|
||||
@state() private loading = false;
|
||||
@state() private connected = false;
|
||||
@state() private error = '';
|
||||
@state() private error = "";
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
private imgEl: HTMLImageElement | null = null;
|
||||
|
||||
createRenderRoot() { return this; }
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.style.display = 'flex';
|
||||
this.style.flexDirection = 'column';
|
||||
this.style.height = '100%';
|
||||
this.style.minHeight = '0';
|
||||
this.style.display = "flex";
|
||||
this.style.flexDirection = "column";
|
||||
this.style.height = "100%";
|
||||
this.style.minHeight = "0";
|
||||
this.connect();
|
||||
}
|
||||
|
||||
|
|
@ -30,29 +32,46 @@ export class JaeBrowserPanel extends LitElement {
|
|||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket('ws://localhost:7702');
|
||||
this.ws.onopen = () => { this.connected = true; this.requestUpdate(); };
|
||||
this.ws.onclose = () => { this.connected = false; this.requestUpdate(); };
|
||||
this.ws.onerror = () => { this.connected = false; this.error = 'Browser server not running.'; this.requestUpdate(); };
|
||||
this.ws = new WebSocket("ws://localhost:7702");
|
||||
this.ws.onopen = () => {
|
||||
this.connected = true;
|
||||
this.requestUpdate();
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.connected = false;
|
||||
this.requestUpdate();
|
||||
};
|
||||
this.ws.onerror = () => {
|
||||
this.connected = false;
|
||||
this.error = "Browser server not running.";
|
||||
this.requestUpdate();
|
||||
};
|
||||
this.ws.onmessage = (e) => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.type === 'screenshot') {
|
||||
if (m.type === "screenshot") {
|
||||
this.loading = false;
|
||||
this.screenshot = `data:image/jpeg;base64,${m.data}`;
|
||||
this.url = m.url;
|
||||
this.inputUrl = m.url;
|
||||
this.error = '';
|
||||
this.error = "";
|
||||
this.requestUpdate();
|
||||
}
|
||||
if (m.type === "loading") {
|
||||
this.loading = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
if (m.type === "error") {
|
||||
this.loading = false;
|
||||
this.error = m.msg;
|
||||
this.requestUpdate();
|
||||
}
|
||||
if (m.type === 'loading') { this.loading = true; this.requestUpdate(); }
|
||||
if (m.type === 'error') { this.loading = false; this.error = m.msg; this.requestUpdate(); }
|
||||
};
|
||||
}
|
||||
|
||||
navigate(url?: string) {
|
||||
const target = url || this.inputUrl;
|
||||
if (!target) return;
|
||||
this.ws?.send(JSON.stringify({ type: 'navigate', url: target }));
|
||||
this.ws?.send(JSON.stringify({ type: "navigate", url: target }));
|
||||
this.loading = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
|
@ -64,48 +83,62 @@ export class JaeBrowserPanel extends LitElement {
|
|||
const scaleY = 800 / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
this.ws?.send(JSON.stringify({ type: 'click', x, y }));
|
||||
this.ws?.send(JSON.stringify({ type: "click", x, y }));
|
||||
this.loading = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private handleScroll(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
this.ws?.send(JSON.stringify({ type: 'scroll', dy: e.deltaY }));
|
||||
this.ws?.send(JSON.stringify({ type: "scroll", dy: e.deltaY }));
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="flex items-center gap-1 px-2 py-1 border-b border-border shrink-0 bg-muted/30" style="height:40px">
|
||||
<button class="p-1 rounded hover:bg-secondary" title="Back" @click=${() => this.ws?.send(JSON.stringify({ type:'back' }))}>
|
||||
<button class="p-1 rounded hover:bg-secondary" title="Back" @click=${() => this.ws?.send(JSON.stringify({ type: "back" }))}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<button class="p-1 rounded hover:bg-secondary" title="Forward" @click=${() => this.ws?.send(JSON.stringify({ type:'fwd' }))}>
|
||||
<button class="p-1 rounded hover:bg-secondary" title="Forward" @click=${() => this.ws?.send(JSON.stringify({ type: "fwd" }))}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
<button class="p-1 rounded hover:bg-secondary" title="Reload" @click=${() => this.ws?.send(JSON.stringify({ type:'reload' }))}>
|
||||
<button class="p-1 rounded hover:bg-secondary" title="Reload" @click=${() => this.ws?.send(JSON.stringify({ type: "reload" }))}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
</button>
|
||||
<input
|
||||
class="flex-1 text-xs border border-border rounded px-2 py-1 bg-background text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||
type="text"
|
||||
.value=${this.inputUrl}
|
||||
@input=${(e: Event) => { this.inputUrl = (e.target as HTMLInputElement).value; }}
|
||||
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter') this.navigate(); }}
|
||||
@input=${(e: Event) => {
|
||||
this.inputUrl = (e.target as HTMLInputElement).value;
|
||||
}}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") this.navigate();
|
||||
}}
|
||||
placeholder="Enter URL and press Enter..."
|
||||
/>
|
||||
<div class="w-2 h-2 rounded-full ml-1 ${this.connected ? 'bg-green-500' : 'bg-red-500'}" title="${this.connected ? 'connected' : 'disconnected'}"></div>
|
||||
<div class="w-2 h-2 rounded-full ml-1 ${this.connected ? "bg-green-500" : "bg-red-500"}" title="${this.connected ? "connected" : "disconnected"}"></div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto min-h-0 relative bg-white">
|
||||
${this.loading ? html`
|
||||
${
|
||||
this.loading
|
||||
? html`
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
||||
<div class="text-sm text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
` : html``}
|
||||
${this.error ? html`
|
||||
`
|
||||
: html``
|
||||
}
|
||||
${
|
||||
this.error
|
||||
? html`
|
||||
<div class="p-4 text-sm text-red-500">${this.error}</div>
|
||||
` : html``}
|
||||
${this.screenshot ? html`
|
||||
`
|
||||
: html``
|
||||
}
|
||||
${
|
||||
this.screenshot
|
||||
? html`
|
||||
<img
|
||||
src=${this.screenshot}
|
||||
class="w-full cursor-crosshair"
|
||||
|
|
@ -114,12 +147,16 @@ export class JaeBrowserPanel extends LitElement {
|
|||
@click=${this.handleImgClick}
|
||||
@wheel=${this.handleScroll}
|
||||
/>
|
||||
` : !this.error ? html`
|
||||
`
|
||||
: !this.error
|
||||
? html`
|
||||
<div class="flex flex-col items-center justify-center h-full text-muted-foreground gap-2">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
<p class="text-sm">Enter a URL above to browse</p>
|
||||
</div>
|
||||
` : html``}
|
||||
`
|
||||
: html``
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
|
|
@ -20,7 +19,9 @@ export class CommandPalette extends LitElement {
|
|||
|
||||
private commands: Command[] = [];
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
setCommands(commands: Command[]) {
|
||||
this.commands = commands;
|
||||
|
|
@ -45,10 +46,11 @@ export class CommandPalette extends LitElement {
|
|||
get filteredCommands(): Command[] {
|
||||
if (!this.query) return this.commands;
|
||||
const q = this.query.toLowerCase();
|
||||
return this.commands.filter(c =>
|
||||
return this.commands.filter(
|
||||
(c) =>
|
||||
c.label.toLowerCase().includes(q) ||
|
||||
c.description?.toLowerCase().includes(q) ||
|
||||
c.keywords?.some(k => k.toLowerCase().includes(q))
|
||||
c.keywords?.some((k) => k.toLowerCase().includes(q)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +78,9 @@ export class CommandPalette extends LitElement {
|
|||
const cmds = this.filteredCommands;
|
||||
|
||||
return html`
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center pt-20" @click=${(e: Event) => { if (e.target === e.currentTarget) this.hide(); }}>
|
||||
<div class="fixed inset-0 z-50 flex items-start justify-center pt-20" @click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) this.hide();
|
||||
}}>
|
||||
<div class="w-full max-w-xl bg-popover border border-border rounded-xl shadow-2xl overflow-hidden">
|
||||
<div class="flex items-center gap-3 px-4 py-3 border-b border-border">
|
||||
<span class="text-muted-foreground text-sm">⌘</span>
|
||||
|
|
@ -85,20 +89,29 @@ export class CommandPalette extends LitElement {
|
|||
placeholder="Type a command..."
|
||||
class="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
.value=${this.query}
|
||||
@input=${(e: Event) => { this.query = (e.target as HTMLInputElement).value; this.selectedIndex = 0; }}
|
||||
@input=${(e: Event) => {
|
||||
this.query = (e.target as HTMLInputElement).value;
|
||||
this.selectedIndex = 0;
|
||||
}}
|
||||
@keydown=${this.handleKeyDown}
|
||||
/>
|
||||
<kbd class="text-xs text-muted-foreground border border-border rounded px-1.5 py-0.5">ESC</kbd>
|
||||
</div>
|
||||
<div class="max-h-80 overflow-y-auto py-1">
|
||||
${cmds.length === 0 ? html`<div class="px-4 py-6 text-center text-sm text-muted-foreground">No commands found</div>` : ""}
|
||||
${cmds.map((cmd, i) => html`
|
||||
${cmds.map(
|
||||
(cmd, i) => html`
|
||||
<button
|
||||
class="w-full flex items-center justify-between px-4 py-2.5 text-sm hover:bg-secondary transition-colors text-left ${
|
||||
i === this.selectedIndex ? "bg-secondary" : ""
|
||||
}"
|
||||
@click=${() => { cmd.action(); this.hide(); }}
|
||||
@mouseover=${() => { this.selectedIndex = i; }}
|
||||
@click=${() => {
|
||||
cmd.action();
|
||||
this.hide();
|
||||
}}
|
||||
@mouseover=${() => {
|
||||
this.selectedIndex = i;
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-medium">${cmd.label}</span>
|
||||
|
|
@ -106,7 +119,8 @@ export class CommandPalette extends LitElement {
|
|||
</div>
|
||||
${cmd.shortcut ? html`<kbd class="text-xs text-muted-foreground border border-border rounded px-1.5 py-0.5 shrink-0">${cmd.shortcut}</kbd>` : ""}
|
||||
</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<div class="px-4 py-2 border-t border-border text-xs text-muted-foreground flex items-center gap-4">
|
||||
<span>↑↓ Navigate</span>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import type { Agent } from "@jaeswift/jae-agent-core";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import type { Agent } from "@jaeswift/jae-agent-core";
|
||||
|
||||
export interface UsageSnapshot {
|
||||
inputTokens: number;
|
||||
|
|
@ -32,7 +31,9 @@ export class CostTracker extends LitElement {
|
|||
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
bindAgent(agent: Agent) {
|
||||
if (this.unsubscribe) this.unsubscribe();
|
||||
|
|
@ -52,8 +53,12 @@ export class CostTracker extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
get totalTokens() { return this.inputTokens + this.outputTokens; }
|
||||
get estimatedCost() { return estimateCost(this.modelId, this.inputTokens, this.outputTokens); }
|
||||
get totalTokens() {
|
||||
return this.inputTokens + this.outputTokens;
|
||||
}
|
||||
get estimatedCost() {
|
||||
return estimateCost(this.modelId, this.inputTokens, this.outputTokens);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.inputTokens = 0;
|
||||
|
|
@ -66,14 +71,19 @@ export class CostTracker extends LitElement {
|
|||
return html`
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-2 py-1 text-xs rounded hover:bg-secondary transition-colors text-muted-foreground"
|
||||
@click=${() => { this.expanded = !this.expanded; this.requestUpdate(); }}
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
title="Token usage & cost"
|
||||
>
|
||||
<span class="font-mono">${this.totalTokens > 0 ? this.totalTokens.toLocaleString() : "0"} tok</span>
|
||||
<span class="text-muted-foreground/50">|</span>
|
||||
<span class="font-mono">$${cost.toFixed(4)}</span>
|
||||
</button>
|
||||
${this.expanded ? html`
|
||||
${
|
||||
this.expanded
|
||||
? html`
|
||||
<div class="absolute top-12 right-2 z-50 bg-popover border border-border rounded-lg shadow-xl p-4 min-w-52">
|
||||
<div class="font-semibold text-sm mb-3 flex items-center justify-between">
|
||||
<span>Token Usage</span>
|
||||
|
|
@ -87,7 +97,9 @@ export class CostTracker extends LitElement {
|
|||
<div class="flex justify-between"><span class="text-muted-foreground">Requests</span><span class="font-mono">${this.requestCount}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("jae-empty-state")
|
||||
export class JaeEmptyState extends LitElement {
|
||||
@property({ type: Boolean }) visible = true;
|
||||
@property({ type: Boolean }) faded = false;
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private _suggestions = [
|
||||
{ icon: "💻", text: "Write me a TypeScript function that debounces API calls" },
|
||||
|
|
@ -18,9 +21,19 @@ export class JaeEmptyState extends LitElement {
|
|||
|
||||
override render() {
|
||||
if (!this.visible) return html``;
|
||||
|
||||
if (this.faded) {
|
||||
return html`
|
||||
<div class="flex flex-col items-center justify-center h-full select-none"
|
||||
style="pointer-events:none">
|
||||
<img src="/mascot/jae-default.png" alt="JAE"
|
||||
style="width:10rem;height:auto;opacity:0.04;filter:grayscale(40%)" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col items-center justify-center h-full min-h-[60vh] px-4 pb-8 select-none">
|
||||
<!-- Mascot -->
|
||||
<div class="relative mb-2 group">
|
||||
<img
|
||||
src="/mascot/jae-default.png"
|
||||
|
|
@ -28,16 +41,13 @@ export class JaeEmptyState extends LitElement {
|
|||
class="w-40 h-auto drop-shadow-2xl transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="text-2xl font-bold text-foreground mb-1">Hey, I'm JAE</h1>
|
||||
<p class="text-muted-foreground text-sm mb-8 text-center max-w-sm">
|
||||
Your AI coding agent. I can write code, search the web, generate images, and a whole lot more.
|
||||
</p>
|
||||
|
||||
<!-- Suggestion chips -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 w-full max-w-xl">
|
||||
${this._suggestions.map(s => html`
|
||||
${this._suggestions.map(
|
||||
(s) => html`
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-secondary/50 hover:bg-secondary hover:border-primary/40 transition-all text-left text-sm group"
|
||||
@click=${() => this.dispatchEvent(new CustomEvent("suggestion", { detail: s.text, bubbles: true, composed: true }))}
|
||||
|
|
@ -45,7 +55,8 @@ export class JaeEmptyState extends LitElement {
|
|||
<span class="text-lg shrink-0">${s.icon}</span>
|
||||
<span class="text-muted-foreground group-hover:text-foreground transition-colors leading-tight">${s.text}</span>
|
||||
</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
|
|
@ -6,53 +5,79 @@ import { customElement, state } from "lit/decorators.js";
|
|||
export class KeyboardShortcuts extends LitElement {
|
||||
@state() private open = false;
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
show() { this.open = true; this.requestUpdate(); }
|
||||
hide() { this.open = false; this.requestUpdate(); }
|
||||
toggle() { this.open = !this.open; this.requestUpdate(); }
|
||||
show() {
|
||||
this.open = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
hide() {
|
||||
this.open = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private readonly shortcuts = [
|
||||
{ group: "General", items: [
|
||||
{
|
||||
group: "General",
|
||||
items: [
|
||||
{ key: "Cmd+K", desc: "Open command palette" },
|
||||
{ key: "?", desc: "Show keyboard shortcuts" },
|
||||
{ key: "Ctrl+L", desc: "Open model selector" },
|
||||
{ key: "Esc", desc: "Close dialogs / abort generation" },
|
||||
]},
|
||||
{ group: "Sessions", items: [
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Sessions",
|
||||
items: [
|
||||
{ key: "Ctrl+N", desc: "New session" },
|
||||
{ key: "Ctrl+H", desc: "Session history" },
|
||||
{ key: "Ctrl+E", desc: "Export session" },
|
||||
]},
|
||||
{ group: "Tools & Features", items: [
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Tools & Features",
|
||||
items: [
|
||||
{ key: "/memory", desc: "Open memory manager" },
|
||||
{ key: "/clear", desc: "Clear conversation" },
|
||||
{ key: "/model", desc: "Switch model" },
|
||||
]},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
override render() {
|
||||
if (!this.open) return html``;
|
||||
return html`
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => { if (e.target === e.currentTarget) this.hide(); }}>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) this.hide();
|
||||
}}>
|
||||
<div class="bg-popover border border-border rounded-xl shadow-2xl p-6 max-w-lg w-full mx-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="font-semibold text-lg">Keyboard Shortcuts</h2>
|
||||
<button class="text-muted-foreground hover:text-foreground" @click=${() => this.hide()}>✕</button>
|
||||
</div>
|
||||
${this.shortcuts.map(group => html`
|
||||
${this.shortcuts.map(
|
||||
(group) => html`
|
||||
<div class="mb-4">
|
||||
<div class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">${group.group}</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
${group.items.map(item => html`
|
||||
${group.items.map(
|
||||
(item) => html`
|
||||
<div class="flex items-center justify-between py-1">
|
||||
<span class="text-sm">${item.desc}</span>
|
||||
<kbd class="text-xs bg-secondary border border-border rounded px-2 py-0.5 font-mono">${item.key}</kbd>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
|
|
@ -19,7 +18,10 @@ async function openDB(): Promise<IDBDatabase> {
|
|||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME, { keyPath: "id" });
|
||||
req.onsuccess = () => { _db = req.result; resolve(_db); };
|
||||
req.onsuccess = () => {
|
||||
_db = req.result;
|
||||
resolve(_db);
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
|
@ -61,7 +63,9 @@ export class MemoryManager extends LitElement {
|
|||
@state() private newTags = "";
|
||||
@state() private filter = "";
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
async show() {
|
||||
this.open = true;
|
||||
|
|
@ -71,23 +75,31 @@ export class MemoryManager extends LitElement {
|
|||
this.loading = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
hide() { this.open = false; this.requestUpdate(); }
|
||||
hide() {
|
||||
this.open = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
get filtered() {
|
||||
if (!this.filter) return this.entries;
|
||||
const q = this.filter.toLowerCase();
|
||||
return this.entries.filter(e => e.content.toLowerCase().includes(q) || e.tags.some(t => t.toLowerCase().includes(q)));
|
||||
return this.entries.filter(
|
||||
(e) => e.content.toLowerCase().includes(q) || e.tags.some((t) => t.toLowerCase().includes(q)),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteEntry(id: string) {
|
||||
await memoryDelete(id);
|
||||
this.entries = this.entries.filter(e => e.id !== id);
|
||||
this.entries = this.entries.filter((e) => e.id !== id);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async addEntry() {
|
||||
if (!this.newContent.trim()) return;
|
||||
const tags = this.newTags.split(",").map(t => t.trim()).filter(Boolean);
|
||||
const tags = this.newTags
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
await memorySave(this.newContent.trim(), tags);
|
||||
this.newContent = "";
|
||||
this.newTags = "";
|
||||
|
|
@ -99,7 +111,9 @@ export class MemoryManager extends LitElement {
|
|||
if (!this.open) return html``;
|
||||
const entries = this.filtered;
|
||||
return html`
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => { if (e.target === e.currentTarget) this.hide(); }}>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) this.hide();
|
||||
}}>
|
||||
<div class="bg-popover border border-border rounded-xl shadow-2xl w-full max-w-2xl mx-4 flex flex-col max-h-[80vh]">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border shrink-0">
|
||||
<h2 class="font-semibold text-lg">🧠 Memory Manager</h2>
|
||||
|
|
@ -107,35 +121,43 @@ export class MemoryManager extends LitElement {
|
|||
</div>
|
||||
<div class="px-6 py-3 border-b border-border shrink-0">
|
||||
<input type="text" placeholder="Filter memories..." class="w-full bg-secondary rounded-lg px-3 py-2 text-sm outline-none"
|
||||
.value=${this.filter} @input=${(e: Event) => { this.filter = (e.target as HTMLInputElement).value; }} />
|
||||
.value=${this.filter} @input=${(e: Event) => {
|
||||
this.filter = (e.target as HTMLInputElement).value;
|
||||
}} />
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||
${this.loading ? html`<div class="text-center text-muted-foreground py-8">Loading...</div>` : ""}
|
||||
${!this.loading && entries.length === 0 ? html`<div class="text-center text-muted-foreground py-8">No memories stored yet</div>` : ""}
|
||||
<div class="flex flex-col gap-2">
|
||||
${entries.map(entry => html`
|
||||
${entries.map(
|
||||
(entry) => html`
|
||||
<div class="flex gap-3 p-3 rounded-lg border border-border bg-background group">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm">${entry.content}</div>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs text-muted-foreground">${entry.timestamp.slice(0, 10)}</span>
|
||||
${entry.tags.map(tag => html`<span class="text-xs bg-secondary px-1.5 py-0.5 rounded">${tag}</span>`)}
|
||||
${entry.tags.map((tag) => html`<span class="text-xs bg-secondary px-1.5 py-0.5 rounded">${tag}</span>`)}
|
||||
</div>
|
||||
</div>
|
||||
<button class="shrink-0 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@click=${() => this.deleteEntry(entry.id)} title="Delete">🗑</button>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-t border-border shrink-0">
|
||||
<div class="flex flex-col gap-2">
|
||||
<textarea placeholder="New memory content..." class="w-full bg-secondary rounded-lg px-3 py-2 text-sm outline-none resize-none"
|
||||
rows="2" .value=${this.newContent}
|
||||
@input=${(e: Event) => { this.newContent = (e.target as HTMLTextAreaElement).value; }}></textarea>
|
||||
@input=${(e: Event) => {
|
||||
this.newContent = (e.target as HTMLTextAreaElement).value;
|
||||
}}></textarea>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" placeholder="Tags (comma separated)" class="flex-1 bg-secondary rounded-lg px-3 py-2 text-sm outline-none"
|
||||
.value=${this.newTags} @input=${(e: Event) => { this.newTags = (e.target as HTMLInputElement).value; }} />
|
||||
.value=${this.newTags} @input=${(e: Event) => {
|
||||
this.newTags = (e.target as HTMLInputElement).value;
|
||||
}} />
|
||||
<button class="bg-primary text-primary-foreground px-4 py-2 rounded-lg text-sm font-medium hover:opacity-90"
|
||||
@click=${() => this.addEntry()}>Save</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import type { AgentMessage } from "@jaeswift/jae-agent-core";
|
||||
|
||||
export function exportSessionAsMarkdown(messages: AgentMessage[], title: string): void {
|
||||
|
|
@ -18,7 +17,10 @@ export function exportSessionAsMarkdown(messages: AgentMessage[], title: string)
|
|||
} else if (msg.role === "assistant") {
|
||||
const m = msg as any;
|
||||
const textBlocks = Array.isArray(m.content)
|
||||
? m.content.filter((b: any) => b.type === "text").map((b: any) => b.text).join("\n")
|
||||
? m.content
|
||||
.filter((b: any) => b.type === "text")
|
||||
.map((b: any) => b.text)
|
||||
.join("\n")
|
||||
: m.content || "";
|
||||
lines.push(`## 🤖 Assistant`, ``, textBlocks, ``, `---`, ``);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import type { SessionMetadata } from "@jaeswift/jae-web-ui";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
@customElement("jae-session-sidebar")
|
||||
export class JaeSessionSidebar extends LitElement {
|
||||
|
|
@ -13,12 +13,18 @@ export class JaeSessionSidebar extends LitElement {
|
|||
@state() private _pinnedIds: Set<string> = new Set();
|
||||
@state() private _confirmDelete: string | null = null;
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const raw = localStorage.getItem("jae-pinned-sessions");
|
||||
if (raw) { try { this._pinnedIds = new Set(JSON.parse(raw)); } catch {} }
|
||||
if (raw) {
|
||||
try {
|
||||
this._pinnedIds = new Set(JSON.parse(raw));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
setSessions(sessions: SessionMetadata[]) {
|
||||
|
|
@ -43,7 +49,10 @@ export class JaeSessionSidebar extends LitElement {
|
|||
} else {
|
||||
this._confirmDelete = id;
|
||||
this.requestUpdate();
|
||||
setTimeout(() => { this._confirmDelete = null; this.requestUpdate(); }, 3000);
|
||||
setTimeout(() => {
|
||||
this._confirmDelete = null;
|
||||
this.requestUpdate();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,9 +67,11 @@ export class JaeSessionSidebar extends LitElement {
|
|||
|
||||
override render() {
|
||||
if (this.collapsed) return html``;
|
||||
const pinned = this._sessions.filter(s => this._pinnedIds.has(s.id))
|
||||
const pinned = this._sessions
|
||||
.filter((s) => this._pinnedIds.has(s.id))
|
||||
.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
||||
const rest = this._sessions.filter(s => !this._pinnedIds.has(s.id))
|
||||
const rest = this._sessions
|
||||
.filter((s) => !this._pinnedIds.has(s.id))
|
||||
.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
||||
const sorted = [...pinned, ...rest];
|
||||
|
||||
|
|
@ -78,18 +89,26 @@ export class JaeSessionSidebar extends LitElement {
|
|||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto py-1">
|
||||
${sorted.length === 0 ? html`
|
||||
${
|
||||
sorted.length === 0
|
||||
? html`
|
||||
<div class="px-4 py-10 text-center">
|
||||
<div class="text-3xl mb-2">💬</div>
|
||||
<div class="text-xs text-muted-foreground">No chats yet</div>
|
||||
</div>
|
||||
` : sorted.map(s => html`
|
||||
`
|
||||
: sorted.map(
|
||||
(s) => html`
|
||||
<div class="group relative flex items-center gap-1 px-2 py-1.5 mx-1 my-0.5 rounded-lg cursor-pointer transition-colors select-none
|
||||
${s.id === this.currentSessionId ? "bg-secondary" : "hover:bg-secondary/50"}"
|
||||
@click=${() => this.onLoadSession?.(s.id)}>
|
||||
${this._pinnedIds.has(s.id) ? html`
|
||||
${
|
||||
this._pinnedIds.has(s.id)
|
||||
? html`
|
||||
<div class="absolute left-0.5 top-1/2 -translate-y-1/2 w-1 h-4 rounded-full bg-primary/60"></div>
|
||||
` : html``}
|
||||
`
|
||||
: html``
|
||||
}
|
||||
<div class="flex-1 min-w-0 pl-1">
|
||||
<div class="text-xs font-medium truncate" style="color: inherit">${s.title || "Untitled"}</div>
|
||||
<div class="text-[10px] text-muted-foreground leading-tight">${this._fmt(s.lastModified)}</div>
|
||||
|
|
@ -118,7 +137,9 @@ export class JaeSessionSidebar extends LitElement {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="px-3 py-1.5 border-t border-border shrink-0">
|
||||
<div class="text-[10px] text-muted-foreground text-center">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
@customElement('jae-terminal-panel')
|
||||
@customElement("jae-terminal-panel")
|
||||
export class JaeTerminalPanel extends LitElement {
|
||||
@state() private connected = false;
|
||||
@state() private connecting = false;
|
||||
|
|
@ -16,14 +16,16 @@ export class JaeTerminalPanel extends LitElement {
|
|||
private container: HTMLElement | null = null;
|
||||
private resizeObs: ResizeObserver | null = null;
|
||||
|
||||
createRenderRoot() { return this; }
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.style.display = 'flex';
|
||||
this.style.flexDirection = 'column';
|
||||
this.style.height = '100%';
|
||||
this.style.minHeight = '0';
|
||||
this.style.display = "flex";
|
||||
this.style.flexDirection = "column";
|
||||
this.style.height = "100%";
|
||||
this.style.minHeight = "0";
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
|
|
@ -35,7 +37,8 @@ export class JaeTerminalPanel extends LitElement {
|
|||
this.resizeObs?.disconnect();
|
||||
this.ws?.close();
|
||||
this.term?.dispose();
|
||||
this.term = null; this.ws = null;
|
||||
this.term = null;
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
|
|
@ -44,17 +47,20 @@ export class JaeTerminalPanel extends LitElement {
|
|||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
|
||||
this.container = this.querySelector('#xterm-container') as HTMLElement;
|
||||
if (!this.container) { this.connecting = false; return; }
|
||||
this.container = this.querySelector("#xterm-container") as HTMLElement;
|
||||
if (!this.container) {
|
||||
this.connecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
this.term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontFamily: '"Fira Code", "Cascadia Code", monospace',
|
||||
fontSize: 13,
|
||||
theme: isDark
|
||||
? { background: '#09090b', foreground: '#e4e4e7', cursor: '#a1a1aa' }
|
||||
: { background: '#ffffff', foreground: '#18181b', cursor: '#52525b' },
|
||||
? { background: "#09090b", foreground: "#e4e4e7", cursor: "#a1a1aa" }
|
||||
: { background: "#ffffff", foreground: "#18181b", cursor: "#52525b" },
|
||||
});
|
||||
this.fitAddon = new FitAddon();
|
||||
this.term.loadAddon(this.fitAddon);
|
||||
|
|
@ -62,50 +68,62 @@ export class JaeTerminalPanel extends LitElement {
|
|||
this.term.open(this.container);
|
||||
this.fitAddon.fit();
|
||||
|
||||
this.ws = new WebSocket('ws://localhost:7701');
|
||||
this.ws = new WebSocket("ws://localhost:7701");
|
||||
this.ws.onopen = () => {
|
||||
this.connected = true; this.connecting = false;
|
||||
this.connected = true;
|
||||
this.connecting = false;
|
||||
this.requestUpdate();
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.connected = false; this.connecting = false;
|
||||
this.term?.write('\r\n\x1b[31m[disconnected]\x1b[0m\r\n');
|
||||
this.connected = false;
|
||||
this.connecting = false;
|
||||
this.term?.write("\r\n\x1b[31m[disconnected]\x1b[0m\r\n");
|
||||
this.requestUpdate();
|
||||
};
|
||||
this.ws.onerror = () => {
|
||||
this.connecting = false; this.connected = false;
|
||||
this.term?.write('\r\n\x1b[31m[connection error - is terminal server running?]\x1b[0m\r\n');
|
||||
this.connecting = false;
|
||||
this.connected = false;
|
||||
this.term?.write("\r\n\x1b[31m[connection error - is terminal server running?]\x1b[0m\r\n");
|
||||
this.requestUpdate();
|
||||
};
|
||||
this.ws.onmessage = (e) => {
|
||||
const m = JSON.parse(e.data);
|
||||
if (m.type === 'data') this.term?.write(m.data);
|
||||
if (m.type === 'exit') this.term?.write(`\r\n\x1b[33m[process exited: ${m.code}]\x1b[0m\r\n`);
|
||||
if (m.type === "data") this.term?.write(m.data);
|
||||
if (m.type === "exit") this.term?.write(`\r\n\x1b[33m[process exited: ${m.code}]\x1b[0m\r\n`);
|
||||
};
|
||||
this.term.onData((d) => { this.ws?.send(JSON.stringify({ type: 'input', data: d })); });
|
||||
this.term.onData((d) => {
|
||||
this.ws?.send(JSON.stringify({ type: "input", data: d }));
|
||||
});
|
||||
|
||||
this.resizeObs = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.fitAddon?.fit();
|
||||
if (this.term && this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'resize', cols: this.term.cols, rows: this.term.rows }));
|
||||
this.ws.send(JSON.stringify({ type: "resize", cols: this.term.cols, rows: this.term.rows }));
|
||||
}
|
||||
});
|
||||
});
|
||||
this.resizeObs.observe(this.container);
|
||||
}
|
||||
|
||||
disconnect() { this.destroyTerminal(); this.connected = false; this.requesting = false; this.requestUpdate(); }
|
||||
disconnect() {
|
||||
this.destroyTerminal();
|
||||
this.connected = false;
|
||||
this.requesting = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="flex items-center gap-2 px-2 py-1 border-b border-border shrink-0 bg-muted/30" style="height:34px">
|
||||
<div class="w-2 h-2 rounded-full ${this.connected ? 'bg-green-500' : 'bg-red-500'}"></div>
|
||||
<div class="w-2 h-2 rounded-full ${this.connected ? "bg-green-500" : "bg-red-500"}"></div>
|
||||
<span class="text-xs font-mono text-muted-foreground">bash</span>
|
||||
<div class="flex-1"></div>
|
||||
${!this.connected && !this.connecting
|
||||
${
|
||||
!this.connected && !this.connecting
|
||||
? html`<button class="text-xs px-2 py-0.5 rounded bg-primary text-primary-foreground hover:opacity-90" @click=${() => this.connect()}>Connect</button>`
|
||||
: html``}
|
||||
: html``
|
||||
}
|
||||
${this.connecting ? html`<span class="text-xs text-muted-foreground">Connecting...</span>` : html``}
|
||||
${this.connected ? html`<button class="text-xs px-2 py-0.5 rounded bg-secondary text-secondary-foreground hover:opacity-90" @click=${() => this.disconnect()}>Kill</button>` : html``}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
export interface UtilityVisibility {
|
||||
|
|
@ -19,19 +19,28 @@ export class JaeUtilityToggle extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) open = false;
|
||||
|
||||
protected override createRenderRoot() { return this; }
|
||||
protected override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private _toggle(key: keyof UtilityVisibility) {
|
||||
this.visibility = { ...this.visibility, [key]: !this.visibility[key] };
|
||||
this.dispatchEvent(new CustomEvent("visibility-change", {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("visibility-change", {
|
||||
detail: this.visibility,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _items: { key: keyof UtilityVisibility; label: string; icon: string; desc: string }[] = [
|
||||
{ key: "showToolCalls", label: "Tool Calls", icon: "🔧", desc: "Show web search, image gen & other tool results" },
|
||||
{
|
||||
key: "showToolCalls",
|
||||
label: "Tool Calls",
|
||||
icon: "🔧",
|
||||
desc: "Show web search, image gen & other tool results",
|
||||
},
|
||||
{ key: "showThinking", label: "Thinking", icon: "🧠", desc: "Show model reasoning / thinking blocks" },
|
||||
{ key: "showSystemMessages", label: "System Messages", icon: "⚙️", desc: "Show system notifications and prompts" },
|
||||
{ key: "showTimestamps", label: "Timestamps", icon: "🕐", desc: "Show message timestamps" },
|
||||
|
|
@ -44,7 +53,10 @@ export class JaeUtilityToggle extends LitElement {
|
|||
<!-- Toggle button -->
|
||||
<button
|
||||
title="Message filters"
|
||||
@click=${() => { this.open = !this.open; this.requestUpdate(); }}
|
||||
@click=${() => {
|
||||
this.open = !this.open;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs border border-border bg-secondary/60 hover:bg-secondary hover:border-primary/40 transition-all text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
|
@ -57,7 +69,9 @@ export class JaeUtilityToggle extends LitElement {
|
|||
</button>
|
||||
|
||||
<!-- Dropdown panel -->
|
||||
${this.open ? html`
|
||||
${
|
||||
this.open
|
||||
? html`
|
||||
<div class="absolute top-full right-0 mt-2 w-72 rounded-xl border border-border bg-background shadow-2xl z-50 overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-border bg-secondary/30">
|
||||
|
|
@ -66,14 +80,18 @@ export class JaeUtilityToggle extends LitElement {
|
|||
<div class="text-sm font-semibold text-foreground">Message Filters</div>
|
||||
<div class="text-xs text-muted-foreground">Control what JAE shows you</div>
|
||||
</div>
|
||||
<button @click=${() => { this.open = false; this.requestUpdate(); }} class="ml-auto text-muted-foreground hover:text-foreground transition-colors">
|
||||
<button @click=${() => {
|
||||
this.open = false;
|
||||
this.requestUpdate();
|
||||
}} class="ml-auto text-muted-foreground hover:text-foreground transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<div class="p-2">
|
||||
${this._items.map(item => html`
|
||||
${this._items.map(
|
||||
(item) => html`
|
||||
<button
|
||||
class="w-full flex items-start gap-3 px-3 py-2.5 rounded-lg hover:bg-secondary/60 transition-colors text-left"
|
||||
@click=${() => this._toggle(item.key)}
|
||||
|
|
@ -84,11 +102,12 @@ export class JaeUtilityToggle extends LitElement {
|
|||
<div class="text-xs text-muted-foreground leading-tight">${item.desc}</div>
|
||||
</div>
|
||||
<!-- Toggle switch -->
|
||||
<div class="shrink-0 mt-0.5 w-9 h-5 rounded-full transition-colors duration-200 flex items-center px-0.5 ${this.visibility[item.key] ? 'bg-primary' : 'bg-muted'}">
|
||||
<div class="w-4 h-4 rounded-full bg-white shadow transition-transform duration-200 ${this.visibility[item.key] ? 'translate-x-4' : 'translate-x-0'}"></div>
|
||||
<div class="shrink-0 mt-0.5 w-9 h-5 rounded-full transition-colors duration-200 flex items-center px-0.5 ${this.visibility[item.key] ? "bg-primary" : "bg-muted"}">
|
||||
<div class="w-4 h-4 rounded-full bg-white shadow transition-transform duration-200 ${this.visibility[item.key] ? "translate-x-4" : "translate-x-0"}"></div>
|
||||
</div>
|
||||
</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
|
|
@ -97,8 +116,13 @@ export class JaeUtilityToggle extends LitElement {
|
|||
</div>
|
||||
</div>
|
||||
<!-- Click-outside overlay -->
|
||||
<div class="fixed inset-0 z-40" @click=${() => { this.open = false; this.requestUpdate(); }}></div>
|
||||
` : html``}
|
||||
<div class="fixed inset-0 z-40" @click=${() => {
|
||||
this.open = false;
|
||||
this.requestUpdate();
|
||||
}}></div>
|
||||
`
|
||||
: html``
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,31 +21,31 @@ import {
|
|||
import { html, render } from "lit";
|
||||
import { Brain, Download, History, Keyboard, Plus, Settings } from "lucide";
|
||||
import "./app.css";
|
||||
import { createImageGenTool, createTTSTool, createWebSearchTool } from "@jaeswift/jae-web-ui";
|
||||
import { icon } from "@mariozechner/mini-lit";
|
||||
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
|
||||
import { Input } from "@mariozechner/mini-lit/dist/Input.js";
|
||||
import type { CommandPalette } from "./components/command-palette.js";
|
||||
import type { CostTracker } from "./components/cost-tracker.js";
|
||||
import type { KeyboardShortcuts } from "./components/keyboard-shortcuts.js";
|
||||
import type { MemoryManager } from "./components/memory-manager.js";
|
||||
import { exportSessionAsJson, exportSessionAsMarkdown } from "./components/session-export.js";
|
||||
import { customConvertToLlm, registerCustomMessageRenderers } from "./custom-messages.js";
|
||||
import { createWebSearchTool, createImageGenTool, createTTSTool } from "@jaeswift/jae-web-ui";
|
||||
import { CommandPalette } from "./components/command-palette.js";
|
||||
import { KeyboardShortcuts } from "./components/keyboard-shortcuts.js";
|
||||
import { MemoryManager } from "./components/memory-manager.js";
|
||||
import { CostTracker } from "./components/cost-tracker.js";
|
||||
import { exportSessionAsMarkdown, exportSessionAsJson } from "./components/session-export.js";
|
||||
import "./components/command-palette.js";
|
||||
import "./components/keyboard-shortcuts.js";
|
||||
import "./components/memory-manager.js";
|
||||
import "./components/cost-tracker.js";
|
||||
import { JaeEmptyState } from "./components/empty-state.js";
|
||||
import { JaeUtilityToggle, type UtilityVisibility } from "./components/utility-toggle.js";
|
||||
import type { JaeUtilityToggle, UtilityVisibility } from "./components/utility-toggle.js";
|
||||
import "./components/empty-state.js";
|
||||
import "./components/utility-toggle.js";
|
||||
import { JaeSessionSidebar } from "./components/session-sidebar.js";
|
||||
import type { JaeSessionSidebar } from "./components/session-sidebar.js";
|
||||
import "./components/session-sidebar.js";
|
||||
|
||||
import { JaeTerminalPanel } from './components/terminal-panel.js';
|
||||
import { JaeBrowserPanel } from './components/browser-panel.js';
|
||||
import './components/terminal-panel.js';
|
||||
import './components/browser-panel.js';
|
||||
import type { JaeBrowserPanel } from "./components/browser-panel.js";
|
||||
import type { JaeTerminalPanel } from "./components/terminal-panel.js";
|
||||
import "./components/terminal-panel.js";
|
||||
import "./components/browser-panel.js";
|
||||
|
||||
registerCustomMessageRenderers();
|
||||
|
||||
|
|
@ -80,12 +80,12 @@ let currentSessionId: string | undefined;
|
|||
let currentTitle = "";
|
||||
let isEditingTitle = false;
|
||||
let agent: Agent;
|
||||
let rightPanel: 'none' | 'terminal' | 'browser' = 'none';
|
||||
let rightPanel: "none" | "terminal" | "browser" = "none";
|
||||
let sidebarWidth = 220;
|
||||
let rightPanelWidth = 480;
|
||||
let hasStarted = false;
|
||||
let terminalPanel: JaeTerminalPanel | null = null;
|
||||
let browserPanel: JaeBrowserPanel | null = null;
|
||||
const browserPanel: JaeBrowserPanel | null = null;
|
||||
let chatPanel: ChatPanel;
|
||||
let agentUnsubscribe: (() => void) | undefined;
|
||||
|
||||
|
|
@ -95,7 +95,9 @@ const memoryManager = document.createElement("memory-manager") as MemoryManager;
|
|||
const costTracker = document.createElement("cost-tracker") as CostTracker;
|
||||
const sidebar = document.createElement("jae-session-sidebar") as JaeSessionSidebar;
|
||||
|
||||
sidebar.onLoadSession = async (id: string) => { await loadSession(id); };
|
||||
sidebar.onLoadSession = async (id: string) => {
|
||||
await loadSession(id);
|
||||
};
|
||||
sidebar.onNewSession = () => newSession();
|
||||
sidebar.addEventListener("delete-session", async (e: Event) => {
|
||||
const id = (e as CustomEvent<string>).detail;
|
||||
|
|
@ -132,9 +134,21 @@ const refreshSidebar = async () => {
|
|||
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||
const meta = e.metaKey || e.ctrlKey;
|
||||
if (meta && e.key === "k") { e.preventDefault(); commandPalette.show(); return; }
|
||||
if (meta && e.key === "e") { e.preventDefault(); handleExport(); return; }
|
||||
if (meta && e.key === "n") { e.preventDefault(); newSession(); return; }
|
||||
if (meta && e.key === "k") {
|
||||
e.preventDefault();
|
||||
commandPalette.show();
|
||||
return;
|
||||
}
|
||||
if (meta && e.key === "e") {
|
||||
e.preventDefault();
|
||||
handleExport();
|
||||
return;
|
||||
}
|
||||
if (meta && e.key === "n") {
|
||||
e.preventDefault();
|
||||
newSession();
|
||||
return;
|
||||
}
|
||||
if (e.key === "?" && !(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) {
|
||||
keyboardShortcuts.toggle();
|
||||
}
|
||||
|
|
@ -142,14 +156,72 @@ window.addEventListener("keydown", (e: KeyboardEvent) => {
|
|||
|
||||
function setupCommands() {
|
||||
commandPalette.setCommands([
|
||||
{ id: "new-session", label: "New Session", description: "Start a fresh conversation", shortcut: "Ctrl+N", keywords: ["new", "fresh", "start"], action: newSession },
|
||||
{ id: "sessions", label: "Session History", description: "Browse and load past sessions", shortcut: "Ctrl+H", keywords: ["history", "sessions", "past"], action: () => SessionListDialog.open(async (id) => await loadSession(id), (id) => { if (id === currentSessionId) newSession(); }) },
|
||||
{ id: "export-md", label: "Export as Markdown", description: "Download current session as .md", shortcut: "Ctrl+E", keywords: ["export", "download", "markdown"], action: () => handleExport("markdown") },
|
||||
{ id: "export-json", label: "Export as JSON", description: "Download current session as .json", keywords: ["export", "download", "json"], action: () => handleExport("json") },
|
||||
{ id: "memory", label: "Memory Manager", description: "Browse and manage stored memories", keywords: ["memory", "remember", "recall"], action: () => memoryManager.show() },
|
||||
{ id: "settings", label: "Settings", description: "Configure providers and models", keywords: ["settings", "config", "provider", "api", "model"], action: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]) },
|
||||
{ id: "shortcuts", label: "Keyboard Shortcuts", description: "View all keyboard shortcuts", shortcut: "?", keywords: ["keyboard", "shortcuts", "help"], action: () => keyboardShortcuts.show() },
|
||||
{ id: "cost", label: "Token Usage & Cost", description: "View API usage stats for this session", keywords: ["tokens", "cost", "usage"], action: () => costTracker.dispatchEvent(new MouseEvent("click")) },
|
||||
{
|
||||
id: "new-session",
|
||||
label: "New Session",
|
||||
description: "Start a fresh conversation",
|
||||
shortcut: "Ctrl+N",
|
||||
keywords: ["new", "fresh", "start"],
|
||||
action: newSession,
|
||||
},
|
||||
{
|
||||
id: "sessions",
|
||||
label: "Session History",
|
||||
description: "Browse and load past sessions",
|
||||
shortcut: "Ctrl+H",
|
||||
keywords: ["history", "sessions", "past"],
|
||||
action: () =>
|
||||
SessionListDialog.open(
|
||||
async (id) => await loadSession(id),
|
||||
(id) => {
|
||||
if (id === currentSessionId) newSession();
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "export-md",
|
||||
label: "Export as Markdown",
|
||||
description: "Download current session as .md",
|
||||
shortcut: "Ctrl+E",
|
||||
keywords: ["export", "download", "markdown"],
|
||||
action: () => handleExport("markdown"),
|
||||
},
|
||||
{
|
||||
id: "export-json",
|
||||
label: "Export as JSON",
|
||||
description: "Download current session as .json",
|
||||
keywords: ["export", "download", "json"],
|
||||
action: () => handleExport("json"),
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
label: "Memory Manager",
|
||||
description: "Browse and manage stored memories",
|
||||
keywords: ["memory", "remember", "recall"],
|
||||
action: () => memoryManager.show(),
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "Settings",
|
||||
description: "Configure providers and models",
|
||||
keywords: ["settings", "config", "provider", "api", "model"],
|
||||
action: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]),
|
||||
},
|
||||
{
|
||||
id: "shortcuts",
|
||||
label: "Keyboard Shortcuts",
|
||||
description: "View all keyboard shortcuts",
|
||||
shortcut: "?",
|
||||
keywords: ["keyboard", "shortcuts", "help"],
|
||||
action: () => keyboardShortcuts.show(),
|
||||
},
|
||||
{
|
||||
id: "cost",
|
||||
label: "Token Usage & Cost",
|
||||
description: "View API usage stats for this session",
|
||||
keywords: ["tokens", "cost", "usage"],
|
||||
action: () => costTracker.dispatchEvent(new MouseEvent("click")),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -166,8 +238,12 @@ const generateTitle = (messages: AgentMessage[]): string => {
|
|||
if (!firstUserMsg || (firstUserMsg.role !== "user" && firstUserMsg.role !== "user-with-attachments")) return "";
|
||||
let text = "";
|
||||
const content = firstUserMsg.content;
|
||||
if (typeof content === "string") { text = content; }
|
||||
else { const textBlocks = content.filter((c: any) => c.type === "text"); text = textBlocks.map((c: any) => c.text || "").join(" "); }
|
||||
if (typeof content === "string") {
|
||||
text = content;
|
||||
} else {
|
||||
const textBlocks = content.filter((c: any) => c.type === "text");
|
||||
text = textBlocks.map((c: any) => c.text || "").join(" ");
|
||||
}
|
||||
text = text.trim();
|
||||
if (!text) return "";
|
||||
const sentenceEnd = text.search(/[.!?]/);
|
||||
|
|
@ -187,21 +263,37 @@ const saveSession = async () => {
|
|||
if (!shouldSaveSession(state.messages)) return;
|
||||
try {
|
||||
const sessionData = {
|
||||
id: currentSessionId, title: currentTitle, model: state.model!,
|
||||
thinkingLevel: state.thinkingLevel, messages: state.messages,
|
||||
createdAt: new Date().toISOString(), lastModified: new Date().toISOString(),
|
||||
id: currentSessionId,
|
||||
title: currentTitle,
|
||||
model: state.model!,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
messages: state.messages,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastModified: new Date().toISOString(),
|
||||
};
|
||||
const metadata = {
|
||||
id: currentSessionId, title: currentTitle,
|
||||
createdAt: sessionData.createdAt, lastModified: sessionData.lastModified,
|
||||
id: currentSessionId,
|
||||
title: currentTitle,
|
||||
createdAt: sessionData.createdAt,
|
||||
lastModified: sessionData.lastModified,
|
||||
messageCount: state.messages.length,
|
||||
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
||||
modelId: state.model?.id || null, thinkingLevel: state.thinkingLevel,
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
modelId: state.model?.id || null,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
preview: generateTitle(state.messages),
|
||||
};
|
||||
await storage.sessions.save(sessionData, metadata);
|
||||
await refreshSidebar();
|
||||
} catch (err) { console.error("Failed to save session:", err); }
|
||||
} catch (err) {
|
||||
console.error("Failed to save session:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const updateUrl = (sessionId: string) => {
|
||||
|
|
@ -214,7 +306,8 @@ const createAgent = async (initialState?: Partial<AgentState>) => {
|
|||
if (agentUnsubscribe) agentUnsubscribe();
|
||||
agent = new Agent({
|
||||
initialState: initialState || {
|
||||
systemPrompt: "You are JAE, a helpful AI assistant and coding agent with access to tools including web search, image generation, JavaScript REPL, text-to-speech, and artifact creation. Use these tools whenever helpful.",
|
||||
systemPrompt:
|
||||
"You are JAE, a helpful AI assistant and coding agent with access to tools including web search, image generation, JavaScript REPL, text-to-speech, and artifact creation. Use these tools whenever helpful.",
|
||||
model: getModel("venice", "llama-3.3-70b"),
|
||||
thinkingLevel: "off",
|
||||
messages: [],
|
||||
|
|
@ -262,8 +355,10 @@ const loadSession = async (sessionId: string): Promise<boolean> => {
|
|||
const metadata = await storage.sessions.getMetadata(sessionId);
|
||||
currentTitle = metadata?.title || "";
|
||||
await createAgent({
|
||||
model: sessionData.model, thinkingLevel: sessionData.thinkingLevel,
|
||||
messages: sessionData.messages, tools: [],
|
||||
model: sessionData.model,
|
||||
thinkingLevel: sessionData.thinkingLevel,
|
||||
messages: sessionData.messages,
|
||||
tools: [],
|
||||
});
|
||||
sidebar.currentSessionId = currentSessionId;
|
||||
updateUrl(sessionId);
|
||||
|
|
@ -287,13 +382,15 @@ const handleSuggestion = (e: Event) => {
|
|||
chatPanel.agentInterface.setInput(text);
|
||||
// Focus the textarea after injection
|
||||
requestAnimationFrame(() => {
|
||||
const ta = document.querySelector("message-editor textarea") as HTMLTextAreaElement
|
||||
|| document.querySelector("textarea") as HTMLTextAreaElement;
|
||||
const ta =
|
||||
(document.querySelector("message-editor textarea") as HTMLTextAreaElement) ||
|
||||
(document.querySelector("textarea") as HTMLTextAreaElement);
|
||||
if (ta) ta.focus();
|
||||
});
|
||||
} else {
|
||||
const ta = document.querySelector("message-editor textarea") as HTMLTextAreaElement
|
||||
|| document.querySelector("textarea") as HTMLTextAreaElement;
|
||||
const ta =
|
||||
(document.querySelector("message-editor textarea") as HTMLTextAreaElement) ||
|
||||
(document.querySelector("textarea") as HTMLTextAreaElement);
|
||||
if (ta) {
|
||||
ta.value = text;
|
||||
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
|
@ -311,18 +408,78 @@ return m.name || m.id || null;
|
|||
const renderApp = () => {
|
||||
const app = document.getElementById("app");
|
||||
if (!app) return;
|
||||
const hasMessages = hasStarted || !!(agent?.state?.messages?.length);
|
||||
render(html`
|
||||
const hasMessages = hasStarted || !!agent?.state?.messages?.length;
|
||||
render(
|
||||
html`
|
||||
<div class="w-full h-screen flex flex-col bg-background text-foreground overflow-hidden">
|
||||
<div class="flex items-center justify-between border-b border-border shrink-0" style="height:44px">
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
${Button({ variant: "ghost", size: "sm", children: icon(History, "sm"), onClick: () => SessionListDialog.open(async (id) => { await loadSession(id); }, (id) => { if (id === currentSessionId) newSession(); }), title: "Sessions" })}
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
children: icon(History, "sm"),
|
||||
onClick: () =>
|
||||
SessionListDialog.open(
|
||||
async (id) => {
|
||||
await loadSession(id);
|
||||
},
|
||||
(id) => {
|
||||
if (id === currentSessionId) newSession();
|
||||
},
|
||||
),
|
||||
title: "Sessions",
|
||||
})}
|
||||
${Button({ variant: "ghost", size: "sm", children: icon(Plus, "sm"), onClick: newSession, title: "New Session (Ctrl+N)" })}
|
||||
${currentTitle
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/mascot/jae-default.png" alt="JAE" class="w-7 h-auto header-logo cursor-pointer" />
|
||||
<span class="text-base font-semibold text-foreground">JAE</span>
|
||||
${getModelLabel() ? html`<span class="ml-1 text-[11px] text-muted-foreground bg-muted/80 px-1.5 py-0.5 rounded font-mono truncate max-w-[180px] cursor-pointer hover:bg-muted" title="${getModelLabel()}">${getModelLabel()}</span>` : html``}
|
||||
</div>
|
||||
${
|
||||
currentTitle
|
||||
? isEditingTitle
|
||||
? html`<div class="flex items-center gap-2">${Input({ type: "text", value: currentTitle, className: "text-sm w-64", onChange: async (e: Event) => { const v = (e.target as HTMLInputElement).value.trim(); if (v && v !== currentTitle && storage.sessions && currentSessionId) { await storage.sessions.updateTitle(currentSessionId, v); currentTitle = v; await refreshSidebar(); } isEditingTitle = false; renderApp(); }, onKeyDown: async (e: KeyboardEvent) => { if (e.key === "Enter") { const v = (e.target as HTMLInputElement).value.trim(); if (v && v !== currentTitle && storage.sessions && currentSessionId) { await storage.sessions.updateTitle(currentSessionId, v); currentTitle = v; await refreshSidebar(); } isEditingTitle = false; renderApp(); } else if (e.key === "Escape") { isEditingTitle = false; renderApp(); } } })}</div>`
|
||||
: html`<button class="px-2 py-1 text-sm text-foreground hover:bg-secondary rounded transition-colors max-w-xs truncate" @click=${() => { isEditingTitle = true; renderApp(); requestAnimationFrame(() => { const inp = app.querySelector('input[type="text"]') as HTMLInputElement; if (inp) { inp.focus(); inp.select(); } }); }} title="Click to edit">${currentTitle}</button>`
|
||||
: html`<div class="flex items-center gap-2"><img src="/mascot/jae-default.png" alt="JAE" class="w-7 h-auto header-logo cursor-pointer" /><span class="text-base font-semibold text-foreground">JAE</span>${getModelLabel() ? html`<span class="ml-1 text-[11px] text-muted-foreground bg-muted/80 px-1.5 py-0.5 rounded font-mono truncate max-w-[180px]" title="${getModelLabel()}">${getModelLabel()}</span>` : html``}</div>`
|
||||
? html`<div class="flex items-center gap-2">${Input({
|
||||
type: "text",
|
||||
value: currentTitle,
|
||||
className: "text-sm w-64",
|
||||
onChange: async (e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value.trim();
|
||||
if (v && v !== currentTitle && storage.sessions && currentSessionId) {
|
||||
await storage.sessions.updateTitle(currentSessionId, v);
|
||||
currentTitle = v;
|
||||
await refreshSidebar();
|
||||
}
|
||||
isEditingTitle = false;
|
||||
renderApp();
|
||||
},
|
||||
onKeyDown: async (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
const v = (e.target as HTMLInputElement).value.trim();
|
||||
if (v && v !== currentTitle && storage.sessions && currentSessionId) {
|
||||
await storage.sessions.updateTitle(currentSessionId, v);
|
||||
currentTitle = v;
|
||||
await refreshSidebar();
|
||||
}
|
||||
isEditingTitle = false;
|
||||
renderApp();
|
||||
} else if (e.key === "Escape") {
|
||||
isEditingTitle = false;
|
||||
renderApp();
|
||||
}
|
||||
},
|
||||
})}</div>`
|
||||
: html`<button class="px-2 py-1 text-sm text-foreground hover:bg-secondary rounded transition-colors max-w-xs truncate" @click=${() => {
|
||||
isEditingTitle = true;
|
||||
renderApp();
|
||||
requestAnimationFrame(() => {
|
||||
const inp = app.querySelector('input[type="text"]') as HTMLInputElement;
|
||||
if (inp) {
|
||||
inp.focus();
|
||||
inp.select();
|
||||
}
|
||||
});
|
||||
}} title="Click to edit">${currentTitle}</button>`
|
||||
: html``
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
|
|
@ -333,8 +490,31 @@ const renderApp = () => {
|
|||
${Button({ variant: "ghost", size: "sm", children: html`<span class="text-xs font-mono px-1">⌘K</span>`, onClick: () => commandPalette.show(), title: "Command Palette (Ctrl+K)" })}
|
||||
${utilityToggle}
|
||||
<theme-toggle></theme-toggle>
|
||||
${Button({ variant: "ghost", size: "sm", children: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`, onClick: () => { rightPanel = rightPanel === 'terminal' ? 'none' : 'terminal'; renderApp(); if (rightPanel === 'terminal') requestAnimationFrame(() => { terminalPanel = document.querySelector('jae-terminal-panel') as JaeTerminalPanel; terminalPanel?.connect(); }); }, title: "Toggle Terminal" })}
|
||||
${Button({ variant: "ghost", size: "sm", children: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`, onClick: () => { rightPanel = rightPanel === 'browser' ? 'none' : 'browser'; renderApp(); }, title: "Toggle Browser" })}
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
children: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
|
||||
onClick: () => {
|
||||
rightPanel = rightPanel === "terminal" ? "none" : "terminal";
|
||||
renderApp();
|
||||
if (rightPanel === "terminal")
|
||||
requestAnimationFrame(() => {
|
||||
terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel;
|
||||
terminalPanel?.connect();
|
||||
});
|
||||
},
|
||||
title: "Toggle Terminal",
|
||||
})}
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
children: html`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
|
||||
onClick: () => {
|
||||
rightPanel = rightPanel === "browser" ? "none" : "browser";
|
||||
renderApp();
|
||||
},
|
||||
title: "Toggle Browser",
|
||||
})}
|
||||
${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"), onClick: () => SettingsDialog.open([new ProvidersModelsTab(), new ProxyTab()]), title: "Settings" })}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -343,60 +523,121 @@ ${Button({ variant: "ghost", size: "sm", children: icon(Settings, "sm"), onClick
|
|||
${sidebar}
|
||||
</div>
|
||||
<div id="sb-resize" style="width:5px;cursor:col-resize;flex-shrink:0;background:transparent;z-index:10;transition:background 0.15s"
|
||||
@mousedown=${(e: MouseEvent) => { e.preventDefault(); const sx=e.clientX,sw=sidebarWidth; const mv=(me: MouseEvent)=>{sidebarWidth=Math.max(150,Math.min(420,sw+me.clientX-sx));const w=document.getElementById("sidebar-wrap");if(w)w.style.width=sidebarWidth+"px";}; const up=()=>{document.removeEventListener("mousemove",mv);document.removeEventListener("mouseup",up);renderApp();}; document.addEventListener("mousemove",mv);document.addEventListener("mouseup",up); }}
|
||||
@mouseenter=${(e: Event)=>{(e.currentTarget as HTMLElement).style.background="rgba(128,128,128,0.4)"}}
|
||||
@mouseleave=${(e: Event)=>{(e.currentTarget as HTMLElement).style.background="transparent"}}
|
||||
@mousedown=${(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const sx = e.clientX,
|
||||
sw = sidebarWidth;
|
||||
const mv = (me: MouseEvent) => {
|
||||
sidebarWidth = Math.max(150, Math.min(420, sw + me.clientX - sx));
|
||||
const w = document.getElementById("sidebar-wrap");
|
||||
if (w) w.style.width = sidebarWidth + "px";
|
||||
};
|
||||
const up = () => {
|
||||
document.removeEventListener("mousemove", mv);
|
||||
document.removeEventListener("mouseup", up);
|
||||
renderApp();
|
||||
};
|
||||
document.addEventListener("mousemove", mv);
|
||||
document.addEventListener("mouseup", up);
|
||||
}}
|
||||
@mouseenter=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).style.background = "rgba(128,128,128,0.4)";
|
||||
}}
|
||||
@mouseleave=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).style.background = "transparent";
|
||||
}}
|
||||
></div>
|
||||
<div class="flex flex-col flex-1 min-w-0 min-h-0 relative">
|
||||
${!hasMessages ? html`
|
||||
<div class="absolute inset-x-0 top-0 z-10 flex flex-col overflow-y-auto bg-background" style="bottom:130px" @suggestion=${handleSuggestion}>
|
||||
<jae-empty-state style="display:flex;flex-direction:column;flex:1;width:100%;min-height:0"></jae-empty-state>
|
||||
<div class="absolute inset-x-0 top-0 z-10 flex flex-col" style="bottom:130px;pointer-events:${hasMessages ? "none" : "auto"}" @suggestion=${handleSuggestion}>
|
||||
<jae-empty-state .faded=${hasMessages} style="display:flex;flex-direction:column;flex:1;width:100%;min-height:0"></jae-empty-state>
|
||||
</div>
|
||||
` : html``}
|
||||
<div id="chat-wrapper" class="flex flex-col flex-1 min-h-0" >
|
||||
${chatPanel}
|
||||
</div>
|
||||
</div>
|
||||
${rightPanel !== 'none' ? html`
|
||||
${
|
||||
rightPanel !== "none"
|
||||
? html`
|
||||
<div id="rp-resize" style="width:5px;cursor:col-resize;flex-shrink:0;background:transparent;z-index:10;transition:background 0.15s"
|
||||
@mousedown=${(e: MouseEvent) => { e.preventDefault(); const sx=e.clientX,sw=rightPanelWidth; const mv=(me: MouseEvent)=>{rightPanelWidth=Math.max(280,Math.min(800,sw-(me.clientX-sx)));const p=document.getElementById("right-panel");if(p)p.style.width=rightPanelWidth+"px";}; const up=()=>{document.removeEventListener("mousemove",mv);document.removeEventListener("mouseup",up);renderApp();}; document.addEventListener("mousemove",mv);document.addEventListener("mouseup",up); }}
|
||||
@mouseenter=${(e: Event)=>{(e.currentTarget as HTMLElement).style.background="rgba(128,128,128,0.4)"}}
|
||||
@mouseleave=${(e: Event)=>{(e.currentTarget as HTMLElement).style.background="transparent"}}
|
||||
@mousedown=${(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const sx = e.clientX,
|
||||
sw = rightPanelWidth;
|
||||
const mv = (me: MouseEvent) => {
|
||||
rightPanelWidth = Math.max(280, Math.min(800, sw - (me.clientX - sx)));
|
||||
const p = document.getElementById("right-panel");
|
||||
if (p) p.style.width = rightPanelWidth + "px";
|
||||
};
|
||||
const up = () => {
|
||||
document.removeEventListener("mousemove", mv);
|
||||
document.removeEventListener("mouseup", up);
|
||||
renderApp();
|
||||
};
|
||||
document.addEventListener("mousemove", mv);
|
||||
document.addEventListener("mouseup", up);
|
||||
}}
|
||||
@mouseenter=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).style.background = "rgba(128,128,128,0.4)";
|
||||
}}
|
||||
@mouseleave=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).style.background = "transparent";
|
||||
}}
|
||||
></div>
|
||||
<div id="right-panel" class="flex flex-col border-l border-border" style="width:${rightPanelWidth}px;min-width:280px;max-width:800px;flex-shrink:0">
|
||||
<div class="flex items-center gap-1 px-2 shrink-0 border-b border-border bg-muted/20" style="height:36px">
|
||||
<button class="text-xs px-2 py-1 rounded ${
|
||||
rightPanel === 'terminal'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-secondary text-muted-foreground'
|
||||
}" @click=${() => { rightPanel = 'terminal'; renderApp(); requestAnimationFrame(() => { if (!terminalPanel) terminalPanel = document.querySelector('jae-terminal-panel') as JaeTerminalPanel; terminalPanel?.connect(); }); }}>Terminal</button>
|
||||
rightPanel === "terminal"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-secondary text-muted-foreground"
|
||||
}" @click=${() => {
|
||||
rightPanel = "terminal";
|
||||
renderApp();
|
||||
requestAnimationFrame(() => {
|
||||
if (!terminalPanel) terminalPanel = document.querySelector("jae-terminal-panel") as JaeTerminalPanel;
|
||||
terminalPanel?.connect();
|
||||
});
|
||||
}}>Terminal</button>
|
||||
<button class="text-xs px-2 py-1 rounded ${
|
||||
rightPanel === 'browser'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-secondary text-muted-foreground'
|
||||
}" @click=${() => { rightPanel = 'browser'; renderApp(); }}>Browser</button>
|
||||
rightPanel === "browser" ? "bg-primary text-primary-foreground" : "hover:bg-secondary text-muted-foreground"
|
||||
}" @click=${() => {
|
||||
rightPanel = "browser";
|
||||
renderApp();
|
||||
}}>Browser</button>
|
||||
<div class="flex-1"></div>
|
||||
<button class="text-xs px-2 py-1 rounded hover:bg-secondary text-muted-foreground" @click=${() => { rightPanel = 'none'; renderApp(); }} title="Close panel">✕</button>
|
||||
<button class="text-xs px-2 py-1 rounded hover:bg-secondary text-muted-foreground" @click=${() => {
|
||||
rightPanel = "none";
|
||||
renderApp();
|
||||
}} title="Close panel">✕</button>
|
||||
</div>
|
||||
${rightPanel === 'terminal' ? html`<jae-terminal-panel class="flex-1 min-h-0"></jae-terminal-panel>` : html``}
|
||||
${rightPanel === 'browser' ? html`<jae-browser-panel class="flex-1 min-h-0"></jae-browser-panel>` : html``}
|
||||
${rightPanel === "terminal" ? html`<jae-terminal-panel class="flex-1 min-h-0"></jae-terminal-panel>` : html``}
|
||||
${rightPanel === "browser" ? html`<jae-browser-panel class="flex-1 min-h-0"></jae-browser-panel>` : html``}
|
||||
</div>
|
||||
` : html``}
|
||||
`
|
||||
: html``
|
||||
}
|
||||
</div>
|
||||
`, app);
|
||||
`,
|
||||
app,
|
||||
);
|
||||
};
|
||||
|
||||
async function initApp() {
|
||||
const app = document.getElementById("app");
|
||||
if (!app) throw new Error("App container not found");
|
||||
render(html`<div class="w-full h-screen flex items-center justify-center bg-background text-foreground"><div class="text-muted-foreground">Loading...</div></div>`, app);
|
||||
render(
|
||||
html`<div class="w-full h-screen flex items-center justify-center bg-background text-foreground"><div class="text-muted-foreground">Loading...</div></div>`,
|
||||
app,
|
||||
);
|
||||
chatPanel = new ChatPanel();
|
||||
setupCommands();
|
||||
await refreshSidebar();
|
||||
const sessionIdFromUrl = new URLSearchParams(window.location.search).get("session");
|
||||
if (sessionIdFromUrl) {
|
||||
const loaded = await loadSession(sessionIdFromUrl);
|
||||
if (!loaded) { newSession(); return; }
|
||||
if (!loaded) {
|
||||
newSession();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await createAgent();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
|
||||
import { getModels } from "@jaeswift/jae-ai";
|
||||
import { html, LitElement, type TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
|
||||
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
|
||||
import { html, LitElement, type TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
const TAG_COLORS: Record<string, string> = {
|
||||
tools: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
|
||||
|
|
@ -15,7 +14,7 @@ const TAG_COLORS: Record<string, string> = {
|
|||
tts: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
|
||||
asr: "bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-300",
|
||||
embedding: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
|
||||
"e2ee": "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300",
|
||||
e2ee: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300",
|
||||
"web-search": "bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-300",
|
||||
};
|
||||
|
||||
|
|
@ -40,7 +39,9 @@ export class VeniceModelBrowser extends LitElement {
|
|||
@state() private filter: string = "all";
|
||||
@state() private search: string = "";
|
||||
|
||||
protected createRenderRoot() { return this; }
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private renderTag(tag: string): TemplateResult {
|
||||
const cls = TAG_COLORS[tag] ?? "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300";
|
||||
|
|
@ -51,7 +52,9 @@ export class VeniceModelBrowser extends LitElement {
|
|||
let models: any[] = [];
|
||||
try {
|
||||
models = (getModels("venice" as any) as any[]) || [];
|
||||
} catch { models = []; }
|
||||
} catch {
|
||||
models = [];
|
||||
}
|
||||
|
||||
// Group by category
|
||||
const grouped: Record<string, any[]> = { text: [], image: [], video: [], audio: [], other: [] };
|
||||
|
|
@ -78,21 +81,31 @@ export class VeniceModelBrowser extends LitElement {
|
|||
<div class="flex flex-col gap-4">
|
||||
<!-- Filter bar -->
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
${filters.map(f => html`
|
||||
${filters.map(
|
||||
(f) => html`
|
||||
<button
|
||||
class="px-3 py-1 rounded-full text-xs font-medium border transition-colors
|
||||
${this.filter === f.id
|
||||
${
|
||||
this.filter === f.id
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "border-border text-muted-foreground hover:bg-secondary"}"
|
||||
@click=${() => { this.filter = f.id; this.requestUpdate(); }}
|
||||
: "border-border text-muted-foreground hover:bg-secondary"
|
||||
}"
|
||||
@click=${() => {
|
||||
this.filter = f.id;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>${f.label}</button>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search models..."
|
||||
class="ml-auto px-3 py-1 text-sm rounded border border-border bg-background text-foreground w-48"
|
||||
.value=${this.search}
|
||||
@input=${(e: Event) => { this.search = (e.target as HTMLInputElement).value; this.requestUpdate(); }}
|
||||
@input=${(e: Event) => {
|
||||
this.search = (e.target as HTMLInputElement).value;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -104,7 +117,8 @@ export class VeniceModelBrowser extends LitElement {
|
|||
<div class="flex flex-col gap-2">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">${CATEGORY_LABELS[cat] ?? cat}</h4>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
${filtered.map((m: any) => html`
|
||||
${filtered.map(
|
||||
(m: any) => html`
|
||||
<div class="flex items-start justify-between p-3 rounded-lg border border-border bg-card hover:bg-secondary/40 transition-colors">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<span class="text-sm font-mono font-medium text-foreground truncate">${m.id}</span>
|
||||
|
|
@ -114,17 +128,22 @@ export class VeniceModelBrowser extends LitElement {
|
|||
${(m.tags ?? []).map((t: string) => this.renderTag(t))}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
|
||||
${models.length === 0 ? html`
|
||||
${
|
||||
models.length === 0
|
||||
? html`
|
||||
<div class="text-sm text-muted-foreground text-center py-4">
|
||||
No Venice models found. Ensure jae-ai includes Venice models.
|
||||
</div>
|
||||
` : ""}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class ModelSelector extends DialogBase {
|
|||
@state() searchQuery = "";
|
||||
@state() filterThinking = false;
|
||||
@state() filterVision = false;
|
||||
@state() private filterProvider = "";
|
||||
@state() customProvidersLoading = false;
|
||||
@state() selectedIndex = 0;
|
||||
@state() private navigationMode: "mouse" | "keyboard" = "mouse";
|
||||
|
|
@ -274,6 +275,59 @@ export class ModelSelector extends DialogBase {
|
|||
});
|
||||
}
|
||||
|
||||
private getUniqueProviders(): string[] {
|
||||
const seen = new Set<string>();
|
||||
const known = getProviders();
|
||||
for (const p of known) {
|
||||
const models = getModels(p as any);
|
||||
if (models.length > 0) seen.add(p);
|
||||
}
|
||||
for (const m of this.customProviderModels) {
|
||||
if (m.provider) seen.add(m.provider);
|
||||
}
|
||||
if (this.allowedProviders) {
|
||||
return Array.from(seen).filter((p) => this.allowedProviders!.has(p));
|
||||
}
|
||||
return Array.from(seen).sort();
|
||||
}
|
||||
|
||||
private renderProviderTabs() {
|
||||
const providers = this.getUniqueProviders();
|
||||
if (providers.length <= 1) return html``;
|
||||
return html`
|
||||
<div class="flex gap-1 flex-wrap">
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded-full border transition-colors ${
|
||||
this.filterProvider === ""
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/40"
|
||||
}"
|
||||
@click=${() => {
|
||||
this.filterProvider = "";
|
||||
this.selectedIndex = 0;
|
||||
if (this.scrollContainerRef.value) this.scrollContainerRef.value.scrollTop = 0;
|
||||
}}
|
||||
>All</button>
|
||||
${providers.map(
|
||||
(p) => html`
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded-full border transition-colors capitalize ${
|
||||
this.filterProvider === p
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/40"
|
||||
}"
|
||||
@click=${() => {
|
||||
this.filterProvider = p;
|
||||
this.selectedIndex = 0;
|
||||
if (this.scrollContainerRef.value) this.scrollContainerRef.value.scrollTop = 0;
|
||||
}}
|
||||
>${p}</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected override renderContent(): TemplateResult {
|
||||
const filteredModels = this.getFilteredModels();
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export { RuntimeMessageBridge } from "./components/sandbox/RuntimeMessageBridge.
|
|||
export { RUNTIME_MESSAGE_ROUTER } from "./components/sandbox/RuntimeMessageRouter.js";
|
||||
export type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js";
|
||||
export { ThinkingBlock } from "./components/ThinkingBlock.js";
|
||||
export { VeniceModelBrowser } from "./components/VeniceModelBrowser.js";
|
||||
export { ApiKeyPromptDialog } from "./dialogs/ApiKeyPromptDialog.js";
|
||||
export { AttachmentOverlay } from "./dialogs/AttachmentOverlay.js";
|
||||
export { CustomProviderDialog } from "./dialogs/CustomProviderDialog.js";
|
||||
|
|
@ -101,6 +102,7 @@ export { MarkdownArtifact } from "./tools/artifacts/MarkdownArtifact.js";
|
|||
export { SvgArtifact } from "./tools/artifacts/SvgArtifact.js";
|
||||
export { TextArtifact } from "./tools/artifacts/TextArtifact.js";
|
||||
export { createExtractDocumentTool, extractDocumentTool } from "./tools/extract-document.js";
|
||||
export { createImageGenTool, imageGenTool } from "./tools/image-gen.js";
|
||||
// Tools
|
||||
export { getToolRenderer, registerToolRenderer, renderTool, setShowJsonMode } from "./tools/index.js";
|
||||
export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.js";
|
||||
|
|
@ -109,8 +111,13 @@ export { BashRenderer } from "./tools/renderers/BashRenderer.js";
|
|||
export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js";
|
||||
// Tool renderers
|
||||
export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.js";
|
||||
export { DiffRenderer } from "./tools/renderers/DiffRenderer.js";
|
||||
export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.js";
|
||||
export { MermaidRenderer } from "./tools/renderers/MermaidRenderer.js";
|
||||
export type { ToolRenderer, ToolRenderResult } from "./tools/types.js";
|
||||
export { createTTSTool, ttsTool } from "./tools/voice-tts.js";
|
||||
// Venice / community tools
|
||||
export { createWebSearchTool, webSearchTool } from "./tools/web-search.js";
|
||||
export type { Attachment } from "./utils/attachment-utils.js";
|
||||
// Utils
|
||||
export { loadAttachment } from "./utils/attachment-utils.js";
|
||||
|
|
@ -118,12 +125,3 @@ export { clearAuthToken, getAuthToken } from "./utils/auth-token.js";
|
|||
export { formatCost, formatModelCost, formatTokenCount, formatUsage } from "./utils/format.js";
|
||||
export { i18n, setLanguage, translations } from "./utils/i18n.js";
|
||||
export { applyProxyIfNeeded, createStreamFn, isCorsError, shouldUseProxyForProvider } from "./utils/proxy-utils.js";
|
||||
|
||||
export { VeniceModelBrowser } from "./components/VeniceModelBrowser.js";
|
||||
|
||||
// Venice / community tools
|
||||
export { createWebSearchTool, webSearchTool } from "./tools/web-search.js";
|
||||
export { createImageGenTool, imageGenTool } from "./tools/image-gen.js";
|
||||
export { createTTSTool, ttsTool } from "./tools/voice-tts.js";
|
||||
export { MermaidRenderer } from "./tools/renderers/MermaidRenderer.js";
|
||||
export { DiffRenderer } from "./tools/renderers/DiffRenderer.js";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { html } from "lit";
|
||||
import { GitCompare } from "lucide";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
|
|
@ -31,10 +30,13 @@ function computeLineDiff(original: string, modified: string): Array<{ type: "add
|
|||
const result: Array<{ type: "add" | "remove" | "same"; line: string }> = [];
|
||||
const maxLen = Math.max(oldLines.length, newLines.length);
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
if (i >= oldLines.length) { result.push({ type: "add", line: newLines[i] }); }
|
||||
else if (i >= newLines.length) { result.push({ type: "remove", line: oldLines[i] }); }
|
||||
else if (oldLines[i] === newLines[i]) { result.push({ type: "same", line: oldLines[i] }); }
|
||||
else {
|
||||
if (i >= oldLines.length) {
|
||||
result.push({ type: "add", line: newLines[i] });
|
||||
} else if (i >= newLines.length) {
|
||||
result.push({ type: "remove", line: oldLines[i] });
|
||||
} else if (oldLines[i] === newLines[i]) {
|
||||
result.push({ type: "same", line: oldLines[i] });
|
||||
} else {
|
||||
result.push({ type: "remove", line: oldLines[i] });
|
||||
result.push({ type: "add", line: newLines[i] });
|
||||
}
|
||||
|
|
@ -63,26 +65,30 @@ class DiffRenderer implements ToolRenderer<DiffParams, DiffDetails> {
|
|||
}
|
||||
const { original, modified, filename } = result.details;
|
||||
const diffLines = computeLineDiff(original, modified);
|
||||
const adds = diffLines.filter(l => l.type === "add").length;
|
||||
const removes = diffLines.filter(l => l.type === "remove").length;
|
||||
const adds = diffLines.filter((l) => l.type === "add").length;
|
||||
const removes = diffLines.filter((l) => l.type === "remove").length;
|
||||
|
||||
return {
|
||||
content: html`
|
||||
<div class="flex flex-col gap-3">
|
||||
${renderHeader(state, GitCompare, html`Diff: ${filename || "file"} <span class="text-green-500 ml-2">+${adds}</span><span class="text-red-500 ml-1">-${removes}</span>`)}
|
||||
<div class="rounded border border-border overflow-auto max-h-96 text-xs font-mono">
|
||||
${diffLines.map((l, i) => html`
|
||||
${diffLines.map(
|
||||
(l, i) => html`
|
||||
<div class="flex gap-0 ${
|
||||
l.type === "add" ? "bg-green-500/10 text-green-700 dark:text-green-400" :
|
||||
l.type === "remove" ? "bg-red-500/10 text-red-700 dark:text-red-400" :
|
||||
"text-muted-foreground"
|
||||
l.type === "add"
|
||||
? "bg-green-500/10 text-green-700 dark:text-green-400"
|
||||
: l.type === "remove"
|
||||
? "bg-red-500/10 text-red-700 dark:text-red-400"
|
||||
: "text-muted-foreground"
|
||||
}">
|
||||
<span class="w-6 text-center shrink-0 select-none border-r border-border px-1">${i + 1}</span>
|
||||
<span class="px-2 whitespace-pre">${
|
||||
l.type === "add" ? "+ " : l.type === "remove" ? "- " : " "
|
||||
}${l.line}</span>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { html } from "lit";
|
||||
import { Image } from "lucide";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
import { getAppStorage } from "../storage/app-storage.js";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
import type { ToolRenderer, ToolRenderResult } from "./types.js";
|
||||
|
||||
const imageGenSchema = Type.Object({
|
||||
|
|
@ -42,7 +42,12 @@ export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenDetails> = {
|
|||
const apiKey = await getAppStorage().providerKeys.get("venice");
|
||||
if (!apiKey) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: Venice API key not set. Add it in Settings > Providers & Models > Venice." }],
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Venice API key not set. Add it in Settings > Providers & Models > Venice.",
|
||||
},
|
||||
],
|
||||
details: { model, prompt, width, height, error: "No API key" },
|
||||
};
|
||||
}
|
||||
|
|
@ -59,7 +64,7 @@ export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenDetails> = {
|
|||
details: { model, prompt, width, height, error: err },
|
||||
};
|
||||
}
|
||||
const data = await res.json() as any;
|
||||
const data = (await res.json()) as any;
|
||||
const b64 = data?.images?.[0];
|
||||
if (!b64) {
|
||||
return {
|
||||
|
|
@ -76,7 +81,10 @@ export const imageGenTool: AgentTool<typeof imageGenSchema, ImageGenDetails> = {
|
|||
};
|
||||
|
||||
class ImageGenRenderer implements ToolRenderer<ImageGenParams, ImageGenDetails> {
|
||||
render(params: ImageGenParams | undefined, result: ToolResultMessage<ImageGenDetails> | undefined): ToolRenderResult {
|
||||
render(
|
||||
params: ImageGenParams | undefined,
|
||||
result: ToolResultMessage<ImageGenDetails> | undefined,
|
||||
): ToolRenderResult {
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
if (result?.details?.dataUrl) {
|
||||
const d = result.details;
|
||||
|
|
@ -97,7 +105,10 @@ class ImageGenRenderer implements ToolRenderer<ImageGenParams, ImageGenDetails>
|
|||
isCustom: false,
|
||||
};
|
||||
}
|
||||
return { content: renderHeader(state, Image, `Generating image: ${params?.prompt?.slice(0, 50) ?? "..."}`), isCustom: false };
|
||||
return {
|
||||
content: renderHeader(state, Image, `Generating image: ${params?.prompt?.slice(0, 50) ?? "..."}`),
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,6 @@ export function renderTool(
|
|||
|
||||
export { getToolRenderer, registerToolRenderer };
|
||||
|
||||
export { webSearchTool, createWebSearchTool, type WebSearchDetails, type WebSearchResult } from "./web-search.js";
|
||||
export { imageGenTool, createImageGenTool, type ImageGenDetails } from "./image-gen.js";
|
||||
export { ttsTool, createTTSTool, type TTSDetails } from "./voice-tts.js";
|
||||
export { createImageGenTool, type ImageGenDetails, imageGenTool } from "./image-gen.js";
|
||||
export { createTTSTool, type TTSDetails, ttsTool } from "./voice-tts.js";
|
||||
export { createWebSearchTool, type WebSearchDetails, type WebSearchResult, webSearchTool } from "./web-search.js";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { html } from "lit";
|
||||
import { Brain, BrainCircuit, Trash2 } from "lucide";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
|
|
@ -31,7 +30,10 @@ async function openDB(): Promise<IDBDatabase> {
|
|||
req.onupgradeneeded = () => {
|
||||
req.result.createObjectStore(STORE_NAME, { keyPath: "id" });
|
||||
};
|
||||
req.onsuccess = () => { db = req.result; resolve(db); };
|
||||
req.onsuccess = () => {
|
||||
db = req.result;
|
||||
resolve(db);
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
|
@ -60,7 +62,7 @@ export async function memoryLoad(): Promise<MemoryEntry[]> {
|
|||
export async function memorySearch(query: string): Promise<MemoryEntry[]> {
|
||||
const all = await memoryLoad();
|
||||
const q = query.toLowerCase();
|
||||
return all.filter(e => e.content.toLowerCase().includes(q) || e.tags.some(t => t.toLowerCase().includes(q)));
|
||||
return all.filter((e) => e.content.toLowerCase().includes(q) || e.tags.some((t) => t.toLowerCase().includes(q)));
|
||||
}
|
||||
|
||||
export async function memoryDelete(id: string): Promise<void> {
|
||||
|
|
@ -105,9 +107,10 @@ export const recallMemoryTool: AgentTool<typeof recallMemorySchema, { results: M
|
|||
parameters: recallMemorySchema,
|
||||
async execute(toolCallId, params, signal) {
|
||||
const results = await memorySearch(params.query);
|
||||
const text = results.length === 0
|
||||
const text =
|
||||
results.length === 0
|
||||
? `No memories found for: ${params.query}`
|
||||
: results.map(r => `[${r.timestamp.slice(0, 10)}] ${r.content}`).join("\n\n");
|
||||
: results.map((r) => `[${r.timestamp.slice(0, 10)}] ${r.content}`).join("\n\n");
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: { results },
|
||||
|
|
@ -139,16 +142,22 @@ class RecallMemoryRenderer implements ToolRenderer {
|
|||
content: html`
|
||||
<div class="flex flex-col gap-3">
|
||||
${renderHeader(state, BrainCircuit, `Memory recall: ${params?.query || ""}`)}
|
||||
${results.length > 0 ? html`
|
||||
${
|
||||
results.length > 0
|
||||
? html`
|
||||
<div class="flex flex-col gap-1">
|
||||
${results.map(r => html`
|
||||
${results.map(
|
||||
(r) => html`
|
||||
<div class="text-xs p-2 rounded border border-border">
|
||||
<div class="text-muted-foreground">${r.timestamp.slice(0, 10)}</div>
|
||||
<div>${r.content}</div>
|
||||
</div>
|
||||
`)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
` : ""}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
isCustom: false,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { html } from "lit";
|
||||
import { GitBranch } from "lucide";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
|
|
@ -44,7 +43,10 @@ async function loadMermaid(): Promise<any> {
|
|||
script.src = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
|
||||
script.onload = () => {
|
||||
const m = (window as any).mermaid;
|
||||
m.initialize({ startOnLoad: false, theme: document.documentElement.classList.contains("dark") ? "dark" : "default" });
|
||||
m.initialize({
|
||||
startOnLoad: false,
|
||||
theme: document.documentElement.classList.contains("dark") ? "dark" : "default",
|
||||
});
|
||||
mermaidLoaded = true;
|
||||
resolve(m);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { html } from "lit";
|
||||
import { FileText } from "lucide";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { renderHeader } from "../renderer-registry.js";
|
||||
import { registerToolRenderer, renderHeader } from "../renderer-registry.js";
|
||||
import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
||||
import { registerToolRenderer } from "../renderer-registry.js";
|
||||
|
||||
export class DiffRenderer implements ToolRenderer {
|
||||
render(params: any, result: ToolResultMessage | undefined, isStreaming?: boolean): ToolRenderResult {
|
||||
const rawContent = result?.content;
|
||||
const resultText = Array.isArray(rawContent)
|
||||
? rawContent.filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n")
|
||||
: typeof rawContent === "string" ? rawContent : "";
|
||||
? rawContent
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n")
|
||||
: typeof rawContent === "string"
|
||||
? rawContent
|
||||
: "";
|
||||
const diff = params?.diff || params?.patch || resultText || "";
|
||||
const filename = params?.file || params?.filename || "";
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
|
|
@ -22,20 +26,27 @@ export class DiffRenderer implements ToolRenderer {
|
|||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileText, label)}
|
||||
${diff ? html`
|
||||
${
|
||||
diff
|
||||
? html`
|
||||
<div class="overflow-auto max-h-96 rounded-lg border border-border">
|
||||
<pre class="text-xs font-mono p-4">${lines.map((line: string) => {
|
||||
let cls = "block";
|
||||
if (line.startsWith("+") && !line.startsWith("+++")) cls = "text-green-500 bg-green-500/10 block px-1";
|
||||
else if (line.startsWith("-") && !line.startsWith("---")) cls = "text-red-500 bg-red-500/10 block px-1";
|
||||
if (line.startsWith("+") && !line.startsWith("+++"))
|
||||
cls = "text-green-500 bg-green-500/10 block px-1";
|
||||
else if (line.startsWith("-") && !line.startsWith("---"))
|
||||
cls = "text-red-500 bg-red-500/10 block px-1";
|
||||
else if (line.startsWith("@@")) cls = "text-blue-400 block px-1";
|
||||
else if (line.startsWith("diff ") || line.startsWith("index ")) cls = "text-muted-foreground block px-1";
|
||||
else if (line.startsWith("diff ") || line.startsWith("index "))
|
||||
cls = "text-muted-foreground block px-1";
|
||||
else cls = "block px-1";
|
||||
return html`<span class=${cls}>${line}</span>
|
||||
`;
|
||||
})}</pre>
|
||||
</div>
|
||||
` : ""}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
isCustom: false,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { html } from "lit";
|
||||
import { GitBranch } from "lucide";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { renderHeader } from "../renderer-registry.js";
|
||||
import { registerToolRenderer, renderHeader } from "../renderer-registry.js";
|
||||
import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
||||
import { registerToolRenderer } from "../renderer-registry.js";
|
||||
|
||||
export class MermaidRenderer implements ToolRenderer {
|
||||
render(params: any, result: ToolResultMessage | undefined, isStreaming?: boolean): ToolRenderResult {
|
||||
const rawContent = result?.content;
|
||||
const resultText = Array.isArray(rawContent)
|
||||
? rawContent.filter((c: any) => c.type === "text").map((c: any) => c.text).join("\n")
|
||||
: typeof rawContent === "string" ? rawContent : "";
|
||||
? rawContent
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n")
|
||||
: typeof rawContent === "string"
|
||||
? rawContent
|
||||
: "";
|
||||
const diagram = params?.diagram || params?.code || resultText || "";
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
const id = "mermaid-" + Math.random().toString(36).slice(2);
|
||||
|
|
@ -19,7 +23,9 @@ export class MermaidRenderer implements ToolRenderer {
|
|||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, GitBranch, "Diagram")}
|
||||
${diagram ? html`
|
||||
${
|
||||
diagram
|
||||
? html`
|
||||
<div class="p-4 bg-background rounded-lg border border-border overflow-auto">
|
||||
<div .id=${id} class="mermaid">${diagram}</div>
|
||||
<script>
|
||||
|
|
@ -41,7 +47,9 @@ export class MermaidRenderer implements ToolRenderer {
|
|||
})();
|
||||
</script>
|
||||
</div>
|
||||
` : ""}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
isCustom: false,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { html } from "lit";
|
||||
import { Volume2 } from "lucide";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
import { getAppStorage } from "../storage/app-storage.js";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
import type { ToolRenderer, ToolRenderResult } from "./types.js";
|
||||
|
||||
const ttsSchema = Type.Object({
|
||||
|
|
@ -79,7 +79,10 @@ class TTSRenderer implements ToolRenderer<TTSParams, TTSDetails> {
|
|||
isCustom: false,
|
||||
};
|
||||
}
|
||||
return { content: renderHeader(state, Volume2, `Speaking: ${params?.text?.slice(0, 50) ?? "..."}`), isCustom: false };
|
||||
return {
|
||||
content: renderHeader(state, Volume2, `Speaking: ${params?.text?.slice(0, 50) ?? "..."}`),
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AgentTool } from "@jaeswift/jae-agent-core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolResultMessage } from "@jaeswift/jae-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { html } from "lit";
|
||||
import { Globe } from "lucide";
|
||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
||||
|
|
@ -30,25 +30,28 @@ 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`);
|
||||
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 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 || [])) {
|
||||
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) {
|
||||
if (results.length >= limit) break;
|
||||
if (sub.FirstURL && sub.Text) results.push({ title: sub.Text.split(" - ")[0], url: sub.FirstURL, snippet: sub.Text });
|
||||
if (sub.FirstURL && sub.Text)
|
||||
results.push({ title: sub.Text.split(" - ")[0], url: sub.FirstURL, snippet: sub.Text });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const r of (data.Results || [])) {
|
||||
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 });
|
||||
}
|
||||
|
|
@ -68,13 +71,19 @@ export const webSearchTool: AgentTool<typeof webSearchSchema, WebSearchDetails>
|
|||
const text = results.length === 0 ? `No results for: ${query}` : lines.join("\n\n");
|
||||
return { content: [{ type: "text", text }], details: { results, query } };
|
||||
} catch (err: any) {
|
||||
return { content: [{ type: "text", text: `Search failed: ${err.message}` }], details: { results: [], query, error: err.message } };
|
||||
return {
|
||||
content: [{ type: "text", text: `Search failed: ${err.message}` }],
|
||||
details: { results: [], query, error: err.message },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
class WebSearchRenderer implements ToolRenderer<WebSearchParams, WebSearchDetails> {
|
||||
render(params: WebSearchParams | undefined, result: ToolResultMessage<WebSearchDetails> | undefined): ToolRenderResult {
|
||||
render(
|
||||
params: WebSearchParams | undefined,
|
||||
result: ToolResultMessage<WebSearchDetails> | undefined,
|
||||
): ToolRenderResult {
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
if (result?.details?.results?.length) {
|
||||
const details = result.details;
|
||||
|
|
@ -83,13 +92,15 @@ class WebSearchRenderer implements ToolRenderer<WebSearchParams, WebSearchDetail
|
|||
<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`
|
||||
${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