diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94184fef..6a10df9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,7 @@ on: branches: - main - test + - concom - dev paths: ['.github/workflows/**', '**/Makefile', '**/*.go', '**/*.json', '**/*.yml', '**/*.ts', '**/*.js'] @@ -413,7 +414,7 @@ jobs: name: mor-launch-win-x64.zip release: - if: ${{ github.repository != 'MorpheusAIs/Morpheus-Lumerin-Node' && (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' )) || github.event.inputs.create_release == 'true' }} + if: ${{ github.repository != 'MorpheusAIs/Morpheus-Lumerin-Node' && (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || github.ref == 'refs/heads/concom' )) || github.event.inputs.create_release == 'true' }} runs-on: ubuntu-latest needs: - Ubuntu-22-x64 diff --git a/ui-desktop/src/main/src/client/apiGateway.js b/ui-desktop/src/main/src/client/apiGateway.js index 2728519e..dda44599 100644 --- a/ui-desktop/src/main/src/client/apiGateway.js +++ b/ui-desktop/src/main/src/client/apiGateway.js @@ -216,6 +216,35 @@ const updateChatHistoryTitle = async ({ id, title}) => { } } + /** + * @param {string} address + * @param {string} endpoint + * @returns {Promise} +*/ +const checkProviderConnectivity = async ({ address, endpoint}) => { + try { + const path = `${config.chain.localProxyRouterUrl}/proxy/provider/ping`; + const response = await fetch(path, { + method: "POST", + body: JSON.stringify({ + providerAddr: address, + providerUrl: endpoint + }), + }); + + if(!response.ok) { + return false; + } + + const body = await response.json(); + return !!body.ping; + } + catch (e) { + console.log("checkProviderConnectivity: Error", e) + return false; + } + } + export default { getAllModels, getBalances, @@ -229,4 +258,5 @@ export default { getChatHistory, updateChatHistoryTitle, deleteChatHistory, + checkProviderConnectivity } \ No newline at end of file diff --git a/ui-desktop/src/main/src/client/subscriptions/no-core.js b/ui-desktop/src/main/src/client/subscriptions/no-core.js index cc03149d..f12bf403 100644 --- a/ui-desktop/src/main/src/client/subscriptions/no-core.js +++ b/ui-desktop/src/main/src/client/subscriptions/no-core.js @@ -36,7 +36,8 @@ const listeners = { "update-chat-history-title": handlers.updateChatHistoryTitle, // Failover "get-failover-setting": handlers.isFailoverEnabled, - "set-failover-setting": handlers.setFailoverSetting + "set-failover-setting": handlers.setFailoverSetting, + "check-provider-connectivity": handlers.checkProviderConnectivity } // Subscribe to messages where no core has to react diff --git a/ui-desktop/src/renderer/src/client/index.jsx b/ui-desktop/src/renderer/src/client/index.jsx index d966bbe2..deb438f6 100644 --- a/ui-desktop/src/renderer/src/client/index.jsx +++ b/ui-desktop/src/renderer/src/client/index.jsx @@ -145,6 +145,7 @@ const createClient = function (createStore) { // Failover getFailoverSetting: utils.forwardToMainProcess('get-failover-setting', 750000), setFailoverSetting: utils.forwardToMainProcess('set-failover-setting', 750000), + checkProviderConnectivity: utils.forwardToMainProcess('check-provider-connectivity', 750000) } const api = { diff --git a/ui-desktop/src/renderer/src/components/chat/Chat.tsx b/ui-desktop/src/renderer/src/components/chat/Chat.tsx index 17736767..b0f73aeb 100644 --- a/ui-desktop/src/renderer/src/components/chat/Chat.tsx +++ b/ui-desktop/src/renderer/src/components/chat/Chat.tsx @@ -45,12 +45,14 @@ const userMessage = { user: 'Me', role: "user", icon: "M", color: "#20dc8e" }; const Chat = (props) => { const chatBlockRef = useRef(null); + const bidsSpinWaitClosed = useRef(false); const [value, setValue] = useState(""); const [isLoading, setIsLoading] = useState(true); const [messages, setMessages] = useState([]); const [isOpen, setIsOpen] = useState(false); const [sessions, setSessions] = useState(); + const [providersAvailability, setProvidersAvailability] = useState([]); const [isSpinning, setIsSpinning] = useState(false); const [meta, setMeta] = useState({ budget: 0, supply: 0 }); @@ -81,14 +83,14 @@ const Chat = (props) => { useEffect(() => { (async () => { - const [meta, chainData, chats, userBalances] = await Promise.all([ - props.getMetaInfo(), + console.time("LOAD") + const [chainData, userSessions, chats] = await Promise.all([ props.getModelsData(), - props.client.getChatHistoryTitles() as Promise, - props.getBalances()]); + props.getSessionsByUser(props.address), + props.client.getChatHistoryTitles() as Promise]); - setBalances(userBalances) - setMeta(meta); + setBalances(chainData.userBalances) + setMeta(chainData.meta); setChainData(chainData) const mappedChatData = chats.reduce((res, item) => { @@ -106,7 +108,15 @@ const Chat = (props) => { }, [] as ChatData[]) setChatsData(mappedChatData); - const sessions = await refreshSessions(chainData?.models); + const sessions = userSessions.reduce((res, item) => { + const sessionModel = chainData.models.find(x => x.Id == item.ModelAgentId); + if (sessionModel) { + item.ModelName = sessionModel.Name; + res.push(item); + } + return res; + }, []); + setSessions(sessions); const openSessions = sessions.filter(s => !isClosed(s)); @@ -120,6 +130,7 @@ const Chat = (props) => { if (!openSessions.length) { useLocalModelChat(); + console.timeEnd("LOAD") return; } @@ -128,10 +139,11 @@ const Chat = (props) => { if (!latestSessionModel) { useLocalModelChat(); + console.timeEnd("LOAD") return; } - const openBid = latestSessionModel?.bids?.find(b => b.Id == latestSession.BidID); + const openBid = await props.getBidInfo(latestSession.BidID) if (!openBid) { useLocalModelChat(); @@ -141,12 +153,66 @@ const Chat = (props) => { setSelectedBid(openBid); setActiveSession(latestSession); setChat({ id: generateHashId(), createdAt: new Date(), modelId: latestSessionModel.ModelAgentId }); - })().then(() => { + console.timeEnd("LOAD") + })() + .then(() => { setIsLoading(false); }) }, []) + useEffect(() => { + if(!chainData) + return; + + (async () => { + const providersMap = chainData.providers.reduce((a, b) => ({ ...a, [b.Address.toLowerCase()]: b }), {}); + const modelsWithBids= (await Promise.all( + chainData.models.map(async m => { + const id = m.Id; + if(m.isLocal){ + return { id } + } + const bids = (await props.getBidsByModelId(id)) + .map(b => ({ ...b, ProviderData: providersMap[b.Provider.toLowerCase()], Model: m })) + .filter(b => b.ProviderData); + + if(!bids.length){ + return null; + } + + return { id, bids } + }) + )).reduce((acc, next) => { + if(!next) { + return acc; + } + const model = chainData.models.find(m => m.Id == next.id); + return [...acc, { ...model, bids: next.bids}] + }, []); + + setChainData({...chainData, models: modelsWithBids}) + bidsSpinWaitClosed.current = true; + })(); + + (async () => { + const availabilityResults = await props.getProvidersAvailability(chainData.providers); + setProvidersAvailability(availabilityResults); + })(); + + }, chainData) + + const spinWaitForBids = async () => { + if(bidsSpinWaitClosed.current) + return; + setIsLoading(true); + while(!bidsSpinWaitClosed.current) { + await new Promise(resolve => setTimeout(resolve, 300)); + } + setIsLoading(false); + } + const toggleDrawer = async () => { + spinWaitForBids(); setIsOpen((prevState) => !prevState) } @@ -228,9 +294,9 @@ const Chat = (props) => { } } - const refreshSessions = async (models = null) => { + const refreshSessions = async () => { const sessions = (await props.getSessionsByUser(props.address)).reduce((res, item) => { - const sessionModel = (models || chainData.models).find(x => x.Id == item.ModelAgentId); + const sessionModel = chainData.models.find(x => x.Id == item.ModelAgentId); if (sessionModel) { item.ModelName = sessionModel.Name; res.push(item); @@ -272,8 +338,6 @@ const Chat = (props) => { setSelectedModel(selectedModel); setIsReadonly(false); - // toggleDrawer(); - setChat({ ...chatData }) if (chatData.isLocal) { @@ -301,6 +365,7 @@ const Chat = (props) => { } const handleReopen = async () => { + spinWaitForBids(); setIsLoading(true); const newSessionId = await onOpenSession(true); setIsReadonly(false); @@ -428,11 +493,9 @@ const Chat = (props) => { const otherMessages = memoState.filter(m => m.id != part.id); if (imageContent) { result = [...otherMessages, { id: part.job, user: modelName, role: "assistant", text: imageContent, isImageContent: true, ...iconProps }]; - } - if (videoRawContent) { + } else if (videoRawContent) { result = [...otherMessages, { id: part.job, user: modelName, role: "assistant", text: videoRawContent, isVideoRawContent: true, ...iconProps }]; - } - else { + } else { const text = `${message?.text || ''}${part?.choices[0]?.delta?.content || ''}`.replace("<|im_start|>", "").replace("<|im_end|>", ""); result = [...otherMessages, { id: part.id, user: modelName, role: "assistant", text: text, ...iconProps }]; } @@ -521,7 +584,10 @@ const Chat = (props) => { setIsReadonly(false); setChat({ id: generateHashId(), createdAt: new Date(), modelId, isLocal }); - const selectedModel = isLocal ? chainData.models.find((m: any) => m.Id == modelId) : chainData.models.find((m: any) => m.Id == modelId && m.bids); + const selectedModel = isLocal + ? chainData.models.find((m: any) => m.Id == modelId) + : chainData.models.find((m: any) => m.Id == modelId && m.bids); + setSelectedModel(selectedModel); if (isLocal) { @@ -535,9 +601,7 @@ const Chat = (props) => { if (openModelSession) { const selectedBid = selectedModel.bids.find(b => b.Id == openModelSession.BidID && b.bids); - if (selectedBid) { - setSelectedBid(selectedBid); - } + setSelectedBid(selectedBid); setActiveSession(openModelSession) return; } @@ -616,7 +680,10 @@ const Chat = (props) => { setOpenChangeModal(true)}> + onClick={async () => { + await spinWaitForBids(); + setOpenChangeModal(true); + } }> New chat @@ -717,6 +784,7 @@ const Chat = (props) => { models={(chainData as any)?.models} isActive={openChangeModal} symbol={props.symbol} + providersAvailability={providersAvailability} onChangeModel={(eventData) => { onCreateNewChat(eventData); }} diff --git a/ui-desktop/src/renderer/src/components/chat/modals/ModelRow.tsx b/ui-desktop/src/renderer/src/components/chat/modals/ModelRow.tsx index 2d2b1581..ea0e0bb3 100644 --- a/ui-desktop/src/renderer/src/components/chat/modals/ModelRow.tsx +++ b/ui-desktop/src/renderer/src/components/chat/modals/ModelRow.tsx @@ -99,12 +99,7 @@ function ModelRow(props) { const bids = props?.model?.bids || []; const modelId = props?.model?.Id || ''; const isLocal = props?.model?.isLocal; - const lastAvailabilityCheck: Date = (() => { - if(!bids?.length) { - return new Date(); - } - return bids.map(b => new Date(b.ProviderData?.availabilityUpdatedAt ?? new Date()))[0]; - })(); + const lastAvailabilityCheck: Date = new Date(props?.model?.lastCheck ?? new Date()); const [selected, changeSelected] = useState(); const [useSelect, setUseSelect] = useState(); diff --git a/ui-desktop/src/renderer/src/components/chat/modals/ModelSelectionModal.tsx b/ui-desktop/src/renderer/src/components/chat/modals/ModelSelectionModal.tsx index 23e7bc5c..56b0593e 100644 --- a/ui-desktop/src/renderer/src/components/chat/modals/ModelSelectionModal.tsx +++ b/ui-desktop/src/renderer/src/components/chat/modals/ModelSelectionModal.tsx @@ -36,7 +36,7 @@ const RVContainer = styled(RVList)` overflow: visible !important; }` -const ModelSelectionModal = ({ isActive, handleClose, models, onChangeModel, symbol }) => { +const ModelSelectionModal = ({ isActive, handleClose, models, onChangeModel, symbol, providersAvailability }) => { const [search, setSearch] = useState(); if (!isActive) { @@ -49,7 +49,30 @@ const ModelSelectionModal = ({ isActive, handleClose, models, onChangeModel, sym } const sortedModels = models - .map(m => ({ ...m, isOnline: m.isLocal || m.bids.some(b => b.ProviderData?.availabilityStatus != "disconnected") })) + .map(m => { + if(m.isLocal || !providersAvailability){ + return ({...m, isOnline: true }) + } + + const info = m.bids.reduce((acc, next) => { + const entry = providersAvailability.find(pa => pa.id == next.Provider); + if(!entry) { + return acc; + } + + if(entry.isOnline) { + return acc; + } + + const isOnline = entry.status != "disconnected"; + + return { + isOnline, + lastCheck: !isOnline ? entry.time : undefined + } + }, {}); + return ({ ...m, ...info }) + } ) .sort((a, b) => b.isOnline - a.isOnline); const filterdModels = search ? sortedModels.filter(m => m.Name.toLowerCase().includes(search.toLowerCase())) : sortedModels; diff --git a/ui-desktop/src/renderer/src/components/dashboard/Dashboard.jsx b/ui-desktop/src/renderer/src/components/dashboard/Dashboard.jsx index 5d5d4ef6..e74d9606 100644 --- a/ui-desktop/src/renderer/src/components/dashboard/Dashboard.jsx +++ b/ui-desktop/src/renderer/src/components/dashboard/Dashboard.jsx @@ -56,6 +56,7 @@ const Dashboard = ({ ethCoinPrice, loadTransactions, getStakedFunds, + explorerUrl, ...props }) => { const [activeModal, setActiveModal] = useState(null) @@ -174,7 +175,7 @@ const Dashboard = ({ window.openLink(`https://sepolia.arbiscan.io/address/${address}`)} + onClick={() => window.openLink(explorerUrl)} block > Transaction Explorer diff --git a/ui-desktop/src/renderer/src/store/hocs/withChatState.jsx b/ui-desktop/src/renderer/src/store/hocs/withChatState.jsx index f85b1c0f..e42afde0 100644 --- a/ui-desktop/src/renderer/src/store/hocs/withChatState.jsx +++ b/ui-desktop/src/renderer/src/store/hocs/withChatState.jsx @@ -4,7 +4,7 @@ import React from 'react'; import { ToastsContext } from '../../components/toasts'; import selectors from '../selectors'; import axios from 'axios'; -import { getSessionsByUser, getBidsByModelId } from '../utils/apiCallsHelper'; +import { getSessionsByUser, getBidsByModelId, getBidInfoById } from '../utils/apiCallsHelper'; const AvailabilityStatus = { available: "available", @@ -93,56 +93,27 @@ const withChatState = WrappedComponent => { } getModelsData = async () => { - const [localModels, modelsResp, providersResp] = await Promise.all([ + const [localModels, modelsResp, providersResp, meta, userBalances] = await Promise.all([ this.getLocalModels(), this.getAllModels(), - this.getProviders()]); + this.getProviders(), + this.getMetaInfo(), + this.getBalances()]); const models = modelsResp.filter(m => !m.IsDeleted); const providers = providersResp.filter(m => !m.IsDeleted); - const availabilityResults = await this.getProvidersAvailability(providers); - availabilityResults.forEach(ar => { - const provider = providers.find(p => p.Address == ar.id); - if(!provider) - return; - - provider.availabilityStatus = ar.status; - provider.availabilityUpdatedAt = ar.time; - }); - - const providersMap = providers.reduce((a, b) => ({ ...a, [b.Address.toLowerCase()]: b }), {}); - - const responses = (await Promise.all( - models.map(async m => { - const id = m.Id; - const bids = (await getBidsByModelId(this.props.config.chain.localProxyRouterUrl, id)) - .filter(b => +b.DeletedAt === 0) - .map(b => ({ ...b, ProviderData: providersMap[b.Provider.toLowerCase()], Model: m })) - .filter(b => b.ProviderData) - .filter(b => b.Provider != this.props.address); + const result = [...localModels.map(m => ({...m, isLocal: true })), ...models]; - return { id, bids } - }) - )).reduce((a,b) => ({...a, [b.id]: b.bids}), {}); - - const result = [...localModels.map(m => ({...m, isLocal: true }))]; - - for (const model of models) { - const id = model.Id; - const bids = responses[id]; - - if(!bids.length) { - continue; - } - - result.push({ ...model, bids }) - } - - return { models: result, providers } + return { models: result, providers, meta, userBalances } } getProvidersAvailability = async (providers) => { + const isValidUrl = (url) => { + const urlRegex = /^(https?:\/\/)?(([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|localhost)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d{1,5})?(\/\S*)?$/; + return urlRegex.test(url); + } + const availabilityResults = await Promise.all(providers.map(async p => { try { const storedRecord = JSON.parse(localStorage.getItem(p.Address)); @@ -156,14 +127,12 @@ const withChatState = WrappedComponent => { } } - const endpoint = p.Endpoint; - const [domain, port] = endpoint.split(":"); - const { data } = await axios.post("https://portchecker.io/api/v1/query", { - host: domain, - ports: [port], - }); - - const isValid = !!data.check?.find((c) => c.port == port && c.status == true); + if(!isValidUrl(p.Endpoint)) { + return ({ id: p.Address, status: AvailabilityStatus.disconnected, time: new Date() }); + } + + const isValid = await this.props.client.checkProviderConnectivity({ endpoint: p.Endpoint, address: p.Address }) + const record = ({id: p.Address, status: isValid ? AvailabilityStatus.available : AvailabilityStatus.disconnected, time: new Date() }); localStorage.setItem(record.id, JSON.stringify({ status: record.status, time: record.time })); return record; @@ -190,6 +159,23 @@ const withChatState = WrappedComponent => { return await getSessionsByUser(this.props.config.chain.localProxyRouterUrl, user); } + getBidInfo = async (id) => { + if(!id){ + return; + } + + return await getBidInfoById(this.props.config.chain.localProxyRouterUrl, id) + } + + getBidsByModelId = async(modelId) => { + if(!modelId) { + return; + } + + const bids = await getBidsByModelId(this.props.config.chain.localProxyRouterUrl, modelId); + return bids.filter(b => +b.DeletedAt === 0).filter(b => b.Provider != this.props.address); + } + onOpenSession = async ({ modelId, duration }) => { this.context.toast('info', 'Processing...'); try { @@ -229,8 +215,10 @@ const withChatState = WrappedComponent => { return ( { page: selectors.getTransactionPage(state), pageSize: selectors.getTransactionPageSize(state), hasNextPage: selectors.getHasNextPage(state), + explorerUrl: selectors.getContractExplorerUrl(state, { + hash: selectors.getWalletAddress(state) + }) }); const mapDispatchToProps = dispatch => ({ diff --git a/ui-desktop/src/renderer/src/store/utils/apiCallsHelper.tsx b/ui-desktop/src/renderer/src/store/utils/apiCallsHelper.tsx index 13354489..40bbcd4e 100644 --- a/ui-desktop/src/renderer/src/store/utils/apiCallsHelper.tsx +++ b/ui-desktop/src/renderer/src/store/utils/apiCallsHelper.tsx @@ -16,7 +16,7 @@ export const getSessionsByUser = async (url, user) => { } } - const limit = 20; + const limit = 50; let offset = 0; let sessions: any[] = []; let all = false; @@ -30,7 +30,7 @@ export const getSessionsByUser = async (url, user) => { all = true; } else { - offset++; + offset += limit; } } @@ -55,7 +55,7 @@ export const getBidsByModelId = async (url, modelId) => { } } - const limit = 20; + const limit = 50; let offset = 0; let bids: any[] = []; let all = false; @@ -69,9 +69,22 @@ export const getBidsByModelId = async (url, modelId) => { all = true; } else { - offset++; + offset += limit; } } return bids; +} + +export const getBidInfoById = async (url, id) => { + try { + const path = `${url}/blockchain/bids/${id}` + const response = await fetch(path); + const data = await response.json(); + return data.bid; + } + catch (e) { + console.log("Error", e) + return undefined; + } } \ No newline at end of file