Upload 33 files
Browse files- eslint.config.js +23 -0
- index.html +16 -0
- package.json +35 -0
- src/App.tsx +758 -0
- src/components/ExamplePrompts.tsx +46 -0
- src/components/LoadingScreen.tsx +291 -0
- src/components/ResultBlock.tsx +21 -0
- src/components/ToolCallIndicator.tsx +104 -0
- src/components/ToolItem.tsx +146 -0
- src/components/ToolResultRenderer.tsx +42 -0
- src/components/icons/HfLogo.tsx +35 -0
- src/components/icons/IBMLogo.tsx +12 -0
- src/constants/db.ts +3 -0
- src/constants/examples.ts +39 -0
- src/constants/models.ts +23 -0
- src/hooks/useLLM.ts +234 -0
- src/index.css +1 -0
- src/main.tsx +10 -0
- src/tools/get_location.js +84 -0
- src/tools/get_time.js +51 -0
- src/tools/index.ts +19 -0
- src/tools/math_eval.js +54 -0
- src/tools/open_webpage.js +49 -0
- src/tools/random_number.js +51 -0
- src/tools/sleep.js +45 -0
- src/tools/speak.js +59 -0
- src/tools/template.js +47 -0
- src/utils.ts +249 -0
- src/vite-env.d.ts +1 -0
- tsconfig.app.json +27 -0
- tsconfig.json +7 -0
- tsconfig.node.json +25 -0
- vite.config.ts +8 -0
eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from "@eslint/js";
|
| 2 |
+
import globals from "globals";
|
| 3 |
+
import reactHooks from "eslint-plugin-react-hooks";
|
| 4 |
+
import reactRefresh from "eslint-plugin-react-refresh";
|
| 5 |
+
import tseslint from "typescript-eslint";
|
| 6 |
+
import { globalIgnores } from "eslint/config";
|
| 7 |
+
|
| 8 |
+
export default tseslint.config([
|
| 9 |
+
globalIgnores(["dist"]),
|
| 10 |
+
{
|
| 11 |
+
files: ["**/*.{ts,tsx}"],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs["recommended-latest"],
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
]);
|
index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link
|
| 6 |
+
rel="icon"
|
| 7 |
+
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>"
|
| 8 |
+
/>
|
| 9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 10 |
+
<title>Granite WebGPU - In-Browser Tool Calling</title>
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<div id="root"></div>
|
| 14 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 15 |
+
</body>
|
| 16 |
+
</html>
|
package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "granite-tool-calling",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@huggingface/transformers": "^3.7.6",
|
| 14 |
+
"@monaco-editor/react": "^4.7.0",
|
| 15 |
+
"@tailwindcss/vite": "^4.1.11",
|
| 16 |
+
"idb": "^8.0.3",
|
| 17 |
+
"lucide-react": "^0.535.0",
|
| 18 |
+
"react": "^19.1.0",
|
| 19 |
+
"react-dom": "^19.1.0",
|
| 20 |
+
"tailwindcss": "^4.1.11"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@eslint/js": "^9.30.1",
|
| 24 |
+
"@types/react": "^19.1.8",
|
| 25 |
+
"@types/react-dom": "^19.1.6",
|
| 26 |
+
"@vitejs/plugin-react": "^4.6.0",
|
| 27 |
+
"eslint": "^9.30.1",
|
| 28 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
| 29 |
+
"eslint-plugin-react-refresh": "^0.4.20",
|
| 30 |
+
"globals": "^16.3.0",
|
| 31 |
+
"typescript": "~5.8.3",
|
| 32 |
+
"typescript-eslint": "^8.35.1",
|
| 33 |
+
"vite": "^7.0.4"
|
| 34 |
+
}
|
| 35 |
+
}
|
src/App.tsx
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, {
|
| 2 |
+
useState,
|
| 3 |
+
useEffect,
|
| 4 |
+
useCallback,
|
| 5 |
+
useRef,
|
| 6 |
+
useMemo,
|
| 7 |
+
} from "react";
|
| 8 |
+
import { openDB, type IDBPDatabase } from "idb";
|
| 9 |
+
import { Play, Plus, RotateCcw, Wrench } from "lucide-react";
|
| 10 |
+
import { useLLM } from "./hooks/useLLM";
|
| 11 |
+
|
| 12 |
+
import type { Tool } from "./components/ToolItem";
|
| 13 |
+
|
| 14 |
+
import {
|
| 15 |
+
extractFunctionAndRenderer,
|
| 16 |
+
generateSchemaFromCode,
|
| 17 |
+
getErrorMessage,
|
| 18 |
+
isMobileOrTablet,
|
| 19 |
+
} from "./utils";
|
| 20 |
+
|
| 21 |
+
import { DB_NAME, STORE_NAME, SETTINGS_STORE_NAME } from "./constants/db";
|
| 22 |
+
|
| 23 |
+
import { DEFAULT_TOOLS, TEMPLATE } from "./tools";
|
| 24 |
+
import ToolResultRenderer from "./components/ToolResultRenderer";
|
| 25 |
+
import ToolCallIndicator from "./components/ToolCallIndicator";
|
| 26 |
+
import ToolItem from "./components/ToolItem";
|
| 27 |
+
import ResultBlock from "./components/ResultBlock";
|
| 28 |
+
import ExamplePrompts from "./components/ExamplePrompts";
|
| 29 |
+
|
| 30 |
+
import { LoadingScreen } from "./components/LoadingScreen";
|
| 31 |
+
|
| 32 |
+
interface RenderInfo {
|
| 33 |
+
call: string;
|
| 34 |
+
result?: any;
|
| 35 |
+
renderer?: string;
|
| 36 |
+
input?: Record<string, any>;
|
| 37 |
+
error?: string;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
interface BaseMessage {
|
| 41 |
+
role: "system" | "user" | "assistant";
|
| 42 |
+
content: string;
|
| 43 |
+
}
|
| 44 |
+
interface ToolMessage {
|
| 45 |
+
role: "tool";
|
| 46 |
+
content: string;
|
| 47 |
+
renderInfo: RenderInfo[]; // Rich data for the UI
|
| 48 |
+
}
|
| 49 |
+
type Message = BaseMessage | ToolMessage;
|
| 50 |
+
|
| 51 |
+
async function getDB(): Promise<IDBPDatabase> {
|
| 52 |
+
return openDB(DB_NAME, 1, {
|
| 53 |
+
upgrade(db) {
|
| 54 |
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
| 55 |
+
db.createObjectStore(STORE_NAME, {
|
| 56 |
+
keyPath: "id",
|
| 57 |
+
autoIncrement: true,
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
if (!db.objectStoreNames.contains(SETTINGS_STORE_NAME)) {
|
| 61 |
+
db.createObjectStore(SETTINGS_STORE_NAME, { keyPath: "key" });
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const App: React.FC = () => {
|
| 68 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
| 69 |
+
const [tools, setTools] = useState<Tool[]>([]);
|
| 70 |
+
const [input, setInput] = useState<string>("");
|
| 71 |
+
const [isGenerating, setIsGenerating] = useState<boolean>(false);
|
| 72 |
+
const isMobile = useMemo(isMobileOrTablet, []);
|
| 73 |
+
const [selectedModel, setSelectedModel] = useState<string>(
|
| 74 |
+
isMobile ? "350M" : "1B",
|
| 75 |
+
);
|
| 76 |
+
const [isModelDropdownOpen, setIsModelDropdownOpen] =
|
| 77 |
+
useState<boolean>(false);
|
| 78 |
+
const [isToolsPanelVisible, setIsToolsPanelVisible] =
|
| 79 |
+
useState<boolean>(false);
|
| 80 |
+
const chatContainerRef = useRef<HTMLDivElement>(null);
|
| 81 |
+
const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
|
| 82 |
+
const toolsContainerRef = useRef<HTMLDivElement>(null);
|
| 83 |
+
const inputRef = useRef<HTMLInputElement>(null);
|
| 84 |
+
const {
|
| 85 |
+
isLoading,
|
| 86 |
+
isReady,
|
| 87 |
+
error,
|
| 88 |
+
progress,
|
| 89 |
+
loadModel,
|
| 90 |
+
generateResponse,
|
| 91 |
+
clearPastKeyValues,
|
| 92 |
+
} = useLLM(selectedModel);
|
| 93 |
+
|
| 94 |
+
const loadTools = useCallback(async (): Promise<void> => {
|
| 95 |
+
const db = await getDB();
|
| 96 |
+
const allTools: Tool[] = await db.getAll(STORE_NAME);
|
| 97 |
+
if (allTools.length === 0) {
|
| 98 |
+
const defaultTools: Tool[] = Object.entries(DEFAULT_TOOLS).map(
|
| 99 |
+
([name, code], id) => ({
|
| 100 |
+
id,
|
| 101 |
+
name,
|
| 102 |
+
code,
|
| 103 |
+
enabled: true,
|
| 104 |
+
isCollapsed: false,
|
| 105 |
+
}),
|
| 106 |
+
);
|
| 107 |
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
| 108 |
+
await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
|
| 109 |
+
await tx.done;
|
| 110 |
+
setTools(defaultTools);
|
| 111 |
+
} else {
|
| 112 |
+
setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
|
| 113 |
+
}
|
| 114 |
+
}, []);
|
| 115 |
+
|
| 116 |
+
useEffect(() => {
|
| 117 |
+
loadTools();
|
| 118 |
+
}, [loadTools]);
|
| 119 |
+
|
| 120 |
+
useEffect(() => {
|
| 121 |
+
if (chatContainerRef.current) {
|
| 122 |
+
chatContainerRef.current.scrollTop =
|
| 123 |
+
chatContainerRef.current.scrollHeight;
|
| 124 |
+
}
|
| 125 |
+
}, [messages]);
|
| 126 |
+
|
| 127 |
+
const updateToolInDB = async (tool: Tool): Promise<void> => {
|
| 128 |
+
const db = await getDB();
|
| 129 |
+
await db.put(STORE_NAME, tool);
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
const saveToolDebounced = (tool: Tool): void => {
|
| 133 |
+
if (tool.id !== undefined && debounceTimers.current[tool.id]) {
|
| 134 |
+
clearTimeout(debounceTimers.current[tool.id]);
|
| 135 |
+
}
|
| 136 |
+
if (tool.id !== undefined) {
|
| 137 |
+
debounceTimers.current[tool.id] = setTimeout(() => {
|
| 138 |
+
updateToolInDB(tool);
|
| 139 |
+
}, 300);
|
| 140 |
+
}
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
const clearChat = useCallback(() => {
|
| 144 |
+
setMessages([]);
|
| 145 |
+
clearPastKeyValues();
|
| 146 |
+
}, [clearPastKeyValues]);
|
| 147 |
+
|
| 148 |
+
const addTool = async (): Promise<void> => {
|
| 149 |
+
const newTool: Omit<Tool, "id"> = {
|
| 150 |
+
name: "new_tool",
|
| 151 |
+
code: TEMPLATE,
|
| 152 |
+
enabled: true,
|
| 153 |
+
isCollapsed: false,
|
| 154 |
+
};
|
| 155 |
+
const db = await getDB();
|
| 156 |
+
const id = await db.add(STORE_NAME, newTool);
|
| 157 |
+
setTools((prev) => {
|
| 158 |
+
const updated = [...prev, { ...newTool, id: id as number }];
|
| 159 |
+
setTimeout(() => {
|
| 160 |
+
if (toolsContainerRef.current) {
|
| 161 |
+
toolsContainerRef.current.scrollTop =
|
| 162 |
+
toolsContainerRef.current.scrollHeight;
|
| 163 |
+
}
|
| 164 |
+
}, 0);
|
| 165 |
+
return updated;
|
| 166 |
+
});
|
| 167 |
+
clearChat();
|
| 168 |
+
};
|
| 169 |
+
|
| 170 |
+
const deleteTool = async (id: number): Promise<void> => {
|
| 171 |
+
if (debounceTimers.current[id]) {
|
| 172 |
+
clearTimeout(debounceTimers.current[id]);
|
| 173 |
+
}
|
| 174 |
+
const db = await getDB();
|
| 175 |
+
await db.delete(STORE_NAME, id);
|
| 176 |
+
setTools(tools.filter((tool) => tool.id !== id));
|
| 177 |
+
clearChat();
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
const toggleToolEnabled = (id: number): void => {
|
| 181 |
+
let changedTool: Tool | undefined;
|
| 182 |
+
const newTools = tools.map((tool) => {
|
| 183 |
+
if (tool.id === id) {
|
| 184 |
+
changedTool = { ...tool, enabled: !tool.enabled };
|
| 185 |
+
return changedTool;
|
| 186 |
+
}
|
| 187 |
+
return tool;
|
| 188 |
+
});
|
| 189 |
+
setTools(newTools);
|
| 190 |
+
if (changedTool) saveToolDebounced(changedTool);
|
| 191 |
+
};
|
| 192 |
+
|
| 193 |
+
const toggleToolCollapsed = (id: number): void => {
|
| 194 |
+
setTools(
|
| 195 |
+
tools.map((tool) =>
|
| 196 |
+
tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool,
|
| 197 |
+
),
|
| 198 |
+
);
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
const expandTool = (id: number): void => {
|
| 202 |
+
setTools(
|
| 203 |
+
tools.map((tool) =>
|
| 204 |
+
tool.id === id ? { ...tool, isCollapsed: false } : tool,
|
| 205 |
+
),
|
| 206 |
+
);
|
| 207 |
+
};
|
| 208 |
+
|
| 209 |
+
const handleToolCodeChange = (id: number, newCode: string): void => {
|
| 210 |
+
let changedTool: Tool | undefined;
|
| 211 |
+
const newTools = tools.map((tool) => {
|
| 212 |
+
if (tool.id === id) {
|
| 213 |
+
const { functionCode } = extractFunctionAndRenderer(newCode);
|
| 214 |
+
const schema = generateSchemaFromCode(functionCode);
|
| 215 |
+
changedTool = { ...tool, code: newCode, name: schema.name };
|
| 216 |
+
return changedTool;
|
| 217 |
+
}
|
| 218 |
+
return tool;
|
| 219 |
+
});
|
| 220 |
+
setTools(newTools);
|
| 221 |
+
if (changedTool) saveToolDebounced(changedTool);
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
interface ToolCallPayload {
|
| 225 |
+
name: string;
|
| 226 |
+
arguments?: Record<string, any>;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const extractToolCalls = (text: string): ToolCallPayload[] => {
|
| 230 |
+
const matches = Array.from(
|
| 231 |
+
text.matchAll(/<tool_call>([\s\S]*?)<\/tool_call>/g),
|
| 232 |
+
);
|
| 233 |
+
const toolCalls: ToolCallPayload[] = [];
|
| 234 |
+
|
| 235 |
+
for (const match of matches) {
|
| 236 |
+
try {
|
| 237 |
+
const parsed = JSON.parse(match[1].trim());
|
| 238 |
+
if (parsed && typeof parsed.name === "string") {
|
| 239 |
+
toolCalls.push({
|
| 240 |
+
name: parsed.name,
|
| 241 |
+
arguments: parsed.arguments ?? {},
|
| 242 |
+
});
|
| 243 |
+
}
|
| 244 |
+
} catch {
|
| 245 |
+
// ignore malformed tool call payloads
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return toolCalls;
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
const executeToolCall = async (
|
| 253 |
+
toolCall: ToolCallPayload,
|
| 254 |
+
): Promise<{
|
| 255 |
+
serializedResult: string;
|
| 256 |
+
rendererCode?: string;
|
| 257 |
+
input: Record<string, any>;
|
| 258 |
+
}> => {
|
| 259 |
+
const toolToUse = tools.find((t) => t.name === toolCall.name && t.enabled);
|
| 260 |
+
if (!toolToUse)
|
| 261 |
+
throw new Error(`Tool '${toolCall.name}' not found or is disabled.`);
|
| 262 |
+
|
| 263 |
+
const { functionCode, rendererCode } = extractFunctionAndRenderer(
|
| 264 |
+
toolToUse.code,
|
| 265 |
+
);
|
| 266 |
+
const schema = generateSchemaFromCode(functionCode);
|
| 267 |
+
const properties = schema.parameters?.properties ?? {};
|
| 268 |
+
const paramNames = Object.keys(properties);
|
| 269 |
+
const requiredParams = schema.parameters?.required ?? [];
|
| 270 |
+
const callArgs = toolCall.arguments ?? {};
|
| 271 |
+
|
| 272 |
+
const finalArgs: any[] = [];
|
| 273 |
+
const resolvedArgs: Record<string, any> = Object.create(null);
|
| 274 |
+
|
| 275 |
+
for (const paramName of paramNames) {
|
| 276 |
+
const propertyConfig = properties[paramName] ?? {};
|
| 277 |
+
if (Object.prototype.hasOwnProperty.call(callArgs, paramName)) {
|
| 278 |
+
const value = callArgs[paramName];
|
| 279 |
+
finalArgs.push(value);
|
| 280 |
+
resolvedArgs[paramName] = value;
|
| 281 |
+
} else if (
|
| 282 |
+
Object.prototype.hasOwnProperty.call(propertyConfig, "default")
|
| 283 |
+
) {
|
| 284 |
+
const value = propertyConfig.default;
|
| 285 |
+
finalArgs.push(value);
|
| 286 |
+
resolvedArgs[paramName] = value;
|
| 287 |
+
} else if (!requiredParams.includes(paramName)) {
|
| 288 |
+
finalArgs.push(undefined);
|
| 289 |
+
resolvedArgs[paramName] = undefined;
|
| 290 |
+
} else {
|
| 291 |
+
throw new Error(`Missing required argument: ${paramName}`);
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
for (const extraKey of Object.keys(callArgs)) {
|
| 296 |
+
if (!Object.prototype.hasOwnProperty.call(resolvedArgs, extraKey)) {
|
| 297 |
+
resolvedArgs[extraKey] = callArgs[extraKey];
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
const bodyMatch = functionCode.match(/function[^{]+\{([\s\S]*)\}/);
|
| 302 |
+
if (!bodyMatch) {
|
| 303 |
+
throw new Error(
|
| 304 |
+
"Could not parse function body. Ensure it's a standard `function` declaration.",
|
| 305 |
+
);
|
| 306 |
+
}
|
| 307 |
+
const body = bodyMatch[1];
|
| 308 |
+
const AsyncFunction = Object.getPrototypeOf(
|
| 309 |
+
async function () {},
|
| 310 |
+
).constructor;
|
| 311 |
+
const func = new AsyncFunction(...paramNames, body);
|
| 312 |
+
const result = await func(...finalArgs);
|
| 313 |
+
|
| 314 |
+
return {
|
| 315 |
+
serializedResult: JSON.stringify(result),
|
| 316 |
+
rendererCode,
|
| 317 |
+
input: resolvedArgs,
|
| 318 |
+
};
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
const executeToolCalls = async (
|
| 322 |
+
toolCalls: ToolCallPayload[],
|
| 323 |
+
): Promise<RenderInfo[]> => {
|
| 324 |
+
if (toolCalls.length === 0) {
|
| 325 |
+
return [{ call: "", error: "No valid tool calls found." }];
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
const results: RenderInfo[] = [];
|
| 329 |
+
|
| 330 |
+
for (const toolCall of toolCalls) {
|
| 331 |
+
const callDisplay = `<tool_call>${JSON.stringify(toolCall)}</tool_call>`;
|
| 332 |
+
try {
|
| 333 |
+
const { serializedResult, rendererCode, input } =
|
| 334 |
+
await executeToolCall(toolCall);
|
| 335 |
+
|
| 336 |
+
let parsedResult: unknown;
|
| 337 |
+
try {
|
| 338 |
+
parsedResult = JSON.parse(serializedResult);
|
| 339 |
+
} catch {
|
| 340 |
+
parsedResult = serializedResult;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
results.push({
|
| 344 |
+
call: callDisplay,
|
| 345 |
+
result: parsedResult,
|
| 346 |
+
renderer: rendererCode,
|
| 347 |
+
input,
|
| 348 |
+
});
|
| 349 |
+
} catch (error) {
|
| 350 |
+
results.push({
|
| 351 |
+
call: callDisplay,
|
| 352 |
+
error: getErrorMessage(error),
|
| 353 |
+
});
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
return results;
|
| 358 |
+
};
|
| 359 |
+
|
| 360 |
+
const handleSendMessage = async (): Promise<void> => {
|
| 361 |
+
if (!input.trim() || !isReady) return;
|
| 362 |
+
|
| 363 |
+
const userMessage: Message = { role: "user", content: input };
|
| 364 |
+
let currentMessages: Message[] = [...messages, userMessage];
|
| 365 |
+
setMessages(currentMessages);
|
| 366 |
+
setInput("");
|
| 367 |
+
setIsGenerating(true);
|
| 368 |
+
|
| 369 |
+
try {
|
| 370 |
+
const toolSchemas = tools
|
| 371 |
+
.filter((tool) => tool.enabled)
|
| 372 |
+
.map((tool) => generateSchemaFromCode(tool.code));
|
| 373 |
+
|
| 374 |
+
while (true) {
|
| 375 |
+
const messagesForGeneration = [...currentMessages];
|
| 376 |
+
|
| 377 |
+
setMessages([...currentMessages, { role: "assistant", content: "" }]);
|
| 378 |
+
|
| 379 |
+
let accumulatedContent = "";
|
| 380 |
+
const response = await generateResponse(
|
| 381 |
+
messagesForGeneration,
|
| 382 |
+
toolSchemas,
|
| 383 |
+
(token: string) => {
|
| 384 |
+
accumulatedContent += token;
|
| 385 |
+
setMessages((current) => {
|
| 386 |
+
const updated = [...current];
|
| 387 |
+
updated[updated.length - 1] = {
|
| 388 |
+
role: "assistant",
|
| 389 |
+
content: accumulatedContent,
|
| 390 |
+
};
|
| 391 |
+
return updated;
|
| 392 |
+
});
|
| 393 |
+
},
|
| 394 |
+
);
|
| 395 |
+
|
| 396 |
+
currentMessages.push({ role: "assistant", content: response });
|
| 397 |
+
const toolCalls = extractToolCalls(response);
|
| 398 |
+
|
| 399 |
+
if (toolCalls.length > 0) {
|
| 400 |
+
const toolResults = await executeToolCalls(toolCalls);
|
| 401 |
+
|
| 402 |
+
const toolMessage: ToolMessage = {
|
| 403 |
+
role: "tool",
|
| 404 |
+
content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
|
| 405 |
+
renderInfo: toolResults,
|
| 406 |
+
};
|
| 407 |
+
currentMessages.push(toolMessage);
|
| 408 |
+
setMessages([...currentMessages]);
|
| 409 |
+
continue;
|
| 410 |
+
} else {
|
| 411 |
+
setMessages(currentMessages);
|
| 412 |
+
break;
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
} catch (error) {
|
| 416 |
+
const errorMessage = getErrorMessage(error);
|
| 417 |
+
setMessages([
|
| 418 |
+
...currentMessages,
|
| 419 |
+
{
|
| 420 |
+
role: "assistant",
|
| 421 |
+
content: `Error generating response: ${errorMessage}`,
|
| 422 |
+
},
|
| 423 |
+
]);
|
| 424 |
+
} finally {
|
| 425 |
+
setIsGenerating(false);
|
| 426 |
+
setTimeout(() => inputRef.current?.focus(), 0);
|
| 427 |
+
}
|
| 428 |
+
};
|
| 429 |
+
|
| 430 |
+
const loadSelectedModel = useCallback(async (): Promise<void> => {
|
| 431 |
+
try {
|
| 432 |
+
await loadModel();
|
| 433 |
+
} catch (error) {
|
| 434 |
+
console.error("Failed to load model:", error);
|
| 435 |
+
}
|
| 436 |
+
}, [selectedModel, loadModel]);
|
| 437 |
+
|
| 438 |
+
const saveSelectedModel = useCallback(async (modelId: string) => {
|
| 439 |
+
try {
|
| 440 |
+
const db = await getDB();
|
| 441 |
+
await db.put(SETTINGS_STORE_NAME, {
|
| 442 |
+
key: "selectedModelId",
|
| 443 |
+
value: modelId,
|
| 444 |
+
});
|
| 445 |
+
} catch (error) {
|
| 446 |
+
console.error("Failed to save selected model ID:", error);
|
| 447 |
+
}
|
| 448 |
+
}, []);
|
| 449 |
+
|
| 450 |
+
const loadSelectedModelId = useCallback(async (): Promise<void> => {
|
| 451 |
+
try {
|
| 452 |
+
const db = await getDB();
|
| 453 |
+
const stored = await db.get(SETTINGS_STORE_NAME, "selectedModelId");
|
| 454 |
+
if (stored && stored.value) {
|
| 455 |
+
setSelectedModel(stored.value);
|
| 456 |
+
}
|
| 457 |
+
} catch (error) {
|
| 458 |
+
console.error("Failed to load selected model ID:", error);
|
| 459 |
+
}
|
| 460 |
+
}, []);
|
| 461 |
+
|
| 462 |
+
useEffect(() => {
|
| 463 |
+
loadSelectedModelId();
|
| 464 |
+
}, [loadSelectedModelId]);
|
| 465 |
+
|
| 466 |
+
const handleModelSelect = async (modelId: string) => {
|
| 467 |
+
setSelectedModel(modelId);
|
| 468 |
+
setIsModelDropdownOpen(false);
|
| 469 |
+
await saveSelectedModel(modelId);
|
| 470 |
+
};
|
| 471 |
+
|
| 472 |
+
const handleExampleClick = async (messageText: string): Promise<void> => {
|
| 473 |
+
if (!isReady || isGenerating) return;
|
| 474 |
+
setInput(messageText);
|
| 475 |
+
|
| 476 |
+
const userMessage: Message = { role: "user", content: messageText };
|
| 477 |
+
const currentMessages: Message[] = [...messages, userMessage];
|
| 478 |
+
setMessages(currentMessages);
|
| 479 |
+
setInput("");
|
| 480 |
+
setIsGenerating(true);
|
| 481 |
+
|
| 482 |
+
try {
|
| 483 |
+
const toolSchemas = tools
|
| 484 |
+
.filter((tool) => tool.enabled)
|
| 485 |
+
.map((tool) => generateSchemaFromCode(tool.code));
|
| 486 |
+
|
| 487 |
+
while (true) {
|
| 488 |
+
const messagesForGeneration = [...currentMessages];
|
| 489 |
+
|
| 490 |
+
setMessages([...currentMessages, { role: "assistant", content: "" }]);
|
| 491 |
+
|
| 492 |
+
let accumulatedContent = "";
|
| 493 |
+
const response = await generateResponse(
|
| 494 |
+
messagesForGeneration,
|
| 495 |
+
toolSchemas,
|
| 496 |
+
(token: string) => {
|
| 497 |
+
accumulatedContent += token;
|
| 498 |
+
setMessages((current) => {
|
| 499 |
+
const updated = [...current];
|
| 500 |
+
updated[updated.length - 1] = {
|
| 501 |
+
role: "assistant",
|
| 502 |
+
content: accumulatedContent,
|
| 503 |
+
};
|
| 504 |
+
return updated;
|
| 505 |
+
});
|
| 506 |
+
},
|
| 507 |
+
);
|
| 508 |
+
|
| 509 |
+
currentMessages.push({ role: "assistant", content: response });
|
| 510 |
+
const toolCalls = extractToolCalls(response);
|
| 511 |
+
|
| 512 |
+
if (toolCalls.length > 0) {
|
| 513 |
+
const toolResults = await executeToolCalls(toolCalls);
|
| 514 |
+
|
| 515 |
+
const toolMessage: ToolMessage = {
|
| 516 |
+
role: "tool",
|
| 517 |
+
content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
|
| 518 |
+
renderInfo: toolResults,
|
| 519 |
+
};
|
| 520 |
+
currentMessages.push(toolMessage);
|
| 521 |
+
setMessages([...currentMessages]);
|
| 522 |
+
continue;
|
| 523 |
+
} else {
|
| 524 |
+
setMessages(currentMessages);
|
| 525 |
+
break;
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
} catch (error) {
|
| 529 |
+
const errorMessage = getErrorMessage(error);
|
| 530 |
+
setMessages([
|
| 531 |
+
...currentMessages,
|
| 532 |
+
{
|
| 533 |
+
role: "assistant",
|
| 534 |
+
content: `Error generating response: ${errorMessage}`,
|
| 535 |
+
},
|
| 536 |
+
]);
|
| 537 |
+
} finally {
|
| 538 |
+
setIsGenerating(false);
|
| 539 |
+
setTimeout(() => inputRef.current?.focus(), 0);
|
| 540 |
+
}
|
| 541 |
+
};
|
| 542 |
+
|
| 543 |
+
return (
|
| 544 |
+
<div className="font-sans min-h-screen bg-gradient-to-br from-[#031b4e] via-[#06183d] to-[#010409] text-gray-100 text-[16px] md:text-[17px]">
|
| 545 |
+
{!isReady ? (
|
| 546 |
+
<LoadingScreen
|
| 547 |
+
isLoading={isLoading}
|
| 548 |
+
progress={progress}
|
| 549 |
+
error={error}
|
| 550 |
+
loadSelectedModel={loadSelectedModel}
|
| 551 |
+
selectedModelId={selectedModel}
|
| 552 |
+
isModelDropdownOpen={isModelDropdownOpen}
|
| 553 |
+
setIsModelDropdownOpen={setIsModelDropdownOpen}
|
| 554 |
+
handleModelSelect={handleModelSelect}
|
| 555 |
+
/>
|
| 556 |
+
) : (
|
| 557 |
+
<div className="flex h-screen text-gray-100 w-full gap-6 py-10 px-8">
|
| 558 |
+
<div className="flex-1 flex flex-col p-6 bg-white/5 backdrop-blur-lg border border-white/10 rounded-3xl shadow-[0_35px_65px_rgba(3,27,78,0.55)] min-h-0">
|
| 559 |
+
<div className="flex items-center justify-between mb-6">
|
| 560 |
+
<div className="space-y-1">
|
| 561 |
+
<span className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.35em] text-[#78a9ff]">
|
| 562 |
+
IBM Granite
|
| 563 |
+
</span>
|
| 564 |
+
<h1 className="text-3xl font-semibold text-white">
|
| 565 |
+
Granite-4.0 Tool Studio
|
| 566 |
+
</h1>
|
| 567 |
+
</div>
|
| 568 |
+
<div className="flex items-center gap-3">
|
| 569 |
+
<button
|
| 570 |
+
disabled={isGenerating}
|
| 571 |
+
onClick={clearChat}
|
| 572 |
+
className={`h-10 flex items-center px-4 rounded-full font-semibold text-sm transition-all border ${
|
| 573 |
+
isGenerating
|
| 574 |
+
? "border-white/15 bg-white/10 text-[#a6c8ff] opacity-50 cursor-not-allowed"
|
| 575 |
+
: "border-white/20 bg-white/8 text-[#d0e2ff] hover:border-[#78a9ff]/50 hover:bg-[#0f62fe]/15"
|
| 576 |
+
}`}
|
| 577 |
+
title="Clear chat"
|
| 578 |
+
>
|
| 579 |
+
<RotateCcw size={14} className="mr-2" /> Reset Thread
|
| 580 |
+
</button>
|
| 581 |
+
<button
|
| 582 |
+
onClick={() =>
|
| 583 |
+
setIsToolsPanelVisible((previous) => !previous)
|
| 584 |
+
}
|
| 585 |
+
className={`h-10 flex items-center px-4 rounded-full font-semibold text-sm transition-all border ${
|
| 586 |
+
isToolsPanelVisible
|
| 587 |
+
? "border-[#78a9ff]/60 bg-[#0f62fe]/25 text-white shadow-[0_10px_25px_rgba(15,98,254,0.25)]"
|
| 588 |
+
: "border-white/20 bg-white/8 text-[#d0e2ff] hover:border-[#78a9ff]/50 hover:bg-[#0f62fe]/15"
|
| 589 |
+
}`}
|
| 590 |
+
>
|
| 591 |
+
<Wrench size={16} className="mr-2" />
|
| 592 |
+
{isToolsPanelVisible ? "Hide Tools" : "Show Tools"}
|
| 593 |
+
</button>
|
| 594 |
+
</div>
|
| 595 |
+
</div>
|
| 596 |
+
<div
|
| 597 |
+
ref={chatContainerRef}
|
| 598 |
+
className="flex-grow bg-[#0b1e3f]/80 border border-white/10 rounded-2xl p-6 overflow-y-auto mb-6 space-y-5 shadow-inner min-h-0"
|
| 599 |
+
>
|
| 600 |
+
{messages.length === 0 && isReady ? (
|
| 601 |
+
<ExamplePrompts onExampleClick={handleExampleClick} />
|
| 602 |
+
) : (
|
| 603 |
+
messages.map((msg, index) => {
|
| 604 |
+
const key = `${msg.role}-${index}`;
|
| 605 |
+
if (msg.role === "user") {
|
| 606 |
+
return (
|
| 607 |
+
<div key={key} className="flex justify-end">
|
| 608 |
+
<div className="px-4 py-3 rounded-2xl max-w-md bg-[#0f62fe]/30 border border-[#78a9ff]/40 shadow-[0_20px_45px_rgba(10,49,140,0.25)]">
|
| 609 |
+
<p className="text-md text-white whitespace-pre-wrap">
|
| 610 |
+
{msg.content}
|
| 611 |
+
</p>
|
| 612 |
+
</div>
|
| 613 |
+
</div>
|
| 614 |
+
);
|
| 615 |
+
}
|
| 616 |
+
if (msg.role === "assistant") {
|
| 617 |
+
const isToolCall = msg.content.includes("<tool_call>");
|
| 618 |
+
if (isToolCall) {
|
| 619 |
+
const nextMessage = messages[index + 1];
|
| 620 |
+
const isCompleted = nextMessage?.role === "tool";
|
| 621 |
+
const hasError =
|
| 622 |
+
isCompleted &&
|
| 623 |
+
(nextMessage as ToolMessage).renderInfo.some(
|
| 624 |
+
(info) => !!info.error,
|
| 625 |
+
);
|
| 626 |
+
return (
|
| 627 |
+
<div key={key} className="flex justify-start">
|
| 628 |
+
<div className="px-4 py-3 rounded-2xl bg-white/8 border border-[#0f62fe]/30 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
| 629 |
+
<ToolCallIndicator
|
| 630 |
+
content={msg.content}
|
| 631 |
+
isRunning={!isCompleted}
|
| 632 |
+
hasError={hasError}
|
| 633 |
+
/>
|
| 634 |
+
</div>
|
| 635 |
+
</div>
|
| 636 |
+
);
|
| 637 |
+
}
|
| 638 |
+
return (
|
| 639 |
+
<div key={key} className="flex justify-start">
|
| 640 |
+
<div className="px-4 py-3 rounded-2xl max-w-md bg-white/8 border border-white/15 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
| 641 |
+
<p className="text-md text-[#d0e2ff] whitespace-pre-wrap">
|
| 642 |
+
{msg.content}
|
| 643 |
+
</p>
|
| 644 |
+
</div>
|
| 645 |
+
</div>
|
| 646 |
+
);
|
| 647 |
+
}
|
| 648 |
+
if (msg.role === "tool") {
|
| 649 |
+
const visibleToolResults = msg.renderInfo.filter(
|
| 650 |
+
(info) =>
|
| 651 |
+
info.error || (info.result != null && info.renderer),
|
| 652 |
+
);
|
| 653 |
+
if (visibleToolResults.length === 0) return null;
|
| 654 |
+
return (
|
| 655 |
+
<div key={key} className="flex justify-start">
|
| 656 |
+
<div className="p-4 rounded-2xl bg-white/8 border border-white/15 max-w-lg shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
| 657 |
+
<div className="space-y-4">
|
| 658 |
+
{visibleToolResults.map((info, idx) => (
|
| 659 |
+
<div className="flex flex-col gap-2" key={idx}>
|
| 660 |
+
<div className="text-xs text-[#a6c8ff] font-mono">
|
| 661 |
+
{info.call}
|
| 662 |
+
</div>
|
| 663 |
+
{info.error ? (
|
| 664 |
+
<ResultBlock error={info.error} />
|
| 665 |
+
) : (
|
| 666 |
+
<ToolResultRenderer
|
| 667 |
+
result={info.result}
|
| 668 |
+
rendererCode={info.renderer}
|
| 669 |
+
input={info.input}
|
| 670 |
+
/>
|
| 671 |
+
)}
|
| 672 |
+
</div>
|
| 673 |
+
))}
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
</div>
|
| 677 |
+
);
|
| 678 |
+
}
|
| 679 |
+
return null;
|
| 680 |
+
})
|
| 681 |
+
)}
|
| 682 |
+
</div>
|
| 683 |
+
<div className="flex items-center gap-3">
|
| 684 |
+
<div className="flex flex-1 items-center bg-white/5 border border-white/10 rounded-2xl overflow-hidden shadow-[0_15px_45px_rgba(0,0,0,0.35)]">
|
| 685 |
+
<input
|
| 686 |
+
ref={inputRef}
|
| 687 |
+
type="text"
|
| 688 |
+
value={input}
|
| 689 |
+
onChange={(e) => setInput(e.target.value)}
|
| 690 |
+
onKeyDown={(e) =>
|
| 691 |
+
e.key === "Enter" &&
|
| 692 |
+
!isGenerating &&
|
| 693 |
+
isReady &&
|
| 694 |
+
handleSendMessage()
|
| 695 |
+
}
|
| 696 |
+
disabled={isGenerating || !isReady}
|
| 697 |
+
className="flex-grow bg-transparent px-5 py-3 text-lg text-white placeholder:text-[#a6c8ff]/70 focus:outline-none disabled:opacity-40"
|
| 698 |
+
placeholder={
|
| 699 |
+
isReady
|
| 700 |
+
? "Type your message here..."
|
| 701 |
+
: "Load a Granite model to enable chat"
|
| 702 |
+
}
|
| 703 |
+
/>
|
| 704 |
+
<button
|
| 705 |
+
onClick={handleSendMessage}
|
| 706 |
+
disabled={isGenerating || !isReady}
|
| 707 |
+
className="h-full px-5 py-3 bg-[#0f62fe] hover:bg-[#0043ce] disabled:bg-[#0f62fe]/40 disabled:cursor-not-allowed text-white font-semibold transition-all"
|
| 708 |
+
>
|
| 709 |
+
<Play size={28} />
|
| 710 |
+
</button>
|
| 711 |
+
</div>
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
{isToolsPanelVisible && (
|
| 715 |
+
<div className="w-full md:w-1/2 flex flex-col p-6 bg-white/5 backdrop-blur-lg border border-white/10 rounded-3xl shadow-[0_35px_65px_rgba(3,27,78,0.55)] min-h-0">
|
| 716 |
+
<div className="flex justify-between items-center mb-6">
|
| 717 |
+
<div>
|
| 718 |
+
<span className="text-xs font-semibold uppercase tracking-[0.25em] text-[#78a9ff]">
|
| 719 |
+
Tool Workspace
|
| 720 |
+
</span>
|
| 721 |
+
<h2 className="text-2xl font-semibold text-white mt-1">
|
| 722 |
+
Tools
|
| 723 |
+
</h2>
|
| 724 |
+
</div>
|
| 725 |
+
<button
|
| 726 |
+
onClick={addTool}
|
| 727 |
+
className="flex items-center bg-gradient-to-r from-[#0f62fe] to-[#4589ff] hover:brightness-110 text-white font-semibold py-2 px-4 rounded-full transition-all shadow-[0_15px_35px_rgba(15,98,254,0.35)]"
|
| 728 |
+
>
|
| 729 |
+
<Plus size={16} className="mr-2" /> Add Tool
|
| 730 |
+
</button>
|
| 731 |
+
</div>
|
| 732 |
+
<div
|
| 733 |
+
ref={toolsContainerRef}
|
| 734 |
+
className="flex-grow bg-[#0b1e3f]/60 border border-white/10 rounded-2xl p-4 overflow-y-auto space-y-3"
|
| 735 |
+
>
|
| 736 |
+
{tools.map((tool) => (
|
| 737 |
+
<ToolItem
|
| 738 |
+
key={tool.id}
|
| 739 |
+
tool={tool}
|
| 740 |
+
onToggleEnabled={() => toggleToolEnabled(tool.id)}
|
| 741 |
+
onToggleCollapsed={() => toggleToolCollapsed(tool.id)}
|
| 742 |
+
onExpand={() => expandTool(tool.id)}
|
| 743 |
+
onDelete={() => deleteTool(tool.id)}
|
| 744 |
+
onCodeChange={(newCode) =>
|
| 745 |
+
handleToolCodeChange(tool.id, newCode)
|
| 746 |
+
}
|
| 747 |
+
/>
|
| 748 |
+
))}
|
| 749 |
+
</div>
|
| 750 |
+
</div>
|
| 751 |
+
)}
|
| 752 |
+
</div>
|
| 753 |
+
)}
|
| 754 |
+
</div>
|
| 755 |
+
);
|
| 756 |
+
};
|
| 757 |
+
|
| 758 |
+
export default App;
|
src/components/ExamplePrompts.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type React from "react";
|
| 2 |
+
import { DEFAULT_EXAMPLES, type Example } from "../constants/examples";
|
| 3 |
+
|
| 4 |
+
interface ExamplePromptsProps {
|
| 5 |
+
examples?: Example[];
|
| 6 |
+
onExampleClick: (messageText: string) => void;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
const ExamplePrompts: React.FC<ExamplePromptsProps> = ({
|
| 10 |
+
examples,
|
| 11 |
+
onExampleClick,
|
| 12 |
+
}) => (
|
| 13 |
+
<div className="flex flex-col items-center justify-center h-full space-y-6">
|
| 14 |
+
<div className="text-center mb-6">
|
| 15 |
+
<h2 className="text-3xl font-semibold text-white mb-1">Try an example</h2>
|
| 16 |
+
<p className="text-md text-[#9bb5ff]">Click one to get started</p>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-4xl w-full px-4">
|
| 20 |
+
{(examples || DEFAULT_EXAMPLES).map((example, index) => (
|
| 21 |
+
<button
|
| 22 |
+
key={index}
|
| 23 |
+
onClick={() => onExampleClick(example.messageText)}
|
| 24 |
+
className="group relative overflow-hidden rounded-2xl border border-white/12 bg-white/8 text-left shadow-[0_22px_55px_rgba(3,27,78,0.35)] transition-all hover:-translate-y-0.5 hover:shadow-[0_28px_65px_rgba(15,98,254,0.35)]"
|
| 25 |
+
>
|
| 26 |
+
<span className="pointer-events-none absolute inset-0 bg-gradient-to-r from-[#0f62fe]/25 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
|
| 27 |
+
<div className="relative flex items-center gap-4 px-5 py-4">
|
| 28 |
+
<span className="flex size-12 flex-shrink-0 items-center justify-center rounded-full border border-[#78a9ff]/30 bg-[#0f62fe]/15 text-xl text-[#a6c8ff] transition-all group-hover:scale-105 group-hover:bg-[#0f62fe]/25 group-hover:text-white">
|
| 29 |
+
{example.icon}
|
| 30 |
+
</span>
|
| 31 |
+
<div className="flex flex-1 items-center">
|
| 32 |
+
<span className="text-md font-medium text-white">
|
| 33 |
+
{example.displayText}
|
| 34 |
+
</span>
|
| 35 |
+
</div>
|
| 36 |
+
<span className="flex size-10 items-center justify-center rounded-full border border-white/10 bg-white/10 text-[#9bb5ff] transition-all group-hover:border-[#78a9ff]/40 group-hover:bg-[#0f62fe]/35 group-hover:text-white">
|
| 37 |
+
→
|
| 38 |
+
</span>
|
| 39 |
+
</div>
|
| 40 |
+
</button>
|
| 41 |
+
))}
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
export default ExamplePrompts;
|
src/components/LoadingScreen.tsx
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ChevronDown } from "lucide-react";
|
| 2 |
+
|
| 3 |
+
import { MODEL_OPTIONS } from "../constants/models";
|
| 4 |
+
import IBMLogo from "./icons/IBMLogo";
|
| 5 |
+
import HfLogo from "./icons/HfLogo";
|
| 6 |
+
|
| 7 |
+
import { useEffect, useRef } from "react";
|
| 8 |
+
|
| 9 |
+
// Define the structure of our animated dots
|
| 10 |
+
interface Dot {
|
| 11 |
+
x: number;
|
| 12 |
+
y: number;
|
| 13 |
+
vx: number;
|
| 14 |
+
vy: number;
|
| 15 |
+
radius: number;
|
| 16 |
+
opacity: number;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export const LoadingScreen = ({
|
| 20 |
+
isLoading,
|
| 21 |
+
progress,
|
| 22 |
+
error,
|
| 23 |
+
loadSelectedModel,
|
| 24 |
+
selectedModelId,
|
| 25 |
+
isModelDropdownOpen,
|
| 26 |
+
setIsModelDropdownOpen,
|
| 27 |
+
handleModelSelect,
|
| 28 |
+
}: {
|
| 29 |
+
isLoading: boolean;
|
| 30 |
+
progress: number;
|
| 31 |
+
error: string | null;
|
| 32 |
+
loadSelectedModel: () => void;
|
| 33 |
+
selectedModelId: string;
|
| 34 |
+
isModelDropdownOpen: boolean;
|
| 35 |
+
setIsModelDropdownOpen: (isOpen: boolean) => void;
|
| 36 |
+
handleModelSelect: (modelId: string) => void;
|
| 37 |
+
}) => {
|
| 38 |
+
const model = MODEL_OPTIONS.find((opt) => opt.id === selectedModelId);
|
| 39 |
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
| 40 |
+
|
| 41 |
+
// Background Animation Effect
|
| 42 |
+
useEffect(() => {
|
| 43 |
+
const canvas = canvasRef.current;
|
| 44 |
+
if (!canvas) return;
|
| 45 |
+
|
| 46 |
+
const ctx = canvas.getContext("2d");
|
| 47 |
+
if (!ctx) return;
|
| 48 |
+
|
| 49 |
+
let animationFrameId: number;
|
| 50 |
+
let dots: Dot[] = [];
|
| 51 |
+
const maxConnectionDistance = 130; // Max distance to draw a line between dots
|
| 52 |
+
const dotSpeed = 0.3; // How fast dots move
|
| 53 |
+
|
| 54 |
+
const setup = () => {
|
| 55 |
+
canvas.width = window.innerWidth;
|
| 56 |
+
canvas.height = window.innerHeight;
|
| 57 |
+
dots = [];
|
| 58 |
+
// Adjust dot density based on screen area
|
| 59 |
+
const numDots = Math.floor((canvas.width * canvas.height) / 20000);
|
| 60 |
+
|
| 61 |
+
for (let i = 0; i < numDots; ++i) {
|
| 62 |
+
dots.push({
|
| 63 |
+
x: Math.random() * canvas.width,
|
| 64 |
+
y: Math.random() * canvas.height,
|
| 65 |
+
vx: (Math.random() - 0.5) * dotSpeed, // Random horizontal velocity
|
| 66 |
+
vy: (Math.random() - 0.5) * dotSpeed, // Random vertical velocity
|
| 67 |
+
radius: Math.random() * 1.5 + 0.5,
|
| 68 |
+
opacity: Math.random() * 0.5 + 0.2,
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
const draw = () => {
|
| 74 |
+
if (!ctx) return;
|
| 75 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 76 |
+
|
| 77 |
+
// 1. Update and draw dots
|
| 78 |
+
dots.forEach((dot) => {
|
| 79 |
+
// Update position
|
| 80 |
+
dot.x += dot.vx;
|
| 81 |
+
dot.y += dot.vy;
|
| 82 |
+
|
| 83 |
+
// Bounce off edges
|
| 84 |
+
if (dot.x <= 0 || dot.x >= canvas.width) dot.vx *= -1;
|
| 85 |
+
if (dot.y <= 0 || dot.y >= canvas.height) dot.vy *= -1;
|
| 86 |
+
|
| 87 |
+
// Draw dot
|
| 88 |
+
ctx.beginPath();
|
| 89 |
+
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
|
| 90 |
+
ctx.fillStyle = `rgba(255, 255, 255, ${dot.opacity})`;
|
| 91 |
+
ctx.fill();
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
// 2. Draw connecting lines
|
| 95 |
+
ctx.lineWidth = 0.5; // Use a thin line for connections
|
| 96 |
+
for (let i = 0; i < dots.length; i++) {
|
| 97 |
+
for (let j = i + 1; j < dots.length; j++) {
|
| 98 |
+
const dot1 = dots[i];
|
| 99 |
+
const dot2 = dots[j];
|
| 100 |
+
const dx = dot1.x - dot2.x;
|
| 101 |
+
const dy = dot1.y - dot2.y;
|
| 102 |
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
| 103 |
+
|
| 104 |
+
// If dots are close enough, draw a line
|
| 105 |
+
if (distance < maxConnectionDistance) {
|
| 106 |
+
// Calculate opacity based on distance (closer = more opaque)
|
| 107 |
+
const opacity = 1 - distance / maxConnectionDistance;
|
| 108 |
+
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity * 0.3})`; // Faint white lines
|
| 109 |
+
ctx.beginPath();
|
| 110 |
+
ctx.moveTo(dot1.x, dot1.y);
|
| 111 |
+
ctx.lineTo(dot2.x, dot2.y);
|
| 112 |
+
ctx.stroke();
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
animationFrameId = requestAnimationFrame(draw);
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
const handleResize = () => {
|
| 121 |
+
cancelAnimationFrame(animationFrameId);
|
| 122 |
+
setup();
|
| 123 |
+
draw();
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
setup();
|
| 127 |
+
draw();
|
| 128 |
+
|
| 129 |
+
window.addEventListener("resize", handleResize);
|
| 130 |
+
|
| 131 |
+
return () => {
|
| 132 |
+
window.removeEventListener("resize", handleResize);
|
| 133 |
+
cancelAnimationFrame(animationFrameId);
|
| 134 |
+
};
|
| 135 |
+
}, []);
|
| 136 |
+
|
| 137 |
+
return (
|
| 138 |
+
<div className="relative flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#031b4e] via-[#06183d] to-[#010409] text-gray-100 text-[16px] md:text-[17px] p-8 overflow-hidden">
|
| 139 |
+
{/* Background Canvas for Animation */}
|
| 140 |
+
<canvas
|
| 141 |
+
ref={canvasRef}
|
| 142 |
+
className="absolute top-0 left-0 w-full h-full z-0"
|
| 143 |
+
/>
|
| 144 |
+
|
| 145 |
+
{/* Vignette Overlay */}
|
| 146 |
+
<div className="absolute top-0 left-0 w-full h-full z-10 bg-[radial-gradient(ellipse_at_center,_rgba(3,27,78,0)_30%,_rgba(1,4,9,0.85)_95%)]"></div>
|
| 147 |
+
|
| 148 |
+
{/* Main Content */}
|
| 149 |
+
<div className="relative z-20 max-w-3xl w-full flex flex-col items-center bg-white/5 border border-white/10 backdrop-blur-xl rounded-3xl p-10 shadow-[0_35px_65px_rgba(3,27,78,0.55)] space-y-8">
|
| 150 |
+
<div className="flex items-center justify-center gap-6 text-5xl md:text-6xl">
|
| 151 |
+
<a
|
| 152 |
+
href="https://huggingface.co/ibm-granite"
|
| 153 |
+
target="_blank"
|
| 154 |
+
rel="noopener noreferrer"
|
| 155 |
+
title="IBM Granite"
|
| 156 |
+
>
|
| 157 |
+
<div className="size-24 md:size-28 bg-blue-500 rounded-sm p-2 flex items-center justify-center">
|
| 158 |
+
<IBMLogo className="text-white" />
|
| 159 |
+
</div>
|
| 160 |
+
</a>
|
| 161 |
+
<span className="text-[#78a9ff]">×</span>
|
| 162 |
+
<a
|
| 163 |
+
href="https://huggingface.co/docs/transformers.js"
|
| 164 |
+
target="_blank"
|
| 165 |
+
rel="noopener noreferrer"
|
| 166 |
+
title="Transformers.js"
|
| 167 |
+
>
|
| 168 |
+
<HfLogo className="h-24 md:h-28 text-gray-300 hover:text-white transition-colors" />
|
| 169 |
+
</a>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div className="w-full text-center">
|
| 173 |
+
<h1 className="text-5xl font-semibold mb-2 text-white tracking-tight">
|
| 174 |
+
Granite-4.0 WebGPU
|
| 175 |
+
</h1>
|
| 176 |
+
<p className="text-md md:text-lg text-[#a6c8ff]">
|
| 177 |
+
In-browser tool calling, powered by Transformers.js
|
| 178 |
+
</p>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<div className="w-full text-left text-[#d0e2ff] space-y-4 text-xl">
|
| 182 |
+
<p>
|
| 183 |
+
This demo showcases in-browser tool calling with Granite-4.0, a new
|
| 184 |
+
series of models by{" "}
|
| 185 |
+
<a
|
| 186 |
+
href="https://huggingface.co/ibm-granite"
|
| 187 |
+
target="_blank"
|
| 188 |
+
rel="noopener noreferrer"
|
| 189 |
+
className="text-[#78a9ff] hover:underline font-medium"
|
| 190 |
+
>
|
| 191 |
+
IBM Granite
|
| 192 |
+
</a>{" "}
|
| 193 |
+
designed for edge AI and on-device deployment.
|
| 194 |
+
</p>
|
| 195 |
+
<p>
|
| 196 |
+
Everything runs entirely in your browser with{" "}
|
| 197 |
+
<a
|
| 198 |
+
href="https://huggingface.co/docs/transformers.js"
|
| 199 |
+
target="_blank"
|
| 200 |
+
rel="noopener noreferrer"
|
| 201 |
+
className="text-[#78a9ff] hover:underline font-medium"
|
| 202 |
+
>
|
| 203 |
+
Transformers.js
|
| 204 |
+
</a>{" "}
|
| 205 |
+
and ONNX Runtime Web, meaning no data is sent to a server. It can
|
| 206 |
+
even run offline!
|
| 207 |
+
</p>
|
| 208 |
+
</div>
|
| 209 |
+
|
| 210 |
+
<p className="text-[#a6c8ff]">
|
| 211 |
+
Select a model and click load to get started.
|
| 212 |
+
</p>
|
| 213 |
+
|
| 214 |
+
<div className="relative w-full max-w-lg">
|
| 215 |
+
<div className="flex rounded-2xl border border-white/12 bg-white/10 overflow-hidden shadow-[0_18px_45px_rgba(3,27,78,0.45)]">
|
| 216 |
+
<button
|
| 217 |
+
onClick={isLoading ? undefined : loadSelectedModel}
|
| 218 |
+
disabled={isLoading}
|
| 219 |
+
className={`flex-1 flex items-center justify-center font-semibold transition-all text-lg ${isLoading ? "bg-white/5 text-[#8da2d8] cursor-not-allowed" : "bg-[#0f62fe] hover:bg-[#0043ce] text-white"}`}
|
| 220 |
+
>
|
| 221 |
+
<div className="px-6 py-3">
|
| 222 |
+
{isLoading ? (
|
| 223 |
+
<div className="flex items-center">
|
| 224 |
+
<span className="inline-block w-5 h-5 border-2 border-white/80 border-t-transparent rounded-full animate-spin"></span>
|
| 225 |
+
<span className="ml-3 text-md font-medium">
|
| 226 |
+
Loading... ({progress}%)
|
| 227 |
+
</span>
|
| 228 |
+
</div>
|
| 229 |
+
) : (
|
| 230 |
+
`Load ${model?.label}`
|
| 231 |
+
)}
|
| 232 |
+
</div>
|
| 233 |
+
</button>
|
| 234 |
+
<button
|
| 235 |
+
onClick={(e) => {
|
| 236 |
+
if (!isLoading) {
|
| 237 |
+
e.stopPropagation();
|
| 238 |
+
setIsModelDropdownOpen(!isModelDropdownOpen);
|
| 239 |
+
}
|
| 240 |
+
}}
|
| 241 |
+
aria-label="Select model"
|
| 242 |
+
className="px-4 py-3 border-l border-white/15 bg-[#0f62fe] hover:bg-[#0043ce] transition-colors text-white disabled:cursor-not-allowed disabled:bg-white/5"
|
| 243 |
+
disabled={isLoading}
|
| 244 |
+
>
|
| 245 |
+
<ChevronDown size={24} />
|
| 246 |
+
</button>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
{isModelDropdownOpen && (
|
| 250 |
+
<div className="absolute left-0 right-0 bottom-full mb-3 bg-[#02102c]/98 border border-white/12 rounded-xl shadow-[0_22px_55px_rgba(3,27,78,0.55)] z-10 w-full overflow-visible backdrop-blur-2xl">
|
| 251 |
+
{MODEL_OPTIONS.map((option) => (
|
| 252 |
+
<button
|
| 253 |
+
key={option.id}
|
| 254 |
+
onClick={() => handleModelSelect(option.id)}
|
| 255 |
+
className={`w-full px-5 py-3 text-left text-sm font-medium rounded-lg border transition-all ${
|
| 256 |
+
selectedModelId === option.id
|
| 257 |
+
? "border-[#78a9ff]/60 bg-[#0f62fe]/25 text-white shadow-[0_10px_25px_rgba(15,98,254,0.25)]"
|
| 258 |
+
: "border-transparent text-[#d0e2ff] hover:border-[#78a9ff]/30 hover:bg-white/12 hover:text-white"
|
| 259 |
+
}`}
|
| 260 |
+
>
|
| 261 |
+
<div className="font-medium">{option.label}</div>
|
| 262 |
+
<div className="text-md text-[#95a8dd]">{option.size}</div>
|
| 263 |
+
</button>
|
| 264 |
+
))}
|
| 265 |
+
</div>
|
| 266 |
+
)}
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
{error && (
|
| 270 |
+
<div className="bg-[#2d0709]/70 border border-[#ff8389]/40 rounded-2xl p-4 w-full max-w-md text-center shadow-[0_15px_35px_rgba(45,7,9,0.4)]">
|
| 271 |
+
<p className="text-sm text-[#ffb3b8]">Error: {error}</p>
|
| 272 |
+
<button
|
| 273 |
+
onClick={loadSelectedModel}
|
| 274 |
+
className="mt-3 text-sm px-4 py-2 rounded-full bg-white/15 hover:bg-white/25 border border-white/20 text-white font-semibold transition-all"
|
| 275 |
+
>
|
| 276 |
+
Retry
|
| 277 |
+
</button>
|
| 278 |
+
</div>
|
| 279 |
+
)}
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
{/* Click-away listener for dropdown */}
|
| 283 |
+
{isModelDropdownOpen && (
|
| 284 |
+
<div
|
| 285 |
+
className="fixed inset-0 z-5"
|
| 286 |
+
onClick={() => setIsModelDropdownOpen(false)}
|
| 287 |
+
/>
|
| 288 |
+
)}
|
| 289 |
+
</div>
|
| 290 |
+
);
|
| 291 |
+
};
|
src/components/ResultBlock.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type React from "react";
|
| 2 |
+
|
| 3 |
+
const ResultBlock: React.FC<{ error?: string; result?: any }> = ({
|
| 4 |
+
error,
|
| 5 |
+
result,
|
| 6 |
+
}) => (
|
| 7 |
+
<div
|
| 8 |
+
className={
|
| 9 |
+
error
|
| 10 |
+
? "bg-red-900 border border-red-600 rounded p-3"
|
| 11 |
+
: "bg-gray-700 border border-gray-600 rounded p-3"
|
| 12 |
+
}
|
| 13 |
+
>
|
| 14 |
+
{error ? <p className="text-red-300 text-sm">Error: {error}</p> : null}
|
| 15 |
+
<pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto mt-2">
|
| 16 |
+
{typeof result === "object" ? JSON.stringify(result, null, 2) : result}
|
| 17 |
+
</pre>
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
export default ResultBlock;
|
src/components/ToolCallIndicator.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type React from "react";
|
| 2 |
+
import { extractToolCallContent } from "../utils";
|
| 3 |
+
|
| 4 |
+
const ToolCallIndicator: React.FC<{
|
| 5 |
+
content: string;
|
| 6 |
+
isRunning: boolean;
|
| 7 |
+
hasError: boolean;
|
| 8 |
+
}> = ({ content, isRunning, hasError }) => {
|
| 9 |
+
const toolCalls = extractToolCallContent(content);
|
| 10 |
+
const displayContent = toolCalls?.join("\n") ?? "...";
|
| 11 |
+
|
| 12 |
+
return (
|
| 13 |
+
<div
|
| 14 |
+
className={`transition-all duration-500 ease-in-out rounded-lg p-4 ${
|
| 15 |
+
isRunning
|
| 16 |
+
? "bg-gradient-to-r from-yellow-900/30 to-orange-900/30 border border-yellow-600/50"
|
| 17 |
+
: hasError
|
| 18 |
+
? "bg-gradient-to-r from-red-900/30 to-rose-900/30 border border-red-600/50"
|
| 19 |
+
: "bg-gradient-to-r from-green-900/30 to-emerald-900/30 border border-green-600/50"
|
| 20 |
+
}`}
|
| 21 |
+
>
|
| 22 |
+
<div className="flex items-start space-x-3">
|
| 23 |
+
<div className="flex-shrink-0">
|
| 24 |
+
<div className="relative size-8">
|
| 25 |
+
{/* Spinner for running */}
|
| 26 |
+
<div
|
| 27 |
+
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
|
| 28 |
+
isRunning ? "opacity-100" : "opacity-0 pointer-events-none"
|
| 29 |
+
}`}
|
| 30 |
+
>
|
| 31 |
+
<div className="size-8 bg-green-400/0 border-2 border-yellow-400 border-t-transparent rounded-full animate-spin"></div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
{/* Cross for error */}
|
| 35 |
+
<div
|
| 36 |
+
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
|
| 37 |
+
hasError ? "opacity-100" : "opacity-0 pointer-events-none"
|
| 38 |
+
}`}
|
| 39 |
+
>
|
| 40 |
+
<div className="size-8 bg-red-400/100 rounded-full flex items-center justify-center transition-colors duration-500 ease-in-out">
|
| 41 |
+
<span className="text-md text-gray-900 font-bold">✗</span>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
{/* Tick for success */}
|
| 46 |
+
<div
|
| 47 |
+
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
|
| 48 |
+
!isRunning && !hasError
|
| 49 |
+
? "opacity-100"
|
| 50 |
+
: "opacity-0 pointer-events-none"
|
| 51 |
+
}`}
|
| 52 |
+
>
|
| 53 |
+
<div className="size-8 bg-green-400/100 rounded-full flex items-center justify-center transition-colors duration-500 ease-in-out">
|
| 54 |
+
<span className="text-md text-gray-900 font-bold">✓</span>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="flex-grow min-w-0">
|
| 60 |
+
<div className="flex items-center space-x-2 mb-2">
|
| 61 |
+
<span
|
| 62 |
+
className={`font-semibold text-sm transition-colors duration-500 ease-in-out ${
|
| 63 |
+
isRunning
|
| 64 |
+
? "text-yellow-400"
|
| 65 |
+
: hasError
|
| 66 |
+
? "text-red-400"
|
| 67 |
+
: "text-green-400"
|
| 68 |
+
}`}
|
| 69 |
+
>
|
| 70 |
+
🔧 Tool Call
|
| 71 |
+
</span>
|
| 72 |
+
{isRunning && (
|
| 73 |
+
<span className="text-yellow-300 text-xs animate-pulse">
|
| 74 |
+
Running...
|
| 75 |
+
</span>
|
| 76 |
+
)}
|
| 77 |
+
</div>
|
| 78 |
+
<div className="bg-gray-800/50 rounded p-2 mb-2">
|
| 79 |
+
<code className="text-sm text-gray-300 font-mono break-all">
|
| 80 |
+
{displayContent}
|
| 81 |
+
</code>
|
| 82 |
+
</div>
|
| 83 |
+
<p
|
| 84 |
+
className={`text-sm transition-colors duration-500 ease-in-out ${
|
| 85 |
+
isRunning
|
| 86 |
+
? "text-yellow-200"
|
| 87 |
+
: hasError
|
| 88 |
+
? "text-red-200"
|
| 89 |
+
: "text-green-200"
|
| 90 |
+
}`}
|
| 91 |
+
>
|
| 92 |
+
{isRunning
|
| 93 |
+
? "Executing tool call..."
|
| 94 |
+
: hasError
|
| 95 |
+
? "Tool call failed"
|
| 96 |
+
: "Tool call completed"}
|
| 97 |
+
</p>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
);
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
export default ToolCallIndicator;
|
src/components/ToolItem.tsx
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Editor from "@monaco-editor/react";
|
| 2 |
+
import { ChevronUp, ChevronDown, Trash2, Power } from "lucide-react";
|
| 3 |
+
import { useMemo } from "react";
|
| 4 |
+
|
| 5 |
+
import { extractFunctionAndRenderer, generateSchemaFromCode } from "../utils";
|
| 6 |
+
|
| 7 |
+
export interface Tool {
|
| 8 |
+
id: number;
|
| 9 |
+
name: string;
|
| 10 |
+
code: string;
|
| 11 |
+
enabled: boolean;
|
| 12 |
+
isCollapsed?: boolean;
|
| 13 |
+
renderer?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface ToolItemProps {
|
| 17 |
+
tool: Tool;
|
| 18 |
+
onToggleEnabled: () => void;
|
| 19 |
+
onToggleCollapsed: () => void;
|
| 20 |
+
onExpand: () => void;
|
| 21 |
+
onDelete: () => void;
|
| 22 |
+
onCodeChange: (newCode: string) => void;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const ToolItem: React.FC<ToolItemProps> = ({
|
| 26 |
+
tool,
|
| 27 |
+
onToggleEnabled,
|
| 28 |
+
onToggleCollapsed,
|
| 29 |
+
onDelete,
|
| 30 |
+
onCodeChange,
|
| 31 |
+
}) => {
|
| 32 |
+
const { functionCode } = extractFunctionAndRenderer(tool.code);
|
| 33 |
+
const schema = useMemo(
|
| 34 |
+
() => generateSchemaFromCode(functionCode),
|
| 35 |
+
[functionCode],
|
| 36 |
+
);
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<div
|
| 40 |
+
className={`rounded-2xl p-5 border border-white/10 bg-white/5 backdrop-blur-sm transition-all shadow-[0_18px_55px_rgba(3,27,78,0.45)] ${!tool.enabled ? "opacity-40" : ""}`}
|
| 41 |
+
>
|
| 42 |
+
<div
|
| 43 |
+
className="flex justify-between items-center cursor-pointer"
|
| 44 |
+
onClick={onToggleCollapsed}
|
| 45 |
+
>
|
| 46 |
+
<div>
|
| 47 |
+
<h3 className="text-lg font-semibold text-[#78a9ff] font-mono">
|
| 48 |
+
{schema.name}
|
| 49 |
+
</h3>
|
| 50 |
+
<div className="text-xs text-[#a6c8ff]/80 mt-1">
|
| 51 |
+
{schema.description}
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
<div className="flex items-center space-x-3">
|
| 55 |
+
<button
|
| 56 |
+
onClick={(e) => {
|
| 57 |
+
e.stopPropagation();
|
| 58 |
+
onToggleEnabled();
|
| 59 |
+
}}
|
| 60 |
+
className={`p-1.5 rounded-full border transition-all ${tool.enabled ? "border-[#0f62fe]/50 bg-[#0f62fe]/20 text-[#78a9ff]" : "border-white/15 bg-white/5 text-white/60"}`}
|
| 61 |
+
>
|
| 62 |
+
<Power size={18} />
|
| 63 |
+
</button>
|
| 64 |
+
<button
|
| 65 |
+
onClick={(e) => {
|
| 66 |
+
e.stopPropagation();
|
| 67 |
+
onDelete();
|
| 68 |
+
}}
|
| 69 |
+
className="p-2 text-[#ff8389] hover:text-white border border-transparent hover:border-[#ff8389]/40 hover:bg-[#ff8389]/10 rounded-full transition-all"
|
| 70 |
+
>
|
| 71 |
+
<Trash2 size={18} />
|
| 72 |
+
</button>
|
| 73 |
+
<button
|
| 74 |
+
onClick={(e) => {
|
| 75 |
+
e.stopPropagation();
|
| 76 |
+
onToggleCollapsed();
|
| 77 |
+
}}
|
| 78 |
+
className="p-2 text-white/70 hover:text-white border border-white/10 hover:border-white/30 hover:bg-white/10 rounded-full transition-all"
|
| 79 |
+
>
|
| 80 |
+
{tool.isCollapsed ? (
|
| 81 |
+
<ChevronDown size={20} />
|
| 82 |
+
) : (
|
| 83 |
+
<ChevronUp size={20} />
|
| 84 |
+
)}
|
| 85 |
+
</button>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
{!tool.isCollapsed && (
|
| 89 |
+
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 90 |
+
<div className="md:col-span-2">
|
| 91 |
+
<label className="text-xs font-semibold uppercase tracking-[0.2em] text-[#a6c8ff]/80">
|
| 92 |
+
Implementation & Renderer
|
| 93 |
+
</label>
|
| 94 |
+
<div
|
| 95 |
+
className="mt-1 rounded-2xl overflow-visible border border-white/12 bg-[#031b4e]/60"
|
| 96 |
+
style={{ overflow: "visible" }}
|
| 97 |
+
>
|
| 98 |
+
<Editor
|
| 99 |
+
height="300px"
|
| 100 |
+
language="javascript"
|
| 101 |
+
theme="vs-dark"
|
| 102 |
+
value={tool.code}
|
| 103 |
+
onChange={(value) => onCodeChange(value || "")}
|
| 104 |
+
options={{
|
| 105 |
+
minimap: { enabled: false },
|
| 106 |
+
scrollbar: { verticalScrollbarSize: 10 },
|
| 107 |
+
fontSize: 14,
|
| 108 |
+
lineDecorationsWidth: 0,
|
| 109 |
+
lineNumbersMinChars: 3,
|
| 110 |
+
scrollBeyondLastLine: false,
|
| 111 |
+
}}
|
| 112 |
+
/>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
<div className="flex flex-col">
|
| 116 |
+
<label className="text-xs font-semibold uppercase tracking-[0.2em] text-[#a6c8ff]/80">
|
| 117 |
+
Generated Schema
|
| 118 |
+
</label>
|
| 119 |
+
<div className="mt-1 rounded-2xl flex-grow overflow-visible border border-white/12 bg-[#031b4e]/60">
|
| 120 |
+
<Editor
|
| 121 |
+
height="300px"
|
| 122 |
+
language="json"
|
| 123 |
+
theme="vs-dark"
|
| 124 |
+
value={JSON.stringify(schema, null, 2)}
|
| 125 |
+
options={{
|
| 126 |
+
readOnly: true,
|
| 127 |
+
minimap: { enabled: false },
|
| 128 |
+
scrollbar: { verticalScrollbarSize: 10 },
|
| 129 |
+
lineNumbers: "off",
|
| 130 |
+
glyphMargin: false,
|
| 131 |
+
folding: false,
|
| 132 |
+
lineDecorationsWidth: 0,
|
| 133 |
+
lineNumbersMinChars: 0,
|
| 134 |
+
scrollBeyondLastLine: false,
|
| 135 |
+
fontSize: 12,
|
| 136 |
+
}}
|
| 137 |
+
/>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
)}
|
| 142 |
+
</div>
|
| 143 |
+
);
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
export default ToolItem;
|
src/components/ToolResultRenderer.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import ResultBlock from "./ResultBlock";
|
| 3 |
+
|
| 4 |
+
const ToolResultRenderer: React.FC<{
|
| 5 |
+
result: any;
|
| 6 |
+
rendererCode?: string;
|
| 7 |
+
input?: any;
|
| 8 |
+
}> = ({ result, rendererCode, input }) => {
|
| 9 |
+
if (!rendererCode) {
|
| 10 |
+
return <ResultBlock result={result} />;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
try {
|
| 14 |
+
const exportMatch = rendererCode.match(/export\s+default\s+(.*)/s);
|
| 15 |
+
if (!exportMatch) {
|
| 16 |
+
throw new Error("Invalid renderer format - no export default found");
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const componentCode = exportMatch[1].trim();
|
| 20 |
+
const componentFunction = new Function(
|
| 21 |
+
"React",
|
| 22 |
+
"input",
|
| 23 |
+
"output",
|
| 24 |
+
`
|
| 25 |
+
const { createElement: h, Fragment } = React;
|
| 26 |
+
const JSXComponent = ${componentCode};
|
| 27 |
+
return JSXComponent(input, output);
|
| 28 |
+
`,
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
const element = componentFunction(React, input || {}, result);
|
| 32 |
+
return element;
|
| 33 |
+
} catch (error) {
|
| 34 |
+
return (
|
| 35 |
+
<ResultBlock
|
| 36 |
+
error={error instanceof Error ? error.message : "Unknown error"}
|
| 37 |
+
result={result}
|
| 38 |
+
/>
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
export default ToolResultRenderer;
|
src/components/icons/HfLogo.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type React from "react";
|
| 2 |
+
|
| 3 |
+
export default (props: React.SVGProps<SVGSVGElement>) => (
|
| 4 |
+
<svg
|
| 5 |
+
{...props}
|
| 6 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 7 |
+
viewBox="0 0 24 24"
|
| 8 |
+
fill="currentColor"
|
| 9 |
+
>
|
| 10 |
+
<path
|
| 11 |
+
d="M2.25 11.535c0-3.407 1.847-6.554 4.844-8.258a9.822 9.822 0 019.687 0c2.997 1.704 4.844 4.851 4.844 8.258 0 5.266-4.337 9.535-9.687 9.535S2.25 16.8 2.25 11.535z"
|
| 12 |
+
fill="#FF9D0B"
|
| 13 |
+
></path>
|
| 14 |
+
<path
|
| 15 |
+
d="M11.938 20.086c4.797 0 8.687-3.829 8.687-8.551 0-4.722-3.89-8.55-8.687-8.55-4.798 0-8.688 3.828-8.688 8.55 0 4.722 3.89 8.55 8.688 8.55z"
|
| 16 |
+
fill="#FFD21E"
|
| 17 |
+
></path>
|
| 18 |
+
<path
|
| 19 |
+
d="M11.875 15.113c2.457 0 3.25-2.156 3.25-3.263 0-.576-.393-.394-1.023-.089-.582.283-1.365.675-2.224.675-1.798 0-3.25-1.693-3.25-.586 0 1.107.79 3.263 3.25 3.263h-.003z"
|
| 20 |
+
fill="#FF323D"
|
| 21 |
+
></path>
|
| 22 |
+
<path
|
| 23 |
+
d="M14.76 9.21c.32.108.445.753.767.585.447-.233.707-.708.659-1.204a1.235 1.235 0 00-.879-1.059 1.262 1.262 0 00-1.33.394c-.322.384-.377.92-.14 1.36.153.283.638-.177.925-.079l-.002.003zm-5.887 0c-.32.108-.448.753-.768.585a1.226 1.226 0 01-.658-1.204c.048-.495.395-.913.878-1.059a1.262 1.262 0 011.33.394c.322.384.377.92.14 1.36-.152.283-.64-.177-.925-.079l.003.003zm1.12 5.34a2.166 2.166 0 011.325-1.106c.07-.02.144.06.219.171l.192.306c.069.1.139.175.209.175.074 0 .15-.074.223-.172l.205-.302c.08-.11.157-.188.234-.165.537.168.986.536 1.25 1.026.932-.724 1.275-1.905 1.275-2.633 0-.508-.306-.426-.81-.19l-.616.296c-.52.24-1.148.48-1.824.48-.676 0-1.302-.24-1.823-.48l-.589-.283c-.52-.248-.838-.342-.838.177 0 .703.32 1.831 1.187 2.56l.18.14z"
|
| 24 |
+
fill="#3A3B45"
|
| 25 |
+
></path>
|
| 26 |
+
<path
|
| 27 |
+
d="M17.812 10.366a.806.806 0 00.813-.8c0-.441-.364-.8-.813-.8a.806.806 0 00-.812.8c0 .442.364.8.812.8zm-11.624 0a.806.806 0 00.812-.8c0-.441-.364-.8-.812-.8a.806.806 0 00-.813.8c0 .442.364.8.813.8zM4.515 13.073c-.405 0-.765.162-1.017.46a1.455 1.455 0 00-.333.925 1.801 1.801 0 00-.485-.074c-.387 0-.737.146-.985.409a1.41 1.41 0 00-.2 1.722 1.302 1.302 0 00-.447.694c-.06.222-.12.69.2 1.166a1.267 1.267 0 00-.093 1.236c.238.533.81.958 1.89 1.405l.24.096c.768.3 1.473.492 1.478.494.89.243 1.808.375 2.732.394 1.465 0 2.513-.443 3.115-1.314.93-1.342.842-2.575-.274-3.763l-.151-.154c-.692-.684-1.155-1.69-1.25-1.912-.195-.655-.71-1.383-1.562-1.383-.46.007-.889.233-1.15.605-.25-.31-.495-.553-.715-.694a1.87 1.87 0 00-.993-.312zm14.97 0c.405 0 .767.162 1.017.46.216.262.333.588.333.925.158-.047.322-.071.487-.074.388 0 .738.146.985.409a1.41 1.41 0 01.2 1.722c.22.178.377.422.445.694.06.222.12.69-.2 1.166.244.37.279.836.093 1.236-.238.533-.81.958-1.889 1.405l-.239.096c-.77.3-1.475.492-1.48.494-.89.243-1.808.375-2.732.394-1.465 0-2.513-.443-3.115-1.314-.93-1.342-.842-2.575.274-3.763l.151-.154c.695-.684 1.157-1.69 1.252-1.912.195-.655.708-1.383 1.56-1.383.46.007.889.233 1.15.605.25-.31.495-.553.718-.694.244-.162.523-.265.814-.3l.176-.012z"
|
| 28 |
+
fill="#FF9D0B"
|
| 29 |
+
></path>
|
| 30 |
+
<path
|
| 31 |
+
d="M9.785 20.132c.688-.994.638-1.74-.305-2.667-.945-.928-1.495-2.288-1.495-2.288s-.205-.788-.672-.714c-.468.074-.81 1.25.17 1.971.977.721-.195 1.21-.573.534-.375-.677-1.405-2.416-1.94-2.751-.532-.332-.907-.148-.782.541.125.687 2.357 2.35 2.14 2.707-.218.362-.983-.42-.983-.42S2.953 14.9 2.43 15.46c-.52.558.398 1.026 1.7 1.803 1.308.778 1.41.985 1.225 1.28-.187.295-3.07-2.1-3.34-1.083-.27 1.011 2.943 1.304 2.745 2.006-.2.7-2.265-1.324-2.685-.537-.425.79 2.913 1.718 2.94 1.725 1.075.276 3.813.859 4.77-.522zm4.432 0c-.687-.994-.64-1.74.305-2.667.943-.928 1.493-2.288 1.493-2.288s.205-.788.675-.714c.465.074.807 1.25-.17 1.971-.98.721.195 1.21.57.534.377-.677 1.407-2.416 1.94-2.751.532-.332.91-.148.782.541-.125.687-2.355 2.35-2.137 2.707.215.362.98-.42.98-.42S21.05 14.9 21.57 15.46c.52.558-.395 1.026-1.7 1.803-1.308.778-1.408.985-1.225 1.28.187.295 3.07-2.1 3.34-1.083.27 1.011-2.94 1.304-2.743 2.006.2.7 2.263-1.324 2.685-.537.423.79-2.912 1.718-2.94 1.725-1.077.276-3.815.859-4.77-.522z"
|
| 32 |
+
fill="#FFD21E"
|
| 33 |
+
></path>
|
| 34 |
+
</svg>
|
| 35 |
+
);
|
src/components/icons/IBMLogo.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type React from "react";
|
| 2 |
+
|
| 3 |
+
export default (props: React.SVGProps<SVGSVGElement>) => (
|
| 4 |
+
<svg
|
| 5 |
+
{...props}
|
| 6 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 7 |
+
viewBox="0 0 1000 400"
|
| 8 |
+
fill="currentColor"
|
| 9 |
+
>
|
| 10 |
+
<path d="M0 0v27.367h194.648V0H0zm222.226 0v27.367h277.383S471.276 0 433.75 0H222.226zm331.797 0v27.367h167.812L711.875 0H554.023zm288.125 0l-9.961 27.367h166.289V0H842.148zM0 53.222v27.367h194.648V53.222H0zm222.226.039V80.59h309.57s-3.615-21.063-9.922-27.329H222.226zm331.797 0V80.59h186.211l-9.219-27.329H554.023zm268.203 0l-9.219 27.329h185.469V53.261h-176.25zM55.937 106.444v27.406h84.297v-27.406H55.937zm222.227 0v27.406h84.297v-27.406h-84.297zm166.289 0v27.406h84.297s5.352-14.473 5.352-27.406h-89.649zm165.508 0v27.406h149.453l-9.961-27.406H609.961zm193.906 0l-10 27.406h150.195v-27.406H803.867zm-747.93 53.262v27.367h84.297v-27.367H55.937zm222.227 0v27.367h215.312s18.012-14.042 23.75-27.367H278.164zm331.797 0v27.367h84.297v-15.234l5.352 15.234h154.414l5.742-15.234v15.234h84.297v-27.367H785.82l-8.398 23.18-8.438-23.18H609.961zM55.937 212.928v27.367h84.297v-27.367H55.937zm222.227 0v27.367h239.062c-5.739-13.281-23.75-27.367-23.75-27.367H278.164zm331.797 0v27.367h84.297v-27.367h-84.297zm99.609 0l10.195 27.367h115.781l9.688-27.367H709.57zm150.195 0v27.367h84.297v-27.367h-84.297zM55.937 266.15v27.366h84.297V266.15H55.937zm222.227 0v27.366h84.297V266.15h-84.297zm166.289 0v27.366h89.648c0-12.915-5.352-27.366-5.352-27.366h-84.296zm165.508 0v27.366h84.297V266.15h-84.297zm118.75 0l9.883 27.366h77.617l9.961-27.366h-97.461zm131.054 0v27.366h84.297V266.15h-84.297zM1.523 319.372v27.406h194.648v-27.406H1.523zm220.703 0v27.406h299.648c6.307-6.275 9.922-27.406 9.922-27.406h-309.57zm333.321 0v27.406h138.711v-27.406H555.547zm192.343 0l10.156 27.406h39.492l9.531-27.406H747.89zm111.875 0v27.406H1000v-27.406H859.765zM1.523 372.633V400h194.648v-27.367H1.523zm220.703 0v27.328H433.75c37.526 0 65.859-27.328 65.859-27.328H222.226zm333.321 0V400h138.711v-27.367H555.547zm211.601 0l9.766 27.29 1.68.038 9.922-27.328h-21.368zm92.617 0V400H1000v-27.367H859.765z" />
|
| 11 |
+
</svg>
|
| 12 |
+
);
|
src/constants/db.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const DB_NAME = "tool-caller-db";
|
| 2 |
+
export const STORE_NAME = "tools";
|
| 3 |
+
export const SETTINGS_STORE_NAME = "settings";
|
src/constants/examples.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Example {
|
| 2 |
+
icon: string;
|
| 3 |
+
displayText: string;
|
| 4 |
+
messageText: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const DEFAULT_EXAMPLES: Example[] = [
|
| 8 |
+
{
|
| 9 |
+
icon: "🌍",
|
| 10 |
+
displayText: "Where am I and what time is it?",
|
| 11 |
+
messageText: "Where am I and what time is it?",
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
icon: "😂",
|
| 15 |
+
displayText: "Tell me a joke.",
|
| 16 |
+
messageText: "Tell me a joke.",
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
icon: "🔢",
|
| 20 |
+
displayText: "Solve a math problem",
|
| 21 |
+
messageText: "What is 123 times 456 divided by 789?",
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
icon: "😴",
|
| 25 |
+
displayText: "Sleep for 3 seconds",
|
| 26 |
+
messageText: "Sleep for 3 seconds",
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
icon: "🎲",
|
| 30 |
+
displayText: "Generate a random number",
|
| 31 |
+
messageText: "Generate a random number between 1 and 100.",
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
icon: "📹",
|
| 35 |
+
displayText: "Play a video",
|
| 36 |
+
messageText:
|
| 37 |
+
'Open this website: "https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1" and do nothing else.',
|
| 38 |
+
},
|
| 39 |
+
];
|
src/constants/models.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const MODEL_OPTIONS = [
|
| 2 |
+
{
|
| 3 |
+
id: "350M",
|
| 4 |
+
modelId: "onnx-community/granite-4.0-350m-ONNX-web",
|
| 5 |
+
dtype: "fp16",
|
| 6 |
+
label: "Granite-4.0 350M (fp16)",
|
| 7 |
+
size: "A lightweight 350M instruct model (~709 MB in size).",
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
id: "1B",
|
| 11 |
+
modelId: "onnx-community/granite-4.0-1b-ONNX-web",
|
| 12 |
+
dtype: "q4",
|
| 13 |
+
label: "Granite-4.0 1B (q4)",
|
| 14 |
+
size: "A medium-sized instruct model (~1.78 GB in size).",
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
id: "3B",
|
| 18 |
+
modelId: "onnx-community/granite-4.0-micro-ONNX-web",
|
| 19 |
+
dtype: "q4f16",
|
| 20 |
+
label: "Granite-4.0 3B (q4f16)",
|
| 21 |
+
size: "A large 3B parameter instruct model (~2.3 GB in size).",
|
| 22 |
+
},
|
| 23 |
+
] as const;
|
src/hooks/useLLM.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef, useCallback } from "react";
|
| 2 |
+
import {
|
| 3 |
+
AutoModelForCausalLM,
|
| 4 |
+
AutoTokenizer,
|
| 5 |
+
TextStreamer,
|
| 6 |
+
} from "@huggingface/transformers";
|
| 7 |
+
import { MODEL_OPTIONS } from "../constants/models";
|
| 8 |
+
|
| 9 |
+
interface LLMState {
|
| 10 |
+
isLoading: boolean;
|
| 11 |
+
isReady: boolean;
|
| 12 |
+
error: string | null;
|
| 13 |
+
progress: number;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface LLMInstance {
|
| 17 |
+
model: any;
|
| 18 |
+
tokenizer: any;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
let moduleCache: {
|
| 22 |
+
[modelId: string]: {
|
| 23 |
+
instance: LLMInstance | null;
|
| 24 |
+
loadingPromise: Promise<LLMInstance> | null;
|
| 25 |
+
};
|
| 26 |
+
} = {};
|
| 27 |
+
|
| 28 |
+
export const useLLM = (modelName?: string) => {
|
| 29 |
+
const [state, setState] = useState<LLMState>({
|
| 30 |
+
isLoading: false,
|
| 31 |
+
isReady: false,
|
| 32 |
+
error: null,
|
| 33 |
+
progress: 0,
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
const instanceRef = useRef<LLMInstance | null>(null);
|
| 37 |
+
const loadingPromiseRef = useRef<Promise<LLMInstance> | null>(null);
|
| 38 |
+
|
| 39 |
+
const abortControllerRef = useRef<AbortController | null>(null);
|
| 40 |
+
const pastKeyValuesRef = useRef<any>(null);
|
| 41 |
+
|
| 42 |
+
const { modelId, dtype } = MODEL_OPTIONS.find((opt) => opt.id === modelName)!;
|
| 43 |
+
const loadModel = useCallback(async () => {
|
| 44 |
+
if (!modelId) {
|
| 45 |
+
throw new Error("Model ID is required");
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if (!moduleCache[modelId]) {
|
| 49 |
+
moduleCache[modelId] = {
|
| 50 |
+
instance: null,
|
| 51 |
+
loadingPromise: null,
|
| 52 |
+
};
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const cache = moduleCache[modelId];
|
| 56 |
+
|
| 57 |
+
const existingInstance = instanceRef.current || cache.instance;
|
| 58 |
+
if (existingInstance) {
|
| 59 |
+
instanceRef.current = existingInstance;
|
| 60 |
+
cache.instance = existingInstance;
|
| 61 |
+
setState((prev) => ({ ...prev, isReady: true, isLoading: false }));
|
| 62 |
+
return existingInstance;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const existingPromise = loadingPromiseRef.current || cache.loadingPromise;
|
| 66 |
+
if (existingPromise) {
|
| 67 |
+
try {
|
| 68 |
+
const instance = await existingPromise;
|
| 69 |
+
instanceRef.current = instance;
|
| 70 |
+
cache.instance = instance;
|
| 71 |
+
setState((prev) => ({ ...prev, isReady: true, isLoading: false }));
|
| 72 |
+
return instance;
|
| 73 |
+
} catch (error) {
|
| 74 |
+
setState((prev) => ({
|
| 75 |
+
...prev,
|
| 76 |
+
isLoading: false,
|
| 77 |
+
error:
|
| 78 |
+
error instanceof Error ? error.message : "Failed to load model",
|
| 79 |
+
}));
|
| 80 |
+
throw error;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
setState((prev) => ({
|
| 85 |
+
...prev,
|
| 86 |
+
isLoading: true,
|
| 87 |
+
error: null,
|
| 88 |
+
progress: 0,
|
| 89 |
+
}));
|
| 90 |
+
|
| 91 |
+
abortControllerRef.current = new AbortController();
|
| 92 |
+
|
| 93 |
+
const loadingPromise = (async () => {
|
| 94 |
+
try {
|
| 95 |
+
const progress_callback = (progress: any) => {
|
| 96 |
+
// Only update progress for weights
|
| 97 |
+
if (
|
| 98 |
+
progress.status === "progress" &&
|
| 99 |
+
progress.file.endsWith(".onnx_data")
|
| 100 |
+
) {
|
| 101 |
+
const percentage = Math.round(
|
| 102 |
+
(progress.loaded / progress.total) * 100,
|
| 103 |
+
);
|
| 104 |
+
setState((prev) => ({ ...prev, progress: percentage }));
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const tokenizer = await AutoTokenizer.from_pretrained(modelId, {
|
| 109 |
+
progress_callback,
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
const model = await AutoModelForCausalLM.from_pretrained(modelId, {
|
| 113 |
+
dtype,
|
| 114 |
+
device: "webgpu",
|
| 115 |
+
progress_callback,
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
const instance = { model, tokenizer };
|
| 119 |
+
instanceRef.current = instance;
|
| 120 |
+
cache.instance = instance;
|
| 121 |
+
loadingPromiseRef.current = null;
|
| 122 |
+
cache.loadingPromise = null;
|
| 123 |
+
|
| 124 |
+
setState((prev) => ({
|
| 125 |
+
...prev,
|
| 126 |
+
isLoading: false,
|
| 127 |
+
isReady: true,
|
| 128 |
+
progress: 100,
|
| 129 |
+
}));
|
| 130 |
+
return instance;
|
| 131 |
+
} catch (error) {
|
| 132 |
+
loadingPromiseRef.current = null;
|
| 133 |
+
cache.loadingPromise = null;
|
| 134 |
+
setState((prev) => ({
|
| 135 |
+
...prev,
|
| 136 |
+
isLoading: false,
|
| 137 |
+
error:
|
| 138 |
+
error instanceof Error ? error.message : "Failed to load model",
|
| 139 |
+
}));
|
| 140 |
+
throw error;
|
| 141 |
+
}
|
| 142 |
+
})();
|
| 143 |
+
|
| 144 |
+
loadingPromiseRef.current = loadingPromise;
|
| 145 |
+
cache.loadingPromise = loadingPromise;
|
| 146 |
+
return loadingPromise;
|
| 147 |
+
}, [modelId]);
|
| 148 |
+
|
| 149 |
+
const generateResponse = useCallback(
|
| 150 |
+
async (
|
| 151 |
+
messages: Array<{ role: string; content: string }>,
|
| 152 |
+
tools: Array<any>,
|
| 153 |
+
onToken?: (token: string) => void,
|
| 154 |
+
): Promise<string> => {
|
| 155 |
+
const instance = instanceRef.current;
|
| 156 |
+
if (!instance) {
|
| 157 |
+
throw new Error("Model not loaded. Call loadModel() first.");
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
const { model, tokenizer } = instance;
|
| 161 |
+
|
| 162 |
+
// Apply chat template with tools
|
| 163 |
+
const input = tokenizer.apply_chat_template(messages, {
|
| 164 |
+
tools,
|
| 165 |
+
add_generation_prompt: true,
|
| 166 |
+
return_dict: true,
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
const streamer = onToken
|
| 170 |
+
? new TextStreamer(tokenizer, {
|
| 171 |
+
skip_prompt: true,
|
| 172 |
+
skip_special_tokens: false,
|
| 173 |
+
callback_function: (token: string) => {
|
| 174 |
+
onToken(token);
|
| 175 |
+
},
|
| 176 |
+
})
|
| 177 |
+
: undefined;
|
| 178 |
+
|
| 179 |
+
// Generate the response
|
| 180 |
+
const { sequences, past_key_values } = await model.generate({
|
| 181 |
+
...input,
|
| 182 |
+
past_key_values: pastKeyValuesRef.current,
|
| 183 |
+
max_new_tokens: 512,
|
| 184 |
+
do_sample: false,
|
| 185 |
+
streamer,
|
| 186 |
+
return_dict_in_generate: true,
|
| 187 |
+
});
|
| 188 |
+
pastKeyValuesRef.current = past_key_values;
|
| 189 |
+
|
| 190 |
+
// Decode the generated text with special tokens preserved (except final <|end_of_text|>) for tool call detection
|
| 191 |
+
const response = tokenizer
|
| 192 |
+
.batch_decode(sequences.slice(null, [input.input_ids.dims[1], null]), {
|
| 193 |
+
skip_special_tokens: false,
|
| 194 |
+
})[0]
|
| 195 |
+
.replace(/<\|end_of_text\|>$/, "");
|
| 196 |
+
|
| 197 |
+
return response;
|
| 198 |
+
},
|
| 199 |
+
[],
|
| 200 |
+
);
|
| 201 |
+
|
| 202 |
+
const clearPastKeyValues = useCallback(() => {
|
| 203 |
+
pastKeyValuesRef.current = null;
|
| 204 |
+
}, []);
|
| 205 |
+
|
| 206 |
+
const cleanup = useCallback(() => {
|
| 207 |
+
if (abortControllerRef.current) {
|
| 208 |
+
abortControllerRef.current.abort();
|
| 209 |
+
}
|
| 210 |
+
}, []);
|
| 211 |
+
|
| 212 |
+
useEffect(() => {
|
| 213 |
+
return cleanup;
|
| 214 |
+
}, [cleanup]);
|
| 215 |
+
|
| 216 |
+
useEffect(() => {
|
| 217 |
+
if (modelId && moduleCache[modelId]) {
|
| 218 |
+
const existingInstance =
|
| 219 |
+
instanceRef.current || moduleCache[modelId].instance;
|
| 220 |
+
if (existingInstance) {
|
| 221 |
+
instanceRef.current = existingInstance;
|
| 222 |
+
setState((prev) => ({ ...prev, isReady: true }));
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
}, [modelId]);
|
| 226 |
+
|
| 227 |
+
return {
|
| 228 |
+
...state,
|
| 229 |
+
loadModel,
|
| 230 |
+
generateResponse,
|
| 231 |
+
clearPastKeyValues,
|
| 232 |
+
cleanup,
|
| 233 |
+
};
|
| 234 |
+
};
|
src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from "react";
|
| 2 |
+
import { createRoot } from "react-dom/client";
|
| 3 |
+
import "./index.css";
|
| 4 |
+
import App from "./App.tsx";
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById("root")!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
);
|
src/tools/get_location.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Get the user's current location using the browser's geolocation API.
|
| 3 |
+
* @returns {Promise<{ latitude: number, longitude: number }>} The current position { latitude, longitude }.
|
| 4 |
+
*/
|
| 5 |
+
export async function get_location() {
|
| 6 |
+
return new Promise((resolve, reject) => {
|
| 7 |
+
if (!navigator.geolocation) {
|
| 8 |
+
reject("Geolocation not supported.");
|
| 9 |
+
return;
|
| 10 |
+
}
|
| 11 |
+
navigator.geolocation.getCurrentPosition(
|
| 12 |
+
(pos) =>
|
| 13 |
+
resolve({
|
| 14 |
+
latitude: pos.coords.latitude,
|
| 15 |
+
longitude: pos.coords.longitude,
|
| 16 |
+
}),
|
| 17 |
+
(err) => reject(err.message || "Geolocation error"),
|
| 18 |
+
);
|
| 19 |
+
});
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default (input, output) =>
|
| 23 |
+
React.createElement(
|
| 24 |
+
"div",
|
| 25 |
+
{ className: "bg-green-50 border border-green-200 rounded-lg p-4" },
|
| 26 |
+
React.createElement(
|
| 27 |
+
"div",
|
| 28 |
+
{ className: "flex items-center mb-2" },
|
| 29 |
+
React.createElement(
|
| 30 |
+
"div",
|
| 31 |
+
{
|
| 32 |
+
className:
|
| 33 |
+
"w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3",
|
| 34 |
+
},
|
| 35 |
+
"📍",
|
| 36 |
+
),
|
| 37 |
+
React.createElement(
|
| 38 |
+
"h3",
|
| 39 |
+
{ className: "text-green-900 font-semibold" },
|
| 40 |
+
"Location",
|
| 41 |
+
),
|
| 42 |
+
),
|
| 43 |
+
output?.latitude && output?.longitude
|
| 44 |
+
? React.createElement(
|
| 45 |
+
"div",
|
| 46 |
+
{ className: "space-y-1 text-sm" },
|
| 47 |
+
React.createElement(
|
| 48 |
+
"p",
|
| 49 |
+
{ className: "text-green-700" },
|
| 50 |
+
React.createElement(
|
| 51 |
+
"span",
|
| 52 |
+
{ className: "font-medium" },
|
| 53 |
+
"Latitude: ",
|
| 54 |
+
),
|
| 55 |
+
output.latitude.toFixed(6),
|
| 56 |
+
),
|
| 57 |
+
React.createElement(
|
| 58 |
+
"p",
|
| 59 |
+
{ className: "text-green-700" },
|
| 60 |
+
React.createElement(
|
| 61 |
+
"span",
|
| 62 |
+
{ className: "font-medium" },
|
| 63 |
+
"Longitude: ",
|
| 64 |
+
),
|
| 65 |
+
output.longitude.toFixed(6),
|
| 66 |
+
),
|
| 67 |
+
React.createElement(
|
| 68 |
+
"a",
|
| 69 |
+
{
|
| 70 |
+
href: `https://maps.google.com?q=${output.latitude},${output.longitude}`,
|
| 71 |
+
target: "_blank",
|
| 72 |
+
rel: "noopener noreferrer",
|
| 73 |
+
className:
|
| 74 |
+
"inline-block mt-2 text-green-600 hover:text-green-800 underline text-xs",
|
| 75 |
+
},
|
| 76 |
+
"View on Google Maps",
|
| 77 |
+
),
|
| 78 |
+
)
|
| 79 |
+
: React.createElement(
|
| 80 |
+
"p",
|
| 81 |
+
{ className: "text-green-700 text-sm" },
|
| 82 |
+
JSON.stringify(output),
|
| 83 |
+
),
|
| 84 |
+
);
|
src/tools/get_time.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Get the current date and time.
|
| 3 |
+
* @returns {{ iso: string, local: string }} The current date and time as ISO and local time strings.
|
| 4 |
+
*/
|
| 5 |
+
export function get_time() {
|
| 6 |
+
const now = new Date();
|
| 7 |
+
return {
|
| 8 |
+
iso: now.toISOString(),
|
| 9 |
+
local: now.toLocaleString(undefined, {
|
| 10 |
+
dateStyle: "full",
|
| 11 |
+
timeStyle: "long",
|
| 12 |
+
}),
|
| 13 |
+
};
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default (input, output) =>
|
| 17 |
+
React.createElement(
|
| 18 |
+
"div",
|
| 19 |
+
{ className: "bg-amber-50 border border-amber-200 rounded-lg p-4" },
|
| 20 |
+
React.createElement(
|
| 21 |
+
"div",
|
| 22 |
+
{ className: "flex items-center mb-2" },
|
| 23 |
+
React.createElement(
|
| 24 |
+
"div",
|
| 25 |
+
{
|
| 26 |
+
className:
|
| 27 |
+
"w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center mr-3",
|
| 28 |
+
},
|
| 29 |
+
"🕐",
|
| 30 |
+
),
|
| 31 |
+
React.createElement(
|
| 32 |
+
"h3",
|
| 33 |
+
{ className: "text-amber-900 font-semibold" },
|
| 34 |
+
"Current Time",
|
| 35 |
+
),
|
| 36 |
+
),
|
| 37 |
+
React.createElement(
|
| 38 |
+
"div",
|
| 39 |
+
{ className: "text-sm space-y-1" },
|
| 40 |
+
React.createElement(
|
| 41 |
+
"p",
|
| 42 |
+
{ className: "text-amber-700 font-mono" },
|
| 43 |
+
output.local,
|
| 44 |
+
),
|
| 45 |
+
React.createElement(
|
| 46 |
+
"p",
|
| 47 |
+
{ className: "text-amber-600 text-xs" },
|
| 48 |
+
new Date(output.iso).toLocaleString(),
|
| 49 |
+
),
|
| 50 |
+
),
|
| 51 |
+
);
|
src/tools/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import SPEAK_TOOL from "./speak.js?raw";
|
| 2 |
+
import GET_LOCATION_TOOL from "./get_location.js?raw";
|
| 3 |
+
import SLEEP_TOOL from "./sleep.js?raw";
|
| 4 |
+
import GET_TIME_TOOL from "./get_time.js?raw";
|
| 5 |
+
import RANDOM_NUMBER_TOOL from "./random_number.js?raw";
|
| 6 |
+
import MATH_EVAL_TOOL from "./math_eval.js?raw";
|
| 7 |
+
import TEMPLATE_TOOL from "./template.js?raw";
|
| 8 |
+
import OPEN_WEBPAGE_TOOL from "./open_webpage.js?raw";
|
| 9 |
+
|
| 10 |
+
export const DEFAULT_TOOLS = {
|
| 11 |
+
speak: SPEAK_TOOL,
|
| 12 |
+
get_location: GET_LOCATION_TOOL,
|
| 13 |
+
sleep: SLEEP_TOOL,
|
| 14 |
+
get_time: GET_TIME_TOOL,
|
| 15 |
+
random_number: RANDOM_NUMBER_TOOL,
|
| 16 |
+
math_eval: MATH_EVAL_TOOL,
|
| 17 |
+
open_webpage: OPEN_WEBPAGE_TOOL,
|
| 18 |
+
};
|
| 19 |
+
export const TEMPLATE = TEMPLATE_TOOL;
|
src/tools/math_eval.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Evaluate a math expression.
|
| 3 |
+
* @param {string} expression - The math expression (e.g., "2 + 2 * (3 - 1)").
|
| 4 |
+
* @returns {number} The result of the expression.
|
| 5 |
+
*/
|
| 6 |
+
export function math_eval(expression) {
|
| 7 |
+
// Only allow numbers, spaces, and math symbols: + - * / % ( ) .
|
| 8 |
+
if (!/^[\d\s+\-*/%.()]+$/.test(expression)) {
|
| 9 |
+
throw new Error("Invalid characters in expression.");
|
| 10 |
+
}
|
| 11 |
+
return Function('"use strict";return (' + expression + ")")();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default (input, output) =>
|
| 15 |
+
React.createElement(
|
| 16 |
+
"div",
|
| 17 |
+
{ className: "bg-emerald-50 border border-emerald-200 rounded-lg p-4" },
|
| 18 |
+
React.createElement(
|
| 19 |
+
"div",
|
| 20 |
+
{ className: "flex items-center mb-2" },
|
| 21 |
+
React.createElement(
|
| 22 |
+
"div",
|
| 23 |
+
{
|
| 24 |
+
className:
|
| 25 |
+
"w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center mr-3",
|
| 26 |
+
},
|
| 27 |
+
"🧮",
|
| 28 |
+
),
|
| 29 |
+
React.createElement(
|
| 30 |
+
"h3",
|
| 31 |
+
{ className: "text-emerald-900 font-semibold" },
|
| 32 |
+
"Math Evaluation",
|
| 33 |
+
),
|
| 34 |
+
),
|
| 35 |
+
React.createElement(
|
| 36 |
+
"div",
|
| 37 |
+
{ className: "text-center" },
|
| 38 |
+
React.createElement(
|
| 39 |
+
"div",
|
| 40 |
+
{ className: "text-lg font-mono text-emerald-700 mb-1" },
|
| 41 |
+
input.expression || "Unknown expression",
|
| 42 |
+
),
|
| 43 |
+
React.createElement(
|
| 44 |
+
"div",
|
| 45 |
+
{ className: "text-2xl font-bold text-emerald-600 mb-1" },
|
| 46 |
+
`= ${output}`,
|
| 47 |
+
),
|
| 48 |
+
React.createElement(
|
| 49 |
+
"p",
|
| 50 |
+
{ className: "text-emerald-500 text-xs" },
|
| 51 |
+
"Calculation result",
|
| 52 |
+
),
|
| 53 |
+
),
|
| 54 |
+
);
|
src/tools/open_webpage.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Open a webpage
|
| 3 |
+
* @param {string} src - The URL of the webpage.
|
| 4 |
+
* @returns {string} The validated URL.
|
| 5 |
+
*/
|
| 6 |
+
export function open_webpage(src) {
|
| 7 |
+
try {
|
| 8 |
+
const urlObj = new URL(src);
|
| 9 |
+
if (!["http:", "https:"].includes(urlObj.protocol)) {
|
| 10 |
+
throw new Error("Only HTTP and HTTPS URLs are allowed.");
|
| 11 |
+
}
|
| 12 |
+
return urlObj.href;
|
| 13 |
+
} catch (error) {
|
| 14 |
+
throw new Error("Invalid URL provided.");
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export default (input, output) => {
|
| 19 |
+
return React.createElement(
|
| 20 |
+
"div",
|
| 21 |
+
{ className: "bg-blue-50 border border-blue-200 rounded-lg p-4" },
|
| 22 |
+
React.createElement(
|
| 23 |
+
"div",
|
| 24 |
+
{ className: "flex items-center mb-2" },
|
| 25 |
+
React.createElement(
|
| 26 |
+
"div",
|
| 27 |
+
{
|
| 28 |
+
className:
|
| 29 |
+
"w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3",
|
| 30 |
+
},
|
| 31 |
+
"🌐",
|
| 32 |
+
),
|
| 33 |
+
React.createElement(
|
| 34 |
+
"h3",
|
| 35 |
+
{ className: "text-blue-900 font-semibold" },
|
| 36 |
+
"Web Page",
|
| 37 |
+
),
|
| 38 |
+
),
|
| 39 |
+
React.createElement("iframe", {
|
| 40 |
+
src: output,
|
| 41 |
+
className: "w-full border border-blue-300 rounded",
|
| 42 |
+
width: 480,
|
| 43 |
+
height: 360,
|
| 44 |
+
title: "Embedded content",
|
| 45 |
+
allow: "autoplay",
|
| 46 |
+
frameBorder: "0",
|
| 47 |
+
}),
|
| 48 |
+
);
|
| 49 |
+
};
|
src/tools/random_number.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generate a random integer between min and max (inclusive).
|
| 3 |
+
* @param {number} min - Minimum value (inclusive).
|
| 4 |
+
* @param {number} max - Maximum value (inclusive).
|
| 5 |
+
* @returns {number} A random integer.
|
| 6 |
+
*/
|
| 7 |
+
export function random_number(min, max) {
|
| 8 |
+
min = Math.ceil(Number(min));
|
| 9 |
+
max = Math.floor(Number(max));
|
| 10 |
+
if (isNaN(min) || isNaN(max) || min > max) {
|
| 11 |
+
throw new Error("Invalid min or max value.");
|
| 12 |
+
}
|
| 13 |
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default (input, output) =>
|
| 17 |
+
React.createElement(
|
| 18 |
+
"div",
|
| 19 |
+
{ className: "bg-indigo-50 border border-indigo-200 rounded-lg p-4" },
|
| 20 |
+
React.createElement(
|
| 21 |
+
"div",
|
| 22 |
+
{ className: "flex items-center mb-2" },
|
| 23 |
+
React.createElement(
|
| 24 |
+
"div",
|
| 25 |
+
{
|
| 26 |
+
className:
|
| 27 |
+
"w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center mr-3",
|
| 28 |
+
},
|
| 29 |
+
"🎲",
|
| 30 |
+
),
|
| 31 |
+
React.createElement(
|
| 32 |
+
"h3",
|
| 33 |
+
{ className: "text-indigo-900 font-semibold" },
|
| 34 |
+
"Random Number",
|
| 35 |
+
),
|
| 36 |
+
),
|
| 37 |
+
React.createElement(
|
| 38 |
+
"div",
|
| 39 |
+
{ className: "text-center" },
|
| 40 |
+
React.createElement(
|
| 41 |
+
"div",
|
| 42 |
+
{ className: "text-3xl font-bold text-indigo-600 mb-1" },
|
| 43 |
+
output,
|
| 44 |
+
),
|
| 45 |
+
React.createElement(
|
| 46 |
+
"p",
|
| 47 |
+
{ className: "text-indigo-500 text-xs" },
|
| 48 |
+
`Range: ${input.min || "?"} - ${input.max || "?"}`,
|
| 49 |
+
),
|
| 50 |
+
),
|
| 51 |
+
);
|
src/tools/sleep.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Sleep for a given number of seconds.
|
| 3 |
+
* @param {number} seconds - The number of seconds to sleep.
|
| 4 |
+
* @return {void}
|
| 5 |
+
*/
|
| 6 |
+
export async function sleep(seconds) {
|
| 7 |
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default (input, output) =>
|
| 11 |
+
React.createElement(
|
| 12 |
+
"div",
|
| 13 |
+
{ className: "bg-purple-50 border border-purple-200 rounded-lg p-4" },
|
| 14 |
+
React.createElement(
|
| 15 |
+
"div",
|
| 16 |
+
{ className: "flex items-center mb-2" },
|
| 17 |
+
React.createElement(
|
| 18 |
+
"div",
|
| 19 |
+
{
|
| 20 |
+
className:
|
| 21 |
+
"w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mr-3",
|
| 22 |
+
},
|
| 23 |
+
"😴",
|
| 24 |
+
),
|
| 25 |
+
React.createElement(
|
| 26 |
+
"h3",
|
| 27 |
+
{ className: "text-purple-900 font-semibold" },
|
| 28 |
+
"Sleep",
|
| 29 |
+
),
|
| 30 |
+
),
|
| 31 |
+
React.createElement(
|
| 32 |
+
"div",
|
| 33 |
+
{ className: "text-sm space-y-1" },
|
| 34 |
+
React.createElement(
|
| 35 |
+
"p",
|
| 36 |
+
{ className: "text-purple-700 font-medium" },
|
| 37 |
+
`Slept for ${input.seconds || "unknown"} seconds`,
|
| 38 |
+
),
|
| 39 |
+
React.createElement(
|
| 40 |
+
"p",
|
| 41 |
+
{ className: "text-purple-600 text-xs" },
|
| 42 |
+
output,
|
| 43 |
+
),
|
| 44 |
+
),
|
| 45 |
+
);
|
src/tools/speak.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Speak text using the browser's speech synthesis API.
|
| 3 |
+
* @param {string} text - The text to speak.
|
| 4 |
+
* @param {string} [voice] - The name of the voice to use (optional).
|
| 5 |
+
* @return {boolean} - Whether the tool was executed successfully.
|
| 6 |
+
*/
|
| 7 |
+
export function speak(text, voice = undefined) {
|
| 8 |
+
const utter = new window.SpeechSynthesisUtterance(text);
|
| 9 |
+
if (voice) {
|
| 10 |
+
const voices = window.speechSynthesis.getVoices();
|
| 11 |
+
const match = voices.find((v) => v.name === voice);
|
| 12 |
+
if (match) utter.voice = match;
|
| 13 |
+
}
|
| 14 |
+
window.speechSynthesis.speak(utter);
|
| 15 |
+
return true;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export default (input, output) =>
|
| 19 |
+
React.createElement(
|
| 20 |
+
"div",
|
| 21 |
+
{ className: "bg-blue-50 border border-blue-200 rounded-lg p-4" },
|
| 22 |
+
React.createElement(
|
| 23 |
+
"div",
|
| 24 |
+
{ className: "flex items-center mb-2" },
|
| 25 |
+
React.createElement(
|
| 26 |
+
"div",
|
| 27 |
+
{
|
| 28 |
+
className:
|
| 29 |
+
"w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3",
|
| 30 |
+
},
|
| 31 |
+
"🔊",
|
| 32 |
+
),
|
| 33 |
+
React.createElement(
|
| 34 |
+
"h3",
|
| 35 |
+
{ className: "text-blue-900 font-semibold" },
|
| 36 |
+
"Speech Synthesis",
|
| 37 |
+
),
|
| 38 |
+
),
|
| 39 |
+
React.createElement(
|
| 40 |
+
"div",
|
| 41 |
+
{ className: "text-sm space-y-1" },
|
| 42 |
+
React.createElement(
|
| 43 |
+
"p",
|
| 44 |
+
{ className: "text-blue-700 font-medium" },
|
| 45 |
+
`Speaking: "${input.text || "Unknown text"}"`,
|
| 46 |
+
),
|
| 47 |
+
input.voice &&
|
| 48 |
+
React.createElement(
|
| 49 |
+
"p",
|
| 50 |
+
{ className: "text-blue-600 text-xs" },
|
| 51 |
+
`Voice: ${input.voice}`,
|
| 52 |
+
),
|
| 53 |
+
React.createElement(
|
| 54 |
+
"p",
|
| 55 |
+
{ className: "text-blue-600 text-xs" },
|
| 56 |
+
typeof output === "string" ? output : "Speech completed successfully",
|
| 57 |
+
),
|
| 58 |
+
),
|
| 59 |
+
);
|
src/tools/template.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Description of the tool.
|
| 3 |
+
* @param {any} parameter1 - Description of the first parameter.
|
| 4 |
+
* @param {any} parameter2 - Description of the second parameter.
|
| 5 |
+
* @returns {any} Description of the return value.
|
| 6 |
+
*/
|
| 7 |
+
export function new_tool(parameter1, parameter2) {
|
| 8 |
+
// TODO: Implement the tool logic here
|
| 9 |
+
return true; // Placeholder return value
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default (input, output) =>
|
| 13 |
+
React.createElement(
|
| 14 |
+
"div",
|
| 15 |
+
{ className: "bg-amber-50 border border-amber-200 rounded-lg p-4" },
|
| 16 |
+
React.createElement(
|
| 17 |
+
"div",
|
| 18 |
+
{ className: "flex items-center mb-2" },
|
| 19 |
+
React.createElement(
|
| 20 |
+
"div",
|
| 21 |
+
{
|
| 22 |
+
className:
|
| 23 |
+
"w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center mr-3",
|
| 24 |
+
},
|
| 25 |
+
"🛠️",
|
| 26 |
+
),
|
| 27 |
+
React.createElement(
|
| 28 |
+
"h3",
|
| 29 |
+
{ className: "text-amber-900 font-semibold" },
|
| 30 |
+
"Tool Name",
|
| 31 |
+
),
|
| 32 |
+
),
|
| 33 |
+
React.createElement(
|
| 34 |
+
"div",
|
| 35 |
+
{ className: "text-sm space-y-1" },
|
| 36 |
+
React.createElement(
|
| 37 |
+
"p",
|
| 38 |
+
{ className: "text-amber-700 font-medium" },
|
| 39 |
+
`Input: ${JSON.stringify(input)}`,
|
| 40 |
+
),
|
| 41 |
+
React.createElement(
|
| 42 |
+
"p",
|
| 43 |
+
{ className: "text-amber-600 text-xs" },
|
| 44 |
+
`Output: ${output}`,
|
| 45 |
+
),
|
| 46 |
+
),
|
| 47 |
+
);
|
src/utils.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface Schema {
|
| 2 |
+
name: string;
|
| 3 |
+
description: string;
|
| 4 |
+
parameters: {
|
| 5 |
+
type: string;
|
| 6 |
+
properties: Record<
|
| 7 |
+
string,
|
| 8 |
+
{
|
| 9 |
+
type: string;
|
| 10 |
+
description: string;
|
| 11 |
+
default?: any;
|
| 12 |
+
}
|
| 13 |
+
>;
|
| 14 |
+
required: string[];
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface JSDocParam {
|
| 19 |
+
type: string;
|
| 20 |
+
description: string;
|
| 21 |
+
isOptional: boolean;
|
| 22 |
+
defaultValue?: string;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export const extractFunctionAndRenderer = (
|
| 26 |
+
code: string,
|
| 27 |
+
): { functionCode: string; rendererCode?: string } => {
|
| 28 |
+
if (typeof code !== "string") {
|
| 29 |
+
return { functionCode: code };
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const exportMatch = code.match(/export\s+default\s+/);
|
| 33 |
+
if (!exportMatch) {
|
| 34 |
+
return { functionCode: code };
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const exportIndex = exportMatch.index!;
|
| 38 |
+
const functionCode = code.substring(0, exportIndex).trim();
|
| 39 |
+
const rendererCode = code.substring(exportIndex).trim();
|
| 40 |
+
|
| 41 |
+
return { functionCode, rendererCode };
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Helper function to extract JSDoc parameters from JSDoc comments.
|
| 46 |
+
*/
|
| 47 |
+
const extractJSDocParams = (
|
| 48 |
+
jsdoc: string,
|
| 49 |
+
): Record<string, JSDocParam & { jsdocDefault?: string }> => {
|
| 50 |
+
const jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> =
|
| 51 |
+
{};
|
| 52 |
+
const lines = jsdoc
|
| 53 |
+
.split("\n")
|
| 54 |
+
.map((line) => line.trim().replace(/^\*\s?/, ""));
|
| 55 |
+
const paramRegex =
|
| 56 |
+
/@param\s+\{([^}]+)\}\s+(\[?[a-zA-Z0-9_]+(?:=[^\]]+)?\]?|\S+)\s*-?\s*(.*)?/;
|
| 57 |
+
|
| 58 |
+
for (const line of lines) {
|
| 59 |
+
const paramMatch = line.match(paramRegex);
|
| 60 |
+
if (paramMatch) {
|
| 61 |
+
let [, type, namePart, description] = paramMatch;
|
| 62 |
+
description = description || "";
|
| 63 |
+
let isOptional = false;
|
| 64 |
+
let name = namePart;
|
| 65 |
+
let jsdocDefault: string | undefined = undefined;
|
| 66 |
+
|
| 67 |
+
if (name.startsWith("[") && name.endsWith("]")) {
|
| 68 |
+
isOptional = true;
|
| 69 |
+
name = name.slice(1, -1);
|
| 70 |
+
}
|
| 71 |
+
if (name.includes("=")) {
|
| 72 |
+
const [n, def] = name.split("=");
|
| 73 |
+
name = n.trim();
|
| 74 |
+
jsdocDefault = def.trim().replace(/['"]/g, "");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
jsdocParams[name] = {
|
| 78 |
+
type: type.toLowerCase(),
|
| 79 |
+
description: description.trim(),
|
| 80 |
+
isOptional,
|
| 81 |
+
defaultValue: undefined,
|
| 82 |
+
jsdocDefault,
|
| 83 |
+
};
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
return jsdocParams;
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Helper function to extract function signature information.
|
| 91 |
+
*/
|
| 92 |
+
const extractFunctionSignature = (
|
| 93 |
+
functionCode: string,
|
| 94 |
+
): {
|
| 95 |
+
name: string;
|
| 96 |
+
params: { name: string; defaultValue?: string }[];
|
| 97 |
+
} | null => {
|
| 98 |
+
const functionSignatureMatch = functionCode.match(
|
| 99 |
+
/function\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/,
|
| 100 |
+
);
|
| 101 |
+
if (!functionSignatureMatch) {
|
| 102 |
+
return null;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
const functionName = functionSignatureMatch[1];
|
| 106 |
+
const params = functionSignatureMatch[2]
|
| 107 |
+
.split(",")
|
| 108 |
+
.map((p) => p.trim())
|
| 109 |
+
.filter(Boolean)
|
| 110 |
+
.map((p) => {
|
| 111 |
+
const [name, defaultValue] = p.split("=").map((s) => s.trim());
|
| 112 |
+
return { name, defaultValue };
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
return { name: functionName, params };
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
export const generateSchemaFromCode = (code: string): Schema => {
|
| 119 |
+
const { functionCode } = extractFunctionAndRenderer(code);
|
| 120 |
+
|
| 121 |
+
if (typeof functionCode !== "string") {
|
| 122 |
+
return {
|
| 123 |
+
name: "invalid_code",
|
| 124 |
+
description: "Code is not a valid string.",
|
| 125 |
+
parameters: { type: "object", properties: {}, required: [] },
|
| 126 |
+
};
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// 1. Extract function signature, name, and parameter names directly from the code
|
| 130 |
+
const signatureInfo = extractFunctionSignature(functionCode);
|
| 131 |
+
if (!signatureInfo) {
|
| 132 |
+
return {
|
| 133 |
+
name: "invalid_function",
|
| 134 |
+
description: "Could not parse function signature.",
|
| 135 |
+
parameters: { type: "object", properties: {}, required: [] },
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const { name: functionName, params: paramsFromSignature } = signatureInfo;
|
| 140 |
+
|
| 141 |
+
const schema: Schema = {
|
| 142 |
+
name: functionName,
|
| 143 |
+
description: "",
|
| 144 |
+
parameters: {
|
| 145 |
+
type: "object",
|
| 146 |
+
properties: {},
|
| 147 |
+
required: [],
|
| 148 |
+
},
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
// 2. Parse JSDoc comments to get descriptions and types
|
| 152 |
+
const jsdocMatch = functionCode.match(/\/\*\*([\s\S]*?)\*\//);
|
| 153 |
+
let jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> = {};
|
| 154 |
+
if (jsdocMatch) {
|
| 155 |
+
const jsdoc = jsdocMatch[1];
|
| 156 |
+
jsdocParams = extractJSDocParams(jsdoc);
|
| 157 |
+
|
| 158 |
+
const descriptionLines = jsdoc
|
| 159 |
+
.split("\n")
|
| 160 |
+
.map((line) => line.trim().replace(/^\*\s?/, ""))
|
| 161 |
+
.filter((line) => !line.startsWith("@") && line);
|
| 162 |
+
|
| 163 |
+
schema.description = descriptionLines.join(" ").trim();
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// 3. Combine signature parameters with JSDoc info
|
| 167 |
+
for (const param of paramsFromSignature) {
|
| 168 |
+
const paramName = param.name;
|
| 169 |
+
const jsdocInfo = jsdocParams[paramName];
|
| 170 |
+
schema.parameters.properties[paramName] = {
|
| 171 |
+
type: jsdocInfo ? jsdocInfo.type : "any",
|
| 172 |
+
description: jsdocInfo ? jsdocInfo.description : "",
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
// Prefer default from signature, then from JSDoc
|
| 176 |
+
if (param.defaultValue !== undefined) {
|
| 177 |
+
// Try to parse as JSON, fallback to string
|
| 178 |
+
try {
|
| 179 |
+
schema.parameters.properties[paramName].default = JSON.parse(
|
| 180 |
+
param.defaultValue.replace(/'/g, '"'),
|
| 181 |
+
);
|
| 182 |
+
} catch {
|
| 183 |
+
schema.parameters.properties[paramName].default = param.defaultValue;
|
| 184 |
+
}
|
| 185 |
+
} else if (jsdocInfo && jsdocInfo.jsdocDefault !== undefined) {
|
| 186 |
+
schema.parameters.properties[paramName].default = jsdocInfo.jsdocDefault;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// A parameter is required if:
|
| 190 |
+
// - Not optional in JSDoc
|
| 191 |
+
// - No default in signature
|
| 192 |
+
// - No default in JSDoc
|
| 193 |
+
const hasDefault =
|
| 194 |
+
param.defaultValue !== undefined ||
|
| 195 |
+
(jsdocInfo && jsdocInfo.jsdocDefault !== undefined);
|
| 196 |
+
if (!jsdocInfo || (!jsdocInfo.isOptional && !hasDefault)) {
|
| 197 |
+
schema.parameters.required.push(paramName);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
return schema;
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
/**
|
| 205 |
+
* Extracts tool call content from a string using the tool call markers.
|
| 206 |
+
*/
|
| 207 |
+
export const extractToolCallContent = (content: string): string[] | null => {
|
| 208 |
+
if (typeof content !== "string") return null;
|
| 209 |
+
const matches = [...content.matchAll(/<tool_call>([\s\S]*?)<\/tool_call>/g)];
|
| 210 |
+
return matches.length ? matches.map(([, inner]) => inner.trim()) : null;
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
export const getErrorMessage = (error: unknown): string => {
|
| 214 |
+
if (error instanceof Error) {
|
| 215 |
+
return error.message;
|
| 216 |
+
}
|
| 217 |
+
if (typeof error === "string") {
|
| 218 |
+
return error;
|
| 219 |
+
}
|
| 220 |
+
if (error && typeof error === "object") {
|
| 221 |
+
return JSON.stringify(error);
|
| 222 |
+
}
|
| 223 |
+
return String(error);
|
| 224 |
+
};
|
| 225 |
+
|
| 226 |
+
/**
|
| 227 |
+
* Adapted from https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser.
|
| 228 |
+
*/
|
| 229 |
+
export function isMobileOrTablet() {
|
| 230 |
+
let check = false;
|
| 231 |
+
(function (a: string) {
|
| 232 |
+
if (
|
| 233 |
+
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
|
| 234 |
+
a,
|
| 235 |
+
) ||
|
| 236 |
+
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
| 237 |
+
a.slice(0, 4),
|
| 238 |
+
)
|
| 239 |
+
)
|
| 240 |
+
check = true;
|
| 241 |
+
})(
|
| 242 |
+
navigator.userAgent ||
|
| 243 |
+
navigator.vendor ||
|
| 244 |
+
("opera" in window && typeof window.opera === "string"
|
| 245 |
+
? window.opera
|
| 246 |
+
: ""),
|
| 247 |
+
);
|
| 248 |
+
return check;
|
| 249 |
+
}
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
tsconfig.app.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
"jsx": "react-jsx",
|
| 17 |
+
|
| 18 |
+
/* Linting */
|
| 19 |
+
"strict": true,
|
| 20 |
+
"noUnusedLocals": true,
|
| 21 |
+
"noUnusedParameters": true,
|
| 22 |
+
"erasableSyntaxOnly": true,
|
| 23 |
+
"noFallthroughCasesInSwitch": true,
|
| 24 |
+
"noUncheckedSideEffectImports": true
|
| 25 |
+
},
|
| 26 |
+
"include": ["src"]
|
| 27 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
tsconfig.node.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
|
| 9 |
+
/* Bundler mode */
|
| 10 |
+
"moduleResolution": "bundler",
|
| 11 |
+
"allowImportingTsExtensions": true,
|
| 12 |
+
"verbatimModuleSyntax": true,
|
| 13 |
+
"moduleDetection": "force",
|
| 14 |
+
"noEmit": true,
|
| 15 |
+
|
| 16 |
+
/* Linting */
|
| 17 |
+
"strict": true,
|
| 18 |
+
"noUnusedLocals": true,
|
| 19 |
+
"noUnusedParameters": true,
|
| 20 |
+
"erasableSyntaxOnly": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true,
|
| 22 |
+
"noUncheckedSideEffectImports": true
|
| 23 |
+
},
|
| 24 |
+
"include": ["vite.config.ts"]
|
| 25 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import react from "@vitejs/plugin-react";
|
| 3 |
+
import tailwindcss from "@tailwindcss/vite";
|
| 4 |
+
|
| 5 |
+
// https://vite.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react(), tailwindcss()],
|
| 8 |
+
});
|