diff --git a/package.json b/package.json index 350d2af..7eeee2d 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "license": "MIT", "type": "module", "dependencies": { - "@kevisual/router": "workspace:*", - "@kevisual/store": "workspace:*", + "@kevisual/router": "0.0.6-alpha-2", + "@kevisual/store": "0.0.1-alpha.7", + "@kevisual/tab-leader": "^0.0.1", "@vitejs/plugin-react": "^4.3.4", "clsx": "^2.1.1", "eventemitter3": "^5.0.1", @@ -23,16 +24,16 @@ "marked": "^15.0.3", "marked-highlight": "^2.2.1", "nanoid": "^5.0.9", - "react": "^18.3.1", - "vite": "^6.0.1", - "zustand": "^5.0.1" + "react": "^19.0.0", + "zustand": "^5.0.2" }, "devDependencies": { "@build/tailwind": "1.0.2-alpha-2", "@types/lodash-es": "^4.17.12", - "@types/react": "^18.3.12", + "@types/react": "^19.0.1", "@types/umami": "^2.10.0", "autoprefixer": "^10.4.20", - "tailwindcss": "^3.4.15" + "tailwindcss": "^3.4.16", + "vite": "^6.0.3" } } \ No newline at end of file diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..0ae70f9 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,11 @@ +import { QueryRouterServer } from '@kevisual/router/browser'; +import { useContextKey } from '@kevisual/store/config'; +import { useConfigKey } from '@kevisual/store/config'; +import { EventEmitter } from 'eventemitter3'; + +export const app = useContextKey('app', () => { + return new QueryRouterServer(); +}); +export const emitter = useContextKey('emitter', () => { + return new EventEmitter(); +}); diff --git a/src/h.ts b/src/h.ts index ed18535..5b64a3a 100644 --- a/src/h.ts +++ b/src/h.ts @@ -6,13 +6,13 @@ const loadChidren = (element: any, children: any[]) => { return; } if (typeof child === 'function') { - console.log('child', child); + // console.log('child', child); return; } if (typeof child === 'undefined') { return; } - console.log('child', child); + // console.log('child', child); element.appendChild(typeof child === 'string' ? document.createTextNode(child) : child); }); }; @@ -52,7 +52,7 @@ export function h(type: string | Function, props: any, ...children: any[]): HTML const checkConnect = () => { if (element.isConnected) { onLoad?.({ el: element, key, _props: props }); - console.log('onLoad', element, key); + // console.log('onLoad', element, key); return true; } return false; diff --git a/src/main.tsx b/src/main.tsx index 29d31c0..8220d7d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,27 +1,35 @@ import { h } from '@/h'; import '@build/tailwind/main.css'; // tab的app-routes模块 -import './tab'; +// import './tab'; +import './app.ts'; +import { start } from '@kevisual/tab-leader'; import './page/index.css'; import './main.css'; import { Page } from '@kevisual/store/page'; import { AiChat } from './page/AiChat'; -const div = document.createElement('div'); - -div.className = 'text-4xl text-center p-4 bg-blue-200'; -div.innerHTML = 'Browser Apps'; +const main = () => { + start(); +}; +main(); const page = new Page({ path: '', key: '', basename: '', }); -page.addPage('/home', 'home'); + +page.addPage('/', 'home'); page.addPage('/ai', 'ai'); page.subscribe('home', (page) => { + const div = document.createElement('div'); + + div.className = 'text-4xl text-center p-4 bg-blue-200'; + div.innerHTML = 'Browser Apps'; + document.body.appendChild(div); }); diff --git a/src/modules/lazy-test.ts b/src/modules/lazy-test.ts new file mode 100644 index 0000000..09a7b73 --- /dev/null +++ b/src/modules/lazy-test.ts @@ -0,0 +1,3 @@ +export const test = () => { + console.log('test'); +}; diff --git a/src/page/AiChat.tsx b/src/page/AiChat.tsx index c280c7e..a91c420 100644 --- a/src/page/AiChat.tsx +++ b/src/page/AiChat.tsx @@ -63,7 +63,7 @@ export const AiChat = () => { store.subscribe( (state, prevState) => { const messages = state.messages; - console.log('store', state, prevState); + // console.log('store', state, prevState); if (aiMessageRef.current) { messages.forEach((message) => { const id = message.id; @@ -87,8 +87,8 @@ export const AiChat = () => { // 如果滚动条距离底部小于100px,自动滚动到底部 if (scrollContainerEl.scrollHeight - scrollContainerEl.scrollTop - scrollContainerEl.clientHeight < 400) { } - console.log('scrollContainerEl', scrollContainerEl.scrollHeight, scrollContainerEl.scrollTop, scrollContainerEl.clientHeight); - console.log('scrollContainerEl<400', scrollContainerEl.scrollHeight - scrollContainerEl.scrollTop - scrollContainerEl.clientHeight < 400); + // console.log('scrollContainerEl', scrollContainerEl.scrollHeight, scrollContainerEl.scrollTop, scrollContainerEl.clientHeight); + // console.log('scrollContainerEl<400', scrollContainerEl.scrollHeight - scrollContainerEl.scrollTop - scrollContainerEl.clientHeight < 400); bottom?.scrollIntoView({ behavior: 'smooth' }); } }; @@ -106,7 +106,7 @@ export const AiChat = () => { const input = textareaRef.current; const state = aiChatStore.getState(); if (state.loading || !state.end) { - console.log('loading'); + // console.log('loading'); return; } if (input && input.value.trim() !== '') { @@ -121,7 +121,7 @@ export const AiChat = () => { ref={ref} className='w-full h-full bg-blue-400 flex justify-center' onLoad={(e) => { - console.log('onLoad======', e, ref.current); + // console.log('onLoad======', e, ref.current); }}>

AI聊天

diff --git a/src/store/ai-chat-store.ts b/src/store/ai-chat-store.ts index de9f619..9fc4597 100644 --- a/src/store/ai-chat-store.ts +++ b/src/store/ai-chat-store.ts @@ -1,6 +1,6 @@ import { StoreManager } from '@kevisual/store'; import type { StateCreator, StoreApi } from 'zustand'; -import { useContextKey } from '@kevisual/store/context'; +import { useContextKey } from '@kevisual/store/config'; import { postOllama, postOllamaStrem } from '@/response/ollama/ollama'; import { nanoid } from 'nanoid'; diff --git a/src/tab.ts b/src/tab.ts deleted file mode 100644 index c4a0be8..0000000 --- a/src/tab.ts +++ /dev/null @@ -1,446 +0,0 @@ -import { nanoid } from 'nanoid'; -import { QueryRouterServer } from '@kevisual/router/browser'; -import { useContextKey } from '@kevisual/store/context'; -import { useConfigKey } from '@kevisual/store/config'; -import { EventEmitter } from 'eventemitter3'; -// const errorCdoe = { -// 840: 'not me', // 不是我 -// 999: 'find max', // 找到最大的 -// }; -export const emitter = useContextKey('emitter', () => { - return new EventEmitter(); -}); - -export const tabId = useConfigKey('tabId', () => { - return nanoid(8); -}); -export const openTabs = useContextKey('openTabs', () => { - return new Set(); -}); -export const tabConfig = useConfigKey('tabConfig', () => { - const random = Math.random().toString(36).substring(7); - return { - title: 'tab random' + random, - description: 'tab config', - tabId: useConfigKey('tabId', () => {}), - }; -}); - -export const app = useContextKey('app', () => { - return new QueryRouterServer(); -}); - -export const umami = useContextKey('umami', () => { - if (!window.umami) { - (window as any).umami = { - track: (event: string, data: any) => { - console.log('umami event not found', event, data); - }, - }; - } - return window.umami; -}); - -export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -// 控制 tab 启动 -let load = false; -let openTime = Date.now(); -let meIsMax = '1'; // 0: 未知, 1: 对比来源,我更大, 2: 不是 - -export type Msg = { - requestId?: string; - path: string; - key: string; - [key: string]: any; -}; -app - .route({ - path: 'tab', - key: 'introduce', - description: '当多个tab打开的时候,找打哦最后打开的,确信那些tabs是有效和打开的', - }) - .define(async (ctx) => { - const source = ctx.query?.source; - const sourceOpenTime = ctx.query?.openTime; - const update = ctx.query?.update; - if (!source) { - ctx.throw?.(400, 'tabId is required'); - } - openTabs.add(source); - if (update) { - localStorage.setItem('tab-' + tabId, source); - } - if (!load) { - // console.log('soucre time', sourceOpenTime, openTime, sourceOpenTime > openTime); - if (openTime < sourceOpenTime) { - meIsMax = '2'; // 来源的时间比我大 - } - } - ctx.code = 999; - ctx.body = 'ok'; - }) - .addTo(app); - -app - .route({ - path: 'tab', - key: 'close', - description: '当触发关闭事件的时候,清除tab', - }) - .define(async (ctx) => { - const source = ctx.query?.source; - if (!source) { - ctx.throw?.(400, 'tabId is required'); - } - openTabs.delete(source); - localStorage.removeItem('tab-' + source); - ctx.body = 'ok'; - }) - .addTo(app); - -app - .route({ - path: 'tab', - key: 'setTabs', - description: '已经知道最新的tabs了,通知所有的channel,设置打开的 tab', - }) - .define(async (ctx) => { - const tabs = ctx.query?.tabs || []; - openTabs.clear(); - tabs.forEach((tab) => { - openTabs.add(tab); - }); - console.log(`[${tabId}]Set open tabs: ${tabs.length}`, openTabs.keys()); - load = true; - ctx.body = 'ok'; - }) - .addTo(app); - -app - .route({ - path: 'tab', - key: 'getTabs', - description: '获取所有的打开的 tab', - }) - .define(async (ctx) => { - ctx.body = Array.from(openTabs); - }) - .addTo(app); - -// 操作私有 tab -app - .route({ - path: 'tab', - key: 'getMe', - description: '获取自己的 tab 信息,比如tabId,openTime,tabConfig', - }) - .define(async (ctx) => { - ctx.body = { tabId, openTime, tabConfig }; - }) - .addTo(app); - -app - .route({ - path: 'tab', - key: 'me', - id: 'me', - description: '验证是否是自己的标签页面,如果是自己则继续,否则返回错误', - }) - .define(async (ctx) => { - // check me - const to = ctx.query?.to; - if (to !== tabId) { - // not me - ctx.throw?.(840, 'not me'); - return; - } - ctx.body = 'ok'; - }) - .addTo(app); - -app - .route({ - path: 'channel', - key: 'postAllTabs', - description: '向所有的 tab 发送消息,如果payload有hasResponse,则等待所有的 tab 返回消息', - }) - .define(async (ctx) => { - const data = ctx.query?.data; - const hasResponse = ctx.query?.hasResponse; - if (!data?.path) { - ctx.throw?.(400, 'path is required'); - } - const res = await new Promise((resolve) => { - if (hasResponse) { - postAllTabs(data, (res) => { - resolve(res); - }); - } else { - postAllTabs(data); - resolve('ok'); - } - }); - ctx.body = res || 'ok'; - }) - .addTo(app); - -app - .route({ - path: 'channel', - key: 'postTab', - description: '向指定的 tab 发送消息,如果payload有hasResponse,则等待指定的 tab 返回消息', - }) - .define(async (ctx) => { - const data = ctx.query?.data; - const hasResponse = ctx.query?.hasResponse; - if (!data?.path) { - ctx.throw?.(400, 'path is required'); - } - const res = await new Promise((resolve) => { - if (hasResponse) { - postTab(data, (res) => { - resolve(res); - }); - } else { - postTab(data); - resolve('ok'); - } - }); - ctx.body = res || 'ok'; - }) - .addTo(app); - -app - .route({ - path: 'tab', - key: 'setConfig', - description: '设置 tab 的配置信息', - }) - .define(async (ctx) => { - const config = ctx.query?.config; - if (config) { - Object.assign(tabConfig, config); - } - ctx.body = tabConfig; - }) - .addTo(app); - -app - .route({ - path: 'tab', - key: 'getConfig', - description: '获取 tab 的配置信息', - }) - .define(async (ctx) => { - ctx.body = tabConfig; - }) - .addTo(app); - -const channel = useContextKey('channel', () => { - return new BroadcastChannel('tab-channel'); -}); - -channel.addEventListener('message', (event) => { - const { requestId, to, source, responseId } = event.data; - if (responseId && to === tabId) { - // 有 id 的消息, 这是这个模块请求返回回来的消息,不被其他模块处理。 - emitter.emit(responseId, event.data); - return; - } - - // console.log('channel message', event.data); - if (to === 'all' || to === tabId) { - const { path, key, payload, ...rest } = event.data; - if (!path) { - return; - } - app - .run({ - path, - key, - payload: { - ...rest, - ...payload, - }, - }) - .then((res) => { - const { data, message, code } = res; - if (requestId) { - if (code !== 999) { - console.log('channel response', requestId, res); - } - const msg = { - // 返回数据一般没有 requestId,如果有,在res中自己放置 - responseId: requestId, - to: source, - source: tabId, - data: data, - message, - code, - }; - if (data?.requestId) { - msg['requestId'] = data.requestId; - } - channel.postMessage(msg); - } - }) - .catch((err) => { - console.error(tabId, requestId, err); - }); - return; - } -}); - -const postAllTabs = (msg: Msg, callback?: (data: any[], error?: any[]) => any, timeout = 3 * 60 * 1000) => { - const action = { - ...msg, - to: 'all', - source: tabId, - }; - if (callback) { - const requestId = nanoid(8); - action.requestId = requestId; - const res: any = []; - const error: any = []; - emitter.on(requestId, (data) => { - res.push(data); - // console.log('postAllTabs callback', data, res.length, openTabs.size); - if (res.length >= openTabs.size - 1) { - callback(res); - emitter.off(requestId); - } - }); - setTimeout(() => { - const tabs = [...openTabs]; - tabs.forEach((tab) => { - if (tab === tabId) { - return; - } - if (!res.find((r) => r.source === tab)) { - error.push({ code: 500, message: 'timeout', tab, source: tab, to: tabId }); - } - }); - callback(res, error); - - emitter.off(requestId); - }, timeout); - } - channel.postMessage(action); -}; - -const postTab = (msg: Msg, callback?: (data: any) => any, timeout = 3 * 60 * 1000) => { - const action = { - ...msg, - to: msg.to, - source: tabId, - }; - if (callback) { - const requestId = nanoid(8); - action.requestId = requestId; - emitter.on(requestId, (data) => { - callback(data); - emitter.off(requestId); - }); - setTimeout(() => { - callback({ code: 500, message: 'timeout' }); - emitter.off(requestId); - }, timeout); - } - channel.postMessage(action); -}; -const channelFn = { - postAllTabs, - postTab, -}; - -useConfigKey('channelMsg', () => { - return channelFn; -}); - -const getTabs = (filter = true) => { - const allTabs = Object.keys(localStorage).filter((key) => key.startsWith('tab-')); - const tabs = allTabs.filter((key) => { - if (!filter) { - return true; - } - const tab = localStorage.getItem(key); - if (tab === tabId) { - return true; - } - localStorage.removeItem(key); - return false; - }); - return tabs; -}; - -const getOtherTabs = async () => { - const res = await app.run({ - path: 'channel', - key: 'postAllTabs', - payload: { - data: { - path: 'tab', - key: 'getConfig', - }, - hasResponse: true, - }, - }); - console.log('getOtherTabs', res); -}; - -// @ts-ignore -window.getOtherTabs = getOtherTabs; -const checkTabs = async () => { - postAllTabs({ path: 'tab', key: 'introduce', payload: { openTime: openTime, update: true } }); - await sleep(1000); - const tabs = getTabs(); - postAllTabs({ path: 'tab', key: 'setTabs', payload: { tabs: tabs.map((tab) => tab.replace('tab-', '')) } }); - openTabs.clear(); - tabs.forEach((tab) => { - openTabs.add(tab.replace('tab-', '')); - }); -}; -/** - * 初始化 tab - */ -const init = async () => { - localStorage.setItem('tab-' + tabId, tabId); - // 向其他 tab 发送消息, 页面已经打开 - postAllTabs({ path: 'tab', key: 'introduce', payload: { openTime: openTime } }); - window.addEventListener('beforeunload', () => { - // 向其他 tab 发送消息, 页面即将关闭 - localStorage.removeItem('tab-' + tabId); - console.log('close tab', tabId); - postAllTabs({ path: 'tab', key: 'close' }); - }); - introduceMe(); -}; -const introduceMe = () => { - if (meIsMax === '2') { - // 我知道我不是最大的了,我自己就不再发送消息了 - return; - } - postAllTabs({ path: 'tab', key: 'introduce', payload: { openTime: openTime } }); - if (!load) { - const time = Date.now() - openTime; - if (time > 2100) { - return; - } - setTimeout(() => { - introduceMe(); - }, 300); - } -}; -// umami.track('tab', { tabId: tabId }); - -window.onload = () => { - setTimeout(() => { - if (meIsMax === '1') { - console.log('I am max', openTime); - checkTabs(); - load = true; - } - }, 2000); -}; -init(); diff --git a/tsconfig.app.json b/tsconfig.app.json index f264f1c..4ddf55f 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -20,8 +20,8 @@ "jsxFactory": "h", "baseUrl": "./", "typeRoots": [ - "./node_modules/@types", - "./node_modules/@kevisual/types", + "node_modules/@types", + "node_modules/@kevisual/types", ], "paths": { "@/*": [