diff --git a/src/util/APIUtil.ts b/src/util/APIUtil.ts new file mode 100644 index 0000000..a0efc3e --- /dev/null +++ b/src/util/APIUtil.ts @@ -0,0 +1,111 @@ +import axios from "axios"; +import { v4 as uuidv4 } from 'uuid'; + +class APIUtil { + private static instance: APIUtil; + private baseUrl: string | undefined; + private accessKey: string | undefined; + private webappUrl: string | undefined; + private currentMessageId: string | undefined; + + + constructor() { + console.log("APIUtil ready"); + } + + public static getInstance(): APIUtil { + if (!APIUtil.instance) { + APIUtil.instance = new APIUtil(); + } + return APIUtil.instance; + } + async fetchWebappUrl() { + try { + const res = await axios.get( + `${this.baseUrl}/addresses/webapp`, + { headers: { 'Authorization': `Bearer ${this.accessKey}` }} + ) + const urlOrPath = res?.data; + if (!urlOrPath) { + throw new Error("No webapp url found"); + } + let href = ""; + if (urlOrPath.startsWith("http://") || urlOrPath.startsWith("https://")) { + href = urlOrPath; + } else { + href = new URL(urlOrPath, this.baseUrl).href + } + if (href.endsWith('/')) { + href = href.slice(0, -1); + } + if (href.endsWith('/api')) { + href = href.slice(0, -4); + } + console.log('Webapp url: ', href) + return href; + } catch (err) { + console.error("Error fetch webapp url:", err); + return "https://app.devchat.ai"; + } + } + + config(baseUrl: string, accessKey: string) { + this.baseUrl = baseUrl; + this.accessKey = accessKey; + if (!this.webappUrl) { + this.fetchWebappUrl().then(url => { + this.webappUrl = url; + }) + } + } + + async createMessage(message: object) { + this.currentMessageId = `msg-${uuidv4()}`; + try { + const res = await axios.post( + `${this.webappUrl}/api/v1/messages`, + {...message, message_id: this.currentMessageId}, + { headers: { + Authorization: `Bearer ${this.accessKey}`, + 'Content-Type': 'application/json', + }} + ) + console.log("Message created: ", res?.data); + } catch(err) { + console.error(err); + } + } + + async createEvent(event: object) { + try { + const res = await axios.post( + `${this.webappUrl}/api/v1/messages/${this.currentMessageId}/events`, + event, + {headers: { + Authorization: `Bearer ${this.accessKey}`, + 'Content-Type': 'application/json', + }} + ) + console.log("Event created: ", res?.data); + } catch(err) { + console.error(err); + } + } + + async getBalance() { + try { + if (!this.webappUrl) this.webappUrl = await this.fetchWebappUrl(); + const res = await axios.get( + `${this.webappUrl}/api/v1/users/profile`, + {headers: { Authorization: `Bearer ${this.accessKey}` }} + ) + return res?.data?.organization + } catch(err) { + console.error(err); + return null; + } + } +} + + +export default APIUtil.getInstance(); diff --git a/src/util/ideaBridge.ts b/src/util/ideaBridge.ts index 715a9b9..63d6225 100644 --- a/src/util/ideaBridge.ts +++ b/src/util/ideaBridge.ts @@ -329,6 +329,9 @@ class IdeaBridge { case "updateSetting/response": this.resviceUpdateSetting(res); break; + case "codeDiffApply/response": + this.resviceCodeDiffApply(res); + break; case "sendUserMessage/response": this.resviceSendUserMessage(res); break; @@ -377,6 +380,10 @@ class IdeaBridge { this.executeHandlers("updateSetting", res.payload); } + resviceCodeDiffApply(res) { + this.executeHandlers("codeDiffApply", res.payload); + } + resviceSendUserMessage(res) { this.executeHandlers("chatWithDevChat", { command: "chatWithDevChat", diff --git a/src/views/App.tsx b/src/views/App.tsx index 812bb4f..315cd37 100644 --- a/src/views/App.tsx +++ b/src/views/App.tsx @@ -8,6 +8,7 @@ import { useMst } from "./stores/RootStore"; import MessageUtil from "@/util/MessageUtil"; import "./App.css"; import "./i18n"; +import APIUtil from "@/util/APIUtil"; export default function App() { const [ready, setReady] = useState(false); @@ -28,6 +29,7 @@ export default function App() { MessageUtil.registerHandler("readConfig", (data: { value: any }) => { console.log("readConfig registerHandler: ", data); config.setConfig(data.value); + APIUtil.config(config.getAPIBase(), config.getUserKey()) config.refreshModelList(); setReady(true); }); diff --git a/src/views/components/BalanceTip/index.tsx b/src/views/components/BalanceTip/index.tsx index cfea49b..43fec7d 100644 --- a/src/views/components/BalanceTip/index.tsx +++ b/src/views/components/BalanceTip/index.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import axios from "axios"; import messageUtil from "@/util/MessageUtil"; import { IconWallet } from "@tabler/icons-react"; import { @@ -11,6 +10,7 @@ import { } from "@mantine/core"; import { Trans } from "react-i18next"; import { useMst } from "@/views/stores/RootStore"; +import APIUtil from "@/util/APIUtil"; const currencyMap = { USD: "$", @@ -28,15 +28,9 @@ function formatCurrency(balance: number | null, currency: string) { return `${currencyMap[currency] || currency}${balance}`; } -const envMap = { - dev: { - requestUrl: "https://apptest.devchat.ai", - link: "https://webtest.devchat.ai", - }, - prod: { - requestUrl: "https://app.devchat.ai", - link: "https://web.devchat.ai", - }, +const links = { + dev: "https://webtest.devchat.ai", + prod: "https://web.devchat.ai", }; export default function WechatTip() { @@ -50,23 +44,13 @@ export default function WechatTip() { const platform = process.env.platform; const getBalance = () => { - if (!envMap[env].requestUrl || !accessKey) { - return; - } - setLoading(true); - axios - .get(`${envMap[env].requestUrl}/api/v1/users/profile`, { - headers: { Authorization: `Bearer ${accessKey}` }, - }) - .then((res) => { - if (res?.data?.organization?.balance) { - setBalance(formatBalance(res?.data?.organization?.balance)); - setCurrency(res?.data?.organization?.currency); - } - }) - .finally(() => { - setLoading(false); - }); + APIUtil.getBalance().then(org => { + setLoading(true); + setBalance(formatBalance(org?.balance)); + setCurrency(org?.currency); + }).finally(() => { + setLoading(false); + }) }; useEffect(() => { @@ -93,7 +77,7 @@ export default function WechatTip() { e.stopPropagation(); messageUtil.sendMessage({ command: "openLink", - url: envMap[env].link, + url: links[env], }); }; @@ -139,7 +123,7 @@ export default function WechatTip() { web.devchat.ai{" "} ) : ( - + web.devchat.ai{" "} )} diff --git a/src/views/components/MessageMarkdown/CodeButtons.tsx b/src/views/components/MessageMarkdown/CodeButtons.tsx index d03f80d..40a44c4 100644 --- a/src/views/components/MessageMarkdown/CodeButtons.tsx +++ b/src/views/components/MessageMarkdown/CodeButtons.tsx @@ -1,9 +1,10 @@ import { Tooltip, ActionIcon, CopyButton, Flex } from "@mantine/core"; import { IconCheck, IconGitCommit, IconFileDiff, IconColumnInsertRight, IconReplace, IconCopy,IconFile } from "@tabler/icons-react"; import React, { useState } from "react"; +import { useMst } from "@/views/stores/RootStore"; import messageUtil from '@/util/MessageUtil'; -import language from "react-syntax-highlighter/dist/esm/languages/hljs/1c"; +import APIUtil from "@/util/APIUtil"; const IconButton = ({ label, color = 'gray', onClick, children }) => ( @@ -31,10 +32,14 @@ const CommitButton = ({ code }) => { }; const CodeCopyButton = ({ code }) => { + const {config} = useMst(); return ( {({ copied, copy }) => ( - + { + copy(); + APIUtil.createEvent({name: 'copy', value: 'copy'}) + }}> {copied ? : } )} @@ -43,11 +48,14 @@ const CodeCopyButton = ({ code }) => { }; const DiffButton = ({ code }) => { + const {config} = useMst(); const handleClick = () => { + const e = 'show_diff'; messageUtil.sendMessage({ - command: 'show_diff', + command: e, content: code }); + APIUtil.createEvent({name: e, value: e}) }; return ( @@ -57,11 +65,14 @@ const DiffButton = ({ code }) => { }; const CodeApplyButton = ({ code }) => { + const {config} = useMst(); const handleClick = () => { + const e = 'code_apply'; messageUtil.sendMessage({ - command: 'code_apply', + command: e, content: code }); + APIUtil.createEvent({name: e, value: e}) }; return ( @@ -71,11 +82,14 @@ const CodeApplyButton = ({ code }) => { }; const FileApplyButton = ({ code }) => { + const {config} = useMst(); const handleClick = () => { + const e = 'code_file_apply'; messageUtil.sendMessage({ - command: 'code_file_apply', + command: e, content: code }); + APIUtil.createEvent({name: e, value: e}) }; return ( @@ -86,12 +100,15 @@ const FileApplyButton = ({ code }) => { // Add a new button to create new file const NewFileButton = ({ language,code }) => { + const {config} = useMst(); const handleClick = () => { + const e = 'code_new_file'; messageUtil.sendMessage({ - command: 'code_new_file', + command: e, language: language, content: code }); + APIUtil.createEvent({name: e, value: e}) }; return ( diff --git a/src/views/components/MessageMarkdown/index.tsx b/src/views/components/MessageMarkdown/index.tsx index e76d460..bdfae4e 100644 --- a/src/views/components/MessageMarkdown/index.tsx +++ b/src/views/components/MessageMarkdown/index.tsx @@ -17,6 +17,7 @@ import { useSetState } from "@mantine/hooks"; import { useTranslation } from "react-i18next"; import { useRouter } from "@/views/router"; import remarkGfm from "remark-gfm"; +import APIUtil from "@/util/APIUtil"; (typeof global !== "undefined" ? global : window).Prism = Prism; require("prismjs/components/prism-java"); @@ -70,7 +71,7 @@ function parseMetaData(string) { const MessageMarkdown = observer((props: MessageMarkdownProps) => { const { children, activeStep = false, messageDone } = props; - const { chat } = useMst(); + const { config, chat } = useMst(); const router = useRouter(); const [steps, setSteps] = useState([]); const tree = fromMarkdown(children); @@ -107,6 +108,13 @@ const MessageMarkdown = observer((props: MessageMarkdownProps) => { }); }; + const handleCodeCopy = (event) => { + const selection = window.getSelection()?.toString(); + console.log("Copied: ", selection); + const e = 'manual_copy'; + APIUtil.createEvent({name: e, value: selection}) + } + useEffect(() => { let previousNode: any = null; let chatmarkCount = 0; @@ -328,6 +336,7 @@ const MessageMarkdown = observer((props: MessageMarkdownProps) => { whiteSpace: "pre", ...props.style, }} + onCopy={handleCodeCopy} {...props} > {tokens.map((line, i) => ( diff --git a/src/views/stores/ChatStore.ts b/src/views/stores/ChatStore.ts index 325644a..43c8f36 100644 --- a/src/views/stores/ChatStore.ts +++ b/src/views/stores/ChatStore.ts @@ -5,6 +5,7 @@ import yaml from "js-yaml"; import { RootInstance } from "./RootStore"; import { useTranslation } from "react-i18next"; import i18next from "i18next"; +import APIUtil from "@/util/APIUtil"; interface Context { content: string; @@ -171,7 +172,12 @@ export const ChatStore = types self.hasDone = false; self.errorMessage = ""; self.currentMessage = ""; - const chatModel = getParent(self).config.getDefaultModel(); + const config = getParent(self).config + const chatModel = config.getDefaultModel(); + messageUtil.registerHandler("codeDiffApply", (_: any) => { + const e = 'code_diff_apply' + APIUtil.createEvent({name: e, value: e}) + }) messageUtil.sendMessage({ command: "sendMessage", text: text, @@ -179,6 +185,7 @@ export const ChatStore = types parent_hash: lastNonEmptyHash(), model: chatModel, }); + APIUtil.createMessage({content: text, model: chatModel}); }; const helpMessage = (originalMessage = false) => { @@ -390,7 +397,7 @@ Thinking... self.messages.push(...messages); }, updateLastMessage: (message: string) => { - console.log("message: ", message); + // console.log("message: ", message); if (self.messages.length > 0) { self.messages[self.messages.length - 1].message = message; // if (message === "") { diff --git a/src/views/stores/ConfigStore.ts b/src/views/stores/ConfigStore.ts index 9d0642d..f9c6c10 100644 --- a/src/views/stores/ConfigStore.ts +++ b/src/views/stores/ConfigStore.ts @@ -2,7 +2,6 @@ import MessageUtil from "@/util/MessageUtil"; import { types, Instance, flow } from "mobx-state-tree"; import modelsTemplate from "@/models"; import cloneDeep from "lodash.clonedeep"; -import { set } from "mobx"; import axios from "axios"; const defaultAPIBase = [