feat(workspace): composer like Claude Desktop, image thumbnails, tool icons in the picker

This commit is contained in:
2026-10-02 16:13:27 +07:00
parent 4802684d2f
commit f35bf2043f
15 changed files with 414 additions and 185 deletions
+5 -13
View File
@@ -1,7 +1,7 @@
/**
* @file AiToolIcon.tsx
* @description An AI tool's icon: the mark shipped in `client/public/ai-tools/`
* when there is one (a light/dark pair where a mark needs one per theme), else
* when there is one, else
* the server's cached favicon (GET /api/ai-tools/:id/icon), else the label's
* first letter. Every tool icon in the app renders through here.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -10,13 +10,13 @@
import { useState } from "react";
import { api, type AiTool } from "../lib/api";
/** Shipped marks by tool id; `[light, dark]` swaps with the theme (CSS only). */
const BUNDLED: Record<string, string | [string, string]> = {
/** Shipped marks by tool id. */
const BUNDLED: Record<string, string> = {
claude: "/ai-tools/claude.svg",
codex: "/ai-tools/codex.svg",
gemini: "/ai-tools/gemini.svg",
openclaw: "/ai-tools/openclaw.svg",
hermes: ["/ai-tools/hermes-black.svg", "/ai-tools/hermes-white.svg"],
hermes: "/ai-tools/hermes.svg",
};
/**
@@ -34,15 +34,7 @@ export function AiToolIcon({
const [failedId, setFailedId] = useState<string | null>(null);
const box = `${className} shrink-0 rounded-md bg-surface-4 object-contain p-1`;
const bundled = BUNDLED[tool.id];
if (typeof bundled === "string") return <img src={bundled} alt="" className={box} />;
if (bundled) {
return (
<>
<img src={bundled[0]} alt="" className={`${box} dark:hidden`} />
<img src={bundled[1]} alt="" className={`${box} hidden dark:block`} />
</>
);
}
if (bundled) return <img src={bundled} alt="" className={box} />;
if (failedId !== tool.id) {
return (
<img