Bladeren bron

stash code

lloydzhou 1 jaar geleden
bovenliggende
commit
271f58d9cf
7 gewijzigde bestanden met toevoegingen van 246 en 194 verwijderingen
  1. 4 37
      app/client/platforms/openai.ts
  2. 4 70
      app/components/chat.tsx
  3. 15 0
      app/components/plugin.module.scss
  4. 113 16
      app/components/plugin.tsx
  5. 24 4
      app/locales/cn.ts
  6. 5 25
      app/locales/en.ts
  7. 81 42
      app/store/plugin.ts

+ 4 - 37
app/client/platforms/openai.ts

@@ -241,49 +241,16 @@ export class ChatGPTApi implements LLMApi {
         );
       }
       if (shouldStream) {
-        const [tools1, funcs2] = usePluginStore
+        const [tools, funcs] = usePluginStore
           .getState()
           .getAsTools(useChatStore.getState().currentSession().mask?.plugin);
-        console.log("getAsTools", tools1, funcs2);
-        // return
-        // TODO mock tools and funcs
-        const tools = [
-          {
-            type: "function",
-            function: {
-              name: "get_current_weather",
-              description: "Get the current weather",
-              parameters: {
-                type: "object",
-                properties: {
-                  location: {
-                    type: "string",
-                    description: "The city and country, eg. San Francisco, USA",
-                  },
-                  format: {
-                    type: "string",
-                    enum: ["celsius", "fahrenheit"],
-                  },
-                },
-                required: ["location", "format"],
-              },
-            },
-          },
-        ];
-        const funcs = {
-          get_current_weather: (args: any) => {
-            console.log("call get_current_weather", args);
-            return new Promise((resolve) => {
-              setTimeout(() => resolve("30"), 3000);
-            });
-          },
-        };
+        console.log("getAsTools", tools, funcs);
         stream(
           chatPath,
           requestPayload,
           getHeaders(),
-          tools1,
-          funcs2,
+          tools,
+          funcs,
           controller,
           // parseSSE
           (text: string, runTools: ChatMessageTool[]) => {

+ 4 - 70
app/components/chat.tsx

@@ -442,70 +442,6 @@ export function ChatActions(props: {
   const navigate = useNavigate();
   const chatStore = useChatStore();
   const pluginStore = usePluginStore();
-  console.log("pluginStore", pluginStore.getAll());
-  // test
-  if (pluginStore.getAll().length == 0) {
-    pluginStore.create({
-      title: "Pet API",
-      version: "1.0.0",
-      content: `{
-  "openapi": "3.0.2",
-  "info": {
-    "title": "Pet API",
-    "version": "1.0.0"
-  },
-  "paths": {
-    "/api/pets": {
-      "get": {
-        "operationId": "getPets",
-        "description": "Returns all pets from the system that the user has access to",
-        "responses": {
-          "200": {
-            "description": "List of Pets",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "type": "array",
-                  "items": {
-                    "$ref": "#/components/schemas/Pet"
-                  }
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  },
-  "components": {
-    "schemas": {
-      "Pet": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "type": "string"
-          },
-          "type": {
-            "type": "string",
-            "enum": [
-              "cat",
-              "dog"
-            ]
-          },
-          "name": {
-            "type": "string"
-          }
-        },
-        "required": [
-          "id",
-          "type"
-        ]
-      }
-    }
-  }
-}`,
-    });
-  }
 
   // switch themes
   const theme = config.theme;
@@ -805,12 +741,10 @@ export function ChatActions(props: {
               value: Plugin.Artifacts,
             },
           ].concat(
-            pluginStore
-              .getAll()
-              .map((item) => ({
-                title: `${item.title}@${item.version}`,
-                value: item.id,
-              })),
+            pluginStore.getAll().map((item) => ({
+              title: `${item.title}@${item.version}`,
+              value: item.id,
+            })),
           )}
           onClose={() => setShowPluginSelector(false)}
           onSelection={(s) => {

+ 15 - 0
app/components/plugin.module.scss

@@ -0,0 +1,15 @@
+.plugin-title {
+  font-weight: bolder;
+  font-size: 16px;
+  margin: 10px 0;
+}
+.plugin-content {
+  font-size: 14px;
+  font-family: inherit;
+  pre code {
+    max-height: 240px;
+    overflow-y: auto;
+    white-space: pre-wrap;
+  }
+}
+

+ 113 - 16
app/components/plugin.tsx

@@ -1,7 +1,11 @@
+import { useDebouncedCallback } from "use-debounce";
+import OpenAPIClientAxios from "openapi-client-axios";
+import yaml from "js-yaml";
 import { IconButton } from "./button";
 import { ErrorBoundary } from "./error";
 
 import styles from "./mask.module.scss";
+import pluginStyles from "./plugin.module.scss";
 
 import DownloadIcon from "../icons/download.svg";
 import EditIcon from "../icons/edit.svg";
@@ -11,7 +15,7 @@ import DeleteIcon from "../icons/delete.svg";
 import EyeIcon from "../icons/eye.svg";
 import CopyIcon from "../icons/copy.svg";
 
-import { Plugin, usePluginStore } from "../store/plugin";
+import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 import {
   Input,
   List,
@@ -20,7 +24,9 @@ import {
   Popover,
   Select,
   showConfirm,
+  showToast,
 } from "./ui-lib";
+import { downloadAs } from "../utils";
 import Locale from "../locales";
 import { useNavigate } from "react-router-dom";
 import { useEffect, useState } from "react";
@@ -30,12 +36,56 @@ import { nanoid } from "nanoid";
 export function PluginPage() {
   const navigate = useNavigate();
   const pluginStore = usePluginStore();
-  const plugins = pluginStore.getAll();
+
+  const allPlugins = pluginStore.getAll();
+  const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
+  const [searchText, setSearchText] = useState("");
+  const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
+
+  // refactored already, now it accurate
+  const onSearch = (text: string) => {
+    setSearchText(text);
+    if (text.length > 0) {
+      const result = allPlugins.filter((m) =>
+        m.title.toLowerCase().includes(text.toLowerCase()),
+      );
+      setSearchPlugins(result);
+    } else {
+      setSearchPlugins(allPlugins);
+    }
+  };
 
   const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
   const editingPlugin = pluginStore.get(editingPluginId);
+  const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
   const closePluginModal = () => setEditingPluginId(undefined);
 
+  const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
+    const content = e.target.innerText;
+    try {
+      const api = new OpenAPIClientAxios({ definition: yaml.load(content) });
+      api
+        .init()
+        .then(() => {
+          if (content != editingPlugin.content) {
+            pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+              plugin.content = content;
+              const tool = FunctionToolService.add(plugin, true);
+              plugin.title = tool.api.definition.info.title;
+              plugin.version = tool.api.definition.info.version;
+            });
+          }
+        })
+        .catch((e) => {
+          console.error(e);
+          showToast(Locale.Plugin.EditModal.Error);
+        });
+    } catch (e) {
+      console.error(e);
+      showToast(Locale.Plugin.EditModal.Error);
+    }
+  }, 100).bind(null, editingPlugin);
+
   return (
     <ErrorBoundary>
       <div className={styles["mask-page"]}>
@@ -61,6 +111,27 @@ export function PluginPage() {
         </div>
 
         <div className={styles["mask-page-body"]}>
+          <div className={styles["mask-filter"]}>
+            <input
+              type="text"
+              className={styles["search-bar"]}
+              placeholder={Locale.Plugin.Page.Search}
+              autoFocus
+              onInput={(e) => onSearch(e.currentTarget.value)}
+            />
+
+            <IconButton
+              className={styles["mask-create"]}
+              icon={<AddIcon />}
+              text={Locale.Plugin.Page.Create}
+              bordered
+              onClick={() => {
+                const createdPlugin = pluginStore.create();
+                setEditingPluginId(createdPlugin.id);
+              }}
+            />
+          </div>
+
           <div>
             {plugins.map((m) => (
               <div className={styles["mask-item"]} key={m.id}>
@@ -71,7 +142,9 @@ export function PluginPage() {
                       {m.title}@<small>{m.version}</small>
                     </div>
                     <div className={styles["mask-info"] + " one-line"}>
-                      {`${Locale.Plugin.Item.Info(m.content.length)} / / `}
+                      {Locale.Plugin.Item.Info(
+                        FunctionToolService.add(m).length,
+                      )}
                     </div>
                   </div>
                 </div>
@@ -123,24 +196,48 @@ export function PluginPage() {
                 onClick={() =>
                   downloadAs(
                     JSON.stringify(editingPlugin),
-                    `${editingPlugin.name}.json`,
+                    `${editingPlugin.title}@${editingPlugin.version}.json`,
                   )
                 }
               />,
-              <IconButton
-                key="copy"
-                icon={<CopyIcon />}
-                bordered
-                text={Locale.Plugin.EditModal.Clone}
-                onClick={() => {
-                  navigate(Path.Plugins);
-                  pluginStore.create(editingPlugin);
-                  setEditingPluginId(undefined);
-                }}
-              />,
             ]}
           >
-            PluginConfig
+            <div className={styles["mask-page"]}>
+              <div className={pluginStyles["plugin-title"]}>
+                {Locale.Plugin.EditModal.Content}
+              </div>
+              <div
+                className={`markdown-body ${pluginStyles["plugin-content"]}`}
+                dir="auto"
+              >
+                <pre>
+                  <code
+                    contentEditable={true}
+                    dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
+                    onBlur={onChangePlugin}
+                  ></code>
+                </pre>
+              </div>
+              <div className={pluginStyles["plugin-title"]}>
+                {Locale.Plugin.EditModal.Method}
+              </div>
+              <div className={styles["mask-page-body"]} style={{ padding: 0 }}>
+                {editingPluginTool?.tools.map((tool, index) => (
+                  <div className={styles["mask-item"]} key={index}>
+                    <div className={styles["mask-header"]}>
+                      <div className={styles["mask-title"]}>
+                        <div className={styles["mask-name"]}>
+                          {tool?.function?.name}
+                        </div>
+                        <div className={styles["mask-info"] + " one-line"}>
+                          {tool?.function?.description}
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </div>
           </Modal>
         </div>
       )}

+ 24 - 4
app/locales/cn.ts

@@ -509,10 +509,6 @@ const cn = {
     Clear: "上下文已清除",
     Revert: "恢复上下文",
   },
-  Plugin: {
-    Name: "插件",
-    Artifacts: "Artifacts",
-  },
   Discovery: {
     Name: "发现",
   },
@@ -534,6 +530,30 @@ const cn = {
       View: "查看",
     },
   },
+  Plugin: {
+    Name: "插件",
+    Artifacts: "Artifacts",
+    Page: {
+      Title: "插件",
+      SubTitle: (count: number) => `${count} 个插件`,
+      Search: "搜索插件",
+      Create: "新建",
+    },
+    Item: {
+      Info: (count: number) => `${count} 方法`,
+      View: "查看",
+      Edit: "编辑",
+      Delete: "删除",
+      DeleteConfirm: "确认删除?",
+    },
+    EditModal: {
+      Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
+      Download: "下载",
+      Content: "OpenAPI Schema",
+      Method: "方法",
+      Error: "格式错误",
+    },
+  },
   Mask: {
     Name: "面具",
     Page: {

+ 5 - 25
app/locales/en.ts

@@ -517,10 +517,6 @@ const en: LocaleType = {
     Clear: "Context Cleared",
     Revert: "Revert",
   },
-  Plugin: {
-    Name: "Plugin",
-    Artifacts: "Artifacts",
-  },
   Discovery: {
     Name: "Discovery",
   },
@@ -544,6 +540,7 @@ const en: LocaleType = {
   },
   Plugin: {
     Name: "Plugin",
+    Artifacts: "Artifacts",
     Page: {
       Title: "Plugins",
       SubTitle: (count: number) => `${count} plugins`,
@@ -551,8 +548,7 @@ const en: LocaleType = {
       Create: "Create",
     },
     Item: {
-      Info: (count: number) => `${count} plugins`,
-      Chat: "Chat",
+      Info: (count: number) => `${count} method`,
       View: "View",
       Edit: "Edit",
       Delete: "Delete",
@@ -562,25 +558,9 @@ const en: LocaleType = {
       Title: (readonly: boolean) =>
         `Edit Plugin ${readonly ? "(readonly)" : ""}`,
       Download: "Download",
-      Clone: "Clone",
-    },
-    Config: {
-      Avatar: "Bot Avatar",
-      Name: "Bot Name",
-      Sync: {
-        Title: "Use Global Config",
-        SubTitle: "Use global config in this chat",
-        Confirm: "Confirm to override custom config with global config?",
-      },
-      HideContext: {
-        Title: "Hide Context Prompts",
-        SubTitle: "Do not show in-context prompts in chat",
-      },
-      Share: {
-        Title: "Share This Plugin",
-        SubTitle: "Generate a link to this mask",
-        Action: "Copy Link",
-      },
+      Content: "OpenAPI Schema",
+      Method: "Method",
+      Error: "OpenAPI Schema Error",
     },
   },
   Mask: {

+ 81 - 42
app/store/plugin.ts

@@ -10,16 +10,91 @@ export type Plugin = {
   createdAt: number;
   title: string;
   version: string;
-  context: string;
+  content: string;
   builtin: boolean;
 };
 
+export type FunctionToolItem = {
+  type: string;
+  function: {
+    name: string;
+    description?: string;
+    parameters: Object;
+  };
+};
+
+type FunctionToolServiceItem = {
+  api: OpenAPIClientAxios;
+  tools: FunctionToolItem[];
+  funcs: Function[];
+};
+
+export const FunctionToolService = {
+  tools: {} as Record<string, FunctionToolServiceItem>,
+  add(plugin: Plugin, replace = false) {
+    if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
+    const api = new OpenAPIClientAxios({
+      definition: yaml.load(plugin.content),
+    });
+    console.log("add", plugin, api);
+    try {
+      api.initSync();
+    } catch (e) {}
+    const operations = api.getOperations();
+    return (this.tools[plugin.id] = {
+      api,
+      length: operations.length,
+      tools: operations.map((o) => {
+        const parameters = o?.requestBody?.content["application/json"]
+          ?.schema || {
+          type: "object",
+          properties: {},
+        };
+        if (!parameters["required"]) {
+          parameters["required"] = [];
+        }
+        if (o.parameters instanceof Array) {
+          o.parameters.forEach((p) => {
+            if (p.in == "query" || p.in == "path") {
+              // const name = `${p.in}__${p.name}`
+              const name = p.name;
+              console.log("p", p, p.schema);
+              parameters["properties"][name] = {
+                type: p.schema.type,
+                description: p.description,
+              };
+              if (p.required) {
+                parameters["required"].push(name);
+              }
+            }
+          });
+        }
+        return {
+          type: "function",
+          function: {
+            name: o.operationId,
+            description: o.description,
+            parameters: parameters,
+          },
+        };
+      }),
+      funcs: operations.reduce((s, o) => {
+        s[o.operationId] = api.client[o.operationId];
+        return s;
+      }, {}),
+    });
+  },
+  get(id) {
+    return this.tools[id];
+  },
+};
+
 export const createEmptyPlugin = () =>
   ({
     id: nanoid(),
     title: "",
-    version: "",
-    context: "",
+    version: "1.0.0",
+    content: "",
     builtin: false,
     createdAt: Date.now(),
   }) as Plugin;
@@ -69,46 +144,10 @@ export const usePluginStore = createPersistStore(
       const selected = ids
         .map((id) => plugins[id])
         .filter((i) => i)
-        .map((i) => [
-          i,
-          new OpenAPIClientAxios({ definition: yaml.load(i.content) }),
-        ])
-        .map(([item, api]) => {
-          api.initSync();
-          const operations = api.getOperations().map((o) => {
-            const parameters = o.parameters;
-            return [
-              {
-                type: "function",
-                function: {
-                  name: o.operationId,
-                  description: o.description,
-                  parameters: o.parameters,
-                },
-              },
-              api.client[o.operationId],
-            ];
-            // return [{
-            // }, function(arg) {
-            //   const args = []
-            //   for (const p in parameters) {
-            //     if (p.type === "object") {
-            //       const a = {}
-            //       for (const n of p.)
-            //     }
-            //   }
-            // }]
-          });
-          return [item, api, operations];
-        });
-      console.log("selected", selected);
-      const result = selected.reduce((s, i) => s.concat(i[2]), []);
+        .map((p) => FunctionToolService.add(p));
       return [
-        result.map(([t, _]) => t),
-        result.reduce((s, i) => {
-          s[i[0].function.name] = i[1];
-          return s;
-        }, {}),
+        selected.reduce((s, i) => s.concat(i.tools), []),
+        selected.reduce((s, i) => Object.assign(s, i.funcs), {}),
       ];
     },
     get(id?: string) {