diff --git a/package.json b/package.json index 9b27609..e10c647 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "dayjs": "^1.11.13", + "fuse.js": "^7.1.0", "highlight.js": "^11.11.1", "i18next": "^25.2.0", "i18next-browser-languagedetector": "^8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2419daa..4dec81d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -2069,6 +2072,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5342,6 +5349,8 @@ snapshots: function-bind@1.1.2: {} + fuse.js@7.1.0: {} + gensync@1.0.0-beta.2: {} get-east-asian-width@1.3.0: {} diff --git a/src/apps/ai-chat/query/chat.ts b/src/apps/ai-chat/query/chat.ts deleted file mode 100644 index f096990..0000000 --- a/src/apps/ai-chat/query/chat.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { query } from '@/modules/query'; -import { Query } from '@kevisual/query'; -import { DataOpts } from '@kevisual/query/query'; - -export class QueryChat { - query: Query; - - constructor({ query }: { query: Query }) { - this.query = query; - } - - getChatList(opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'get-chat-list', - }, - opts, - ); - } - - getChat(id: string, opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'get-chat', - data: { - id, - }, - }, - opts, - ); - } - updateChat(data: any, opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'update-chat', - data, - }, - opts, - ); - } - - deleteChat(id: string, opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'delete-chat', - data: { - id, - }, - }, - opts, - ); - } - /** - * 获取模型列表 - * @param opts - * @returns - */ - getModelList(data?: { usernames?: string[] }, opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'get-model-list', - data, - }, - opts, - ); - } - /** - * 聊天对话模型 - * @param data - * @param chatOpts - * @param opts - * @returns - */ - chat(data: ChatDataOpts, chatOpts: ChatOpts, opts?: DataOpts) { - const { username, model, group, getFull = true } = chatOpts; - if (!username || !model || !group) { - throw new Error('username, model, group is required'); - } - return this.query.post( - { - path: 'ai', - key: 'chat', - ...chatOpts, - getFull, - data, - }, - opts, - ); - } - clearConfigCache(opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'clear-cache', - }, - opts, - ); - } - /** - * 获取聊天使用情况 - * @param opts - * @returns - */ - getChatUsage(opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'get-chat-usage', - }, - opts, - ); - } - - /** - * 清除当前用户模型自己的统计 - * @param opts - * @returns - */ - clearSelfUsage(opts?: DataOpts) { - return this.query.post( - { - path: 'ai', - key: 'clear-chat-limit', - }, - opts, - ); - } -} -export type ChatDataOpts = { - id?: string; - title?: string; - messages?: any[]; - data?: any; - type?: 'temp' | 'keep' | string; -}; -export type ChatOpts = { - username: string; - model: string; - /** - * 获取完整消息回复 - */ - getFull?: boolean; - group: string; - /** - * openai的参数 - */ - options?: any; -}; diff --git a/src/apps/ai-chat/store/index.ts b/src/apps/ai-chat/store/index.ts index f428ffc..6480e16 100644 --- a/src/apps/ai-chat/store/index.ts +++ b/src/apps/ai-chat/store/index.ts @@ -5,11 +5,11 @@ import { query } from '@/modules/query'; import { useStore, BoundStore } from '@kevisual/store/react'; import { toast } from 'react-toastify'; import { ChastHistoryMessage } from './type'; -import { QueryChat } from '../query/chat'; import { CacheStore } from '@kevisual/cache/cache-store'; import { QueryMark } from '@/query/query-mark/query-mark'; +import { QueryApp as QueryAi } from '@/query/query-ai/query-ai'; export const queryMark = new QueryMark({ query: query, markType: 'chat' }); -export const queryChat = new QueryChat({ query }); +export const queryChat = new QueryAi({ query }); const dbName = 'chat'; export const store = useContextKey('store', () => { diff --git a/src/apps/ai-mark/index.tsx b/src/apps/ai-mark/index.tsx new file mode 100644 index 0000000..1d1b3fb --- /dev/null +++ b/src/apps/ai-mark/index.tsx @@ -0,0 +1,112 @@ +import { query } from '@/modules/query'; +import { QueryMark, Mark } from '@/query/query-mark/query-mark'; +import { useEffect, useRef, useState, useCallback } from 'react'; +import { Tooltip } from '@/components/a/tooltip'; +export const queryMark = new QueryMark({ query: query, markType: 'md' }); +import Fuse from 'fuse.js'; +import { debounce } from 'lodash-es'; +import { SquareArrowOutUpRight } from 'lucide-react'; + +export const App = () => { + const [list, setList] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const fuseRef = useRef>(null); + + // 实际执行搜索的函数 + const performSearch = (term: string) => { + if (!term.trim()) { + setSearchResults([]); + return; + } + + if (fuseRef.current) { + const results = fuseRef.current.search(term); + setSearchResults(results.map((result) => result.item)); + } + }; + + // 使用 useRef 和 useCallback 创建防抖搜索函数,确保它在组件生命周期中只创建一次 + const debouncedSearchRef = useRef( + debounce((term: string) => { + performSearch(term); + }, 1000), // 300毫秒的防抖延迟 + ); + + useEffect(() => { + init(); + + // 组件卸载时取消待执行的防抖函数 + return () => { + debouncedSearchRef.current.cancel(); + }; + }, []); + + const init = async () => { + const res = await queryMark.getMarkList(); + if (res.code === 200) { + const list = res.data?.list || []; + setList(list!); + const fuse = new Fuse(list, { + keys: ['title', 'description', 'tags', 'summary'], + }); + fuseRef.current = fuse; + const result = fuse.search('苏生不惑的博客'); + console.log(result); + } + }; + + const handleSearch = (term: string) => { + setSearchTerm(term); // 立即更新输入框的值 + debouncedSearchRef.current(term); // 使用防抖函数进行搜索 + }; + + const onOpen = (item: Mark) => { + const link = item.link; + window.open(link); + }; + + // 确定要显示的列表:有搜索内容时显示搜索结果,否则显示全部 + const displayList = searchTerm.trim() ? searchResults : list; + + return ( +
+
+ handleSearch(e.target.value)} + placeholder='搜索书签...' + className='w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300' + /> +
+
+ {displayList.length === 0 && searchTerm.trim() ? ( +
没有找到匹配的结果
+ ) : ( + displayList.map((item) => { + return ( +
+ +

+ {item.title || '-'} + + onOpen(item)} /> +

+
+

{item.summary}

+
+ {item.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ ); + }) + )} +
+
+ ); +}; diff --git a/src/apps/assistant-home/index.tsx b/src/apps/assistant-home/index.tsx new file mode 100644 index 0000000..5896a49 --- /dev/null +++ b/src/apps/assistant-home/index.tsx @@ -0,0 +1,33 @@ +import { Button } from '@/components/a/button.tsx'; +import { TextEditor } from '@kevisual/markdown-editor/tiptap/editor.ts'; +import { Select } from '@/components/a/select.tsx'; +import { useEffect, useRef, useState } from 'react'; +import { getSuggestionItems } from '../ai-chat/editor/suggestion/item'; +import { html2md } from '@kevisual/markdown-editor/tiptap/index.ts'; +import { chatId } from '../ai-chat/utils/uuid'; +import '../ai-chat/index.css'; +export const App = (props: any) => { + const ref = useRef(null); + const editorRef = useRef(null); + useEffect(() => { + if (ref.current) { + editorRef.current = new TextEditor(); + editorRef.current.createEditor(ref.current, { + items: getSuggestionItems(), + }); + } + return () => { + if (ref.current && editorRef.current) { + editorRef.current?.destroy?.(); + } + }; + }, []); + return ( +
+
+
+
+ +
+ ); +}; diff --git a/src/data/blogs/logger-life/0003-ai-token.md b/src/data/blogs/logger-life/0003-ai-token.md new file mode 100644 index 0000000..ffcfa48 --- /dev/null +++ b/src/data/blogs/logger-life/0003-ai-token.md @@ -0,0 +1,11 @@ +--- +share: private +title: "AI Token 记录" +date: 2025-05-25 01:30:00 +--- + +当使用 AI 的时候,如果需要做什么任务的话,其实感觉 token 的消耗数是非常的大的。如果想要消耗的不那么大,就必须要精致化的功能模块,代码就必须要细致。 + +比如指令和任务匹配,比如文本需要抽取。 + +为什么我会觉得消耗大呢,假如用户有一个 agent 任务,首先要分析用户的意图,要把所有要调用的函数都列出来给到ai,当ai返回结果的时候,还要把结果返回给用户,用户调用对应的函数,生成的结果又给到ai,ai再返回给用户。在n多次的循环中,就有很多的消耗。 diff --git a/src/lib/random.ts b/src/lib/random.ts index 8db5892..3dc9c1e 100644 --- a/src/lib/random.ts +++ b/src/lib/random.ts @@ -19,3 +19,8 @@ export const randomId = (number: number) => { const _letter = uuid(1); return `${_letter}${nanoid(number)}`; }; + +export const randomLetter = (number: number = 8, opts?: { before?: string; after?: string }) => { + const { before = '', after = '' } = opts || {}; + return `${before}${uuid(number)}${after}`; +}; diff --git a/src/modules/query.ts b/src/modules/query.ts index 722980b..9f5d906 100644 --- a/src/modules/query.ts +++ b/src/modules/query.ts @@ -1,5 +1,30 @@ +import { toast } from 'react-toastify'; import { Query, ClientQuery } from '@kevisual/query/query'; - +import { QueryLoginBrowser } from '@/query/query-login/query-login-browser'; +import { toastLogin } from './toast/ToastLogin'; export const query = new Query(); export const clientQuery = new ClientQuery(); + +export const queryLogin = new QueryLoginBrowser({ + query: query as any, +}); + +query.beforeRequest = queryLogin.beforeRequest.bind(queryLogin); +query.afterResponse = async (res, ctx) => { + const newRes = await queryLogin.run401Action(res, ctx, { + afterAlso401: () => { + toastLogin(); + }, + afterCheck: (res) => { + console.log('afterCheck', res); + if (res.code === 200) { + toast.success('刷新登陆信息'); + setTimeout(() => { + window.location.reload(); + }, 1000); + } + }, + }); + return newRes as any; +}; diff --git a/src/pages/index.astro b/src/pages/index.astro index 0ba1aa2..50c7ce6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,9 +1,9 @@ --- -import '../styles/global.css'; +import '@/styles/global.css'; import Blank from '@/components/html/blank.astro'; -import { App as AIChat } from '@/apps/ai-chat/index.tsx'; +import { App as AssistantHome } from '@/apps/assistant-home/index.tsx'; --- - + diff --git a/src/pages/mark/ai-chat.astro b/src/pages/mark/ai-chat.astro new file mode 100644 index 0000000..15a7ca3 --- /dev/null +++ b/src/pages/mark/ai-chat.astro @@ -0,0 +1,9 @@ +--- +import '@/styles/global.css'; +import Blank from '@/components/html/blank.astro'; +import { App as AIChat } from '@/apps/ai-chat/index.tsx'; +--- + + + + diff --git a/src/pages/mark/ai-mark.astro b/src/pages/mark/ai-mark.astro new file mode 100644 index 0000000..cb618d9 --- /dev/null +++ b/src/pages/mark/ai-mark.astro @@ -0,0 +1,9 @@ +--- +import '@/styles/global.css'; +import Blank from '@/components/html/blank.astro'; +import { App } from '@/apps/ai-mark'; +--- + + + + diff --git a/src/pages/test/2025/01-get-query.astro b/src/pages/test/2025/01-get-query.astro new file mode 100644 index 0000000..d5912ed --- /dev/null +++ b/src/pages/test/2025/01-get-query.astro @@ -0,0 +1,14 @@ +--- +import '@/styles/global.css'; +import Blank from '@/components/html/blank.astro'; +import { QueryMark } from '@/query/query-mark/query-mark'; +--- + + +
+

Get Query

+ +
+
diff --git a/src/query/query-ai/defines/ai.ts b/src/query/query-ai/defines/ai.ts index b143bed..d8ca83b 100644 --- a/src/query/query-ai/defines/ai.ts +++ b/src/query/query-ai/defines/ai.ts @@ -12,6 +12,27 @@ export type PostChat = { user?: string; }; +export type ChatDataOpts = { + id?: string; + title?: string; + messages?: any[]; + data?: any; + type?: 'temp' | 'keep' | string; +}; +export type ChatOpts = { + username: string; + model: string; + /** + * 获取完整消息回复 + */ + getFull?: boolean; + group: string; + /** + * openai的参数 + */ + options?: any; +}; + export const appDefine = QueryUtil.create({ chat: { path: 'ai', diff --git a/src/query/query-ai/query-ai.ts b/src/query/query-ai/query-ai.ts index 817c108..2ede31c 100644 --- a/src/query/query-ai/query-ai.ts +++ b/src/query/query-ai/query-ai.ts @@ -1,5 +1,5 @@ import { appDefine } from './defines/ai.ts'; -import { PostChat } from './defines/ai.ts'; +import { PostChat, ChatOpts, ChatDataOpts } from './defines/ai.ts'; import { BaseQuery, DataOpts, Query } from '@kevisual/query/query'; @@ -22,4 +22,80 @@ export class QueryApp extends BaseQuery userItem.user.username === username); + const user = localUserList.find((userItem) => userItem.user!.username === username); if (user) { this.storage.setItem('token', user.accessToken || ''); await this.beforeSetLoginUser({ accessToken: user.accessToken, refreshToken: user.refreshToken }); diff --git a/src/query/query-mark/query-mark.ts b/src/query/query-mark/query-mark.ts index 729499d..1d58692 100644 --- a/src/query/query-mark/query-mark.ts +++ b/src/query/query-mark/query-mark.ts @@ -143,7 +143,7 @@ export class QueryMark extends QueryMarkBase { this.markType = opts?.markType || 'simple'; } async getMarkList(search?: SearchOpts, opts?: DataOpts) { - return this.post({ key: 'list', ...search, markType: this.markType }, opts); + return this.post>({ key: 'list', ...search, markType: this.markType }, opts); } async updateMark(data: any, opts?: DataOpts) { if (!data.id) {