@yh-ui/hooks 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.localStorageAdapter = exports.IndexedDBAdapter = void 0;
7
+ exports.useAiConversations = useAiConversations;
8
+ var _vue = require("vue");
9
+ const localStorageAdapter = exports.localStorageAdapter = {
10
+ getItem: key => {
11
+ try {
12
+ return localStorage.getItem(key);
13
+ } catch {
14
+ return null;
15
+ }
16
+ },
17
+ setItem: (key, value) => {
18
+ try {
19
+ localStorage.setItem(key, value);
20
+ } catch {}
21
+ },
22
+ removeItem: key => {
23
+ try {
24
+ localStorage.removeItem(key);
25
+ } catch {}
26
+ }
27
+ };
28
+ class IndexedDBAdapter {
29
+ db = null;
30
+ dbName;
31
+ storeName = "ai_conversations";
32
+ ready;
33
+ constructor(dbName = "yh-ui-ai") {
34
+ this.dbName = dbName;
35
+ this.ready = this.init();
36
+ }
37
+ init() {
38
+ return new Promise((resolve, reject) => {
39
+ if (typeof indexedDB === "undefined") {
40
+ resolve();
41
+ return;
42
+ }
43
+ const req = indexedDB.open(this.dbName, 1);
44
+ req.onupgradeneeded = () => {
45
+ req.result.createObjectStore(this.storeName);
46
+ };
47
+ req.onsuccess = () => {
48
+ this.db = req.result;
49
+ resolve();
50
+ };
51
+ req.onerror = () => reject(req.error);
52
+ });
53
+ }
54
+ async getItem(key) {
55
+ await this.ready;
56
+ if (!this.db) return null;
57
+ return new Promise(resolve => {
58
+ const tx = this.db.transaction(this.storeName, "readonly");
59
+ const req = tx.objectStore(this.storeName).get(key);
60
+ req.onsuccess = () => resolve(req.result ?? null);
61
+ req.onerror = () => resolve(null);
62
+ });
63
+ }
64
+ async setItem(key, value) {
65
+ await this.ready;
66
+ if (!this.db) return;
67
+ return new Promise(resolve => {
68
+ const tx = this.db.transaction(this.storeName, "readwrite");
69
+ tx.objectStore(this.storeName).put(value, key);
70
+ tx.oncomplete = () => resolve();
71
+ });
72
+ }
73
+ async removeItem(key) {
74
+ await this.ready;
75
+ if (!this.db) return;
76
+ return new Promise(resolve => {
77
+ const tx = this.db.transaction(this.storeName, "readwrite");
78
+ tx.objectStore(this.storeName).delete(key);
79
+ tx.oncomplete = () => resolve();
80
+ });
81
+ }
82
+ }
83
+ exports.IndexedDBAdapter = IndexedDBAdapter;
84
+ function getGroupLabel(updatedAt) {
85
+ const now = Date.now();
86
+ const diff = now - updatedAt;
87
+ const oneDay = 864e5;
88
+ if (diff < oneDay) return "today";
89
+ if (diff < 7 * oneDay) return "last7Days";
90
+ if (diff < 30 * oneDay) return "last30Days";
91
+ return "earlier";
92
+ }
93
+ const GROUP_ORDER = ["today", "last7Days", "last30Days", "earlier"];
94
+ function useAiConversations(options = {}) {
95
+ const {
96
+ idGenerator = () => Math.random().toString(36).substring(2, 9),
97
+ storage = "localStorage",
98
+ storageKey = "yh-ui-ai-conversations",
99
+ pageSize = 20
100
+ } = options;
101
+ let adapter = null;
102
+ if (storage === "localStorage") {
103
+ adapter = localStorageAdapter;
104
+ } else if (storage === "indexedDB") {
105
+ adapter = new IndexedDBAdapter();
106
+ } else if (storage && typeof storage === "object") {
107
+ adapter = storage;
108
+ }
109
+ const conversations = (0, _vue.ref)([]);
110
+ const page = (0, _vue.ref)(1);
111
+ const isLoadingMore = (0, _vue.ref)(false);
112
+ const initPromise = (async () => {
113
+ let stored = [];
114
+ if (adapter) {
115
+ try {
116
+ const raw = await adapter.getItem(storageKey);
117
+ if (raw) stored = JSON.parse(raw);
118
+ } catch {
119
+ stored = [];
120
+ }
121
+ }
122
+ const init = options.initialConversations ?? [];
123
+ const merged = [...init];
124
+ for (const s of stored) {
125
+ if (!merged.find(c => c.id === s.id)) {
126
+ merged.push(s);
127
+ }
128
+ }
129
+ conversations.value = merged.sort((a, b) => {
130
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
131
+ return b.updatedAt - a.updatedAt;
132
+ });
133
+ })();
134
+ const persist = async () => {
135
+ if (!adapter) return;
136
+ try {
137
+ await adapter.setItem(storageKey, JSON.stringify(conversations.value));
138
+ } catch {}
139
+ };
140
+ const groupedConversations = (0, _vue.computed)(() => {
141
+ const groups = {
142
+ today: [],
143
+ last7Days: [],
144
+ last30Days: [],
145
+ earlier: []
146
+ };
147
+ for (const c of conversations.value) {
148
+ if (c.pinned) continue;
149
+ const key = getGroupLabel(c.updatedAt);
150
+ groups[key].push(c);
151
+ }
152
+ const result = [];
153
+ const pinned = conversations.value.filter(c => c.pinned);
154
+ if (pinned.length > 0) {
155
+ result.push({
156
+ label: "pinned",
157
+ items: pinned
158
+ });
159
+ }
160
+ for (const key of GROUP_ORDER) {
161
+ if (groups[key].length > 0) {
162
+ result.push({
163
+ label: key,
164
+ items: groups[key]
165
+ });
166
+ }
167
+ }
168
+ return result;
169
+ });
170
+ const pagedConversations = (0, _vue.computed)(() => {
171
+ return conversations.value.slice(0, page.value * pageSize);
172
+ });
173
+ const hasMore = (0, _vue.computed)(() => {
174
+ return pagedConversations.value.length < conversations.value.length;
175
+ });
176
+ const loadMore = async () => {
177
+ if (!hasMore.value || isLoadingMore.value) return;
178
+ isLoadingMore.value = true;
179
+ await new Promise(r => setTimeout(r, 300));
180
+ page.value++;
181
+ isLoadingMore.value = false;
182
+ };
183
+ const createConversation = async (title, meta) => {
184
+ const newConv = {
185
+ id: idGenerator(),
186
+ title,
187
+ updatedAt: Date.now(),
188
+ meta
189
+ };
190
+ conversations.value.unshift(newConv);
191
+ await persist();
192
+ return newConv;
193
+ };
194
+ const removeConversation = async id => {
195
+ const idx = conversations.value.findIndex(c => c.id === id);
196
+ if (idx !== -1) {
197
+ conversations.value.splice(idx, 1);
198
+ await persist();
199
+ }
200
+ };
201
+ const updateConversation = async (id, updates) => {
202
+ const idx = conversations.value.findIndex(c => c.id === id);
203
+ if (idx !== -1) {
204
+ conversations.value[idx] = {
205
+ ...conversations.value[idx],
206
+ ...updates,
207
+ updatedAt: Date.now()
208
+ };
209
+ await persist();
210
+ }
211
+ };
212
+ const pinConversation = async (id, pinned = true) => {
213
+ await updateConversation(id, {
214
+ pinned
215
+ });
216
+ conversations.value.sort((a, b) => {
217
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
218
+ return b.updatedAt - a.updatedAt;
219
+ });
220
+ await persist();
221
+ };
222
+ const clear = async () => {
223
+ conversations.value = [];
224
+ if (adapter) {
225
+ await adapter.removeItem(storageKey);
226
+ }
227
+ };
228
+ return {
229
+ /** 完整会话列表 */
230
+ conversations,
231
+ /** 按时间分组后的列表(置顶 / 今天 / 最近 7 天 / 更早) */
232
+ groupedConversations,
233
+ /** 分页后的列表 */
234
+ pagedConversations,
235
+ /** 是否还有更多数据 */
236
+ hasMore,
237
+ /** 加载更多 */
238
+ loadMore,
239
+ /** 加载更多状态 */
240
+ isLoadingMore,
241
+ /** 等待初始化完成(SSR 场景使用) */
242
+ ready: initPromise,
243
+ /** 新建会话 */
244
+ createConversation,
245
+ /** 删除会话 */
246
+ removeConversation,
247
+ /** 更新会话属性 */
248
+ updateConversation,
249
+ /** 置顶/取消置顶 */
250
+ pinConversation,
251
+ /** 清空全部 */
252
+ clear
253
+ };
254
+ }
@@ -0,0 +1,148 @@
1
+ export interface AiConversation {
2
+ id: string;
3
+ title: string;
4
+ /**
5
+ * 最近更新时间(时间戳 ms)
6
+ */
7
+ updatedAt: number;
8
+ /**
9
+ * 对话预览/最后一条消息内容
10
+ */
11
+ excerpt?: string;
12
+ /**
13
+ * 是否置顶
14
+ */
15
+ pinned?: boolean;
16
+ /**
17
+ * 自定义数据(例如知识库 ID 等业务数据)
18
+ */
19
+ meta?: Record<string, unknown>;
20
+ }
21
+ export interface StorageAdapter {
22
+ getItem(key: string): string | null | Promise<string | null>;
23
+ setItem(key: string, value: string): void | Promise<void>;
24
+ removeItem(key: string): void | Promise<void>;
25
+ }
26
+ /**
27
+ * localStorage 适配器(默认)
28
+ */
29
+ export declare const localStorageAdapter: StorageAdapter;
30
+ /**
31
+ * IndexedDB 适配器(适合大量数据持久化)
32
+ */
33
+ export declare class IndexedDBAdapter implements StorageAdapter {
34
+ private db;
35
+ private dbName;
36
+ private storeName;
37
+ private ready;
38
+ constructor(dbName?: string);
39
+ private init;
40
+ getItem(key: string): Promise<string | null>;
41
+ setItem(key: string, value: string): Promise<void>;
42
+ removeItem(key: string): Promise<void>;
43
+ }
44
+ export type ConversationGroup = {
45
+ label: string;
46
+ items: AiConversation[];
47
+ };
48
+ export interface UseAiConversationsOptions {
49
+ /** 初始化数据(或从后端的直出) */
50
+ initialConversations?: AiConversation[];
51
+ /** 自定义生成 ID 的函数 */
52
+ idGenerator?: () => string;
53
+ /**
54
+ * 持久化存储适配器
55
+ * - false: 不持久化(仅内存)
56
+ * - 'localStorage': 使用 localStorage(默认)
57
+ * - 'indexedDB': 使用 IndexedDB
58
+ * - StorageAdapter: 自定义适配器
59
+ */
60
+ storage?: false | 'localStorage' | 'indexedDB' | StorageAdapter;
61
+ /** 持久化 key 前缀 */
62
+ storageKey?: string;
63
+ /**
64
+ * 每次加载的数量(用于分页 / 懒加载)
65
+ * @default 20
66
+ */
67
+ pageSize?: number;
68
+ }
69
+ /**
70
+ * useAiConversations - 会话历史管理 Hook
71
+ *
72
+ * 特性:
73
+ * - 完整的 CRUD + 置顶操作
74
+ * - 可插拔持久化(localStorage / IndexedDB / 自定义)
75
+ * - 按时间自动分组(今天 / 最近 7 天 / 最近 30 天 / 更早)
76
+ * - 分页懒加载
77
+ */
78
+ export declare function useAiConversations(options?: UseAiConversationsOptions): {
79
+ /** 完整会话列表 */
80
+ conversations: import("vue").Ref<{
81
+ id: string;
82
+ title: string;
83
+ updatedAt: number;
84
+ excerpt?: string
85
+ /**
86
+ * 是否置顶
87
+ */
88
+ | undefined;
89
+ pinned?: boolean
90
+ /**
91
+ * 自定义数据(例如知识库 ID 等业务数据)
92
+ */
93
+ | undefined;
94
+ meta?: Record<string, unknown> | undefined;
95
+ }[], AiConversation[] | {
96
+ id: string;
97
+ title: string;
98
+ updatedAt: number;
99
+ excerpt?: string
100
+ /**
101
+ * 是否置顶
102
+ */
103
+ | undefined;
104
+ pinned?: boolean
105
+ /**
106
+ * 自定义数据(例如知识库 ID 等业务数据)
107
+ */
108
+ | undefined;
109
+ meta?: Record<string, unknown> | undefined;
110
+ }[]>;
111
+ /** 按时间分组后的列表(置顶 / 今天 / 最近 7 天 / 更早) */
112
+ groupedConversations: import("vue").ComputedRef<ConversationGroup[]>;
113
+ /** 分页后的列表 */
114
+ pagedConversations: import("vue").ComputedRef<{
115
+ id: string;
116
+ title: string;
117
+ updatedAt: number;
118
+ excerpt?: string
119
+ /**
120
+ * 是否置顶
121
+ */
122
+ | undefined;
123
+ pinned?: boolean
124
+ /**
125
+ * 自定义数据(例如知识库 ID 等业务数据)
126
+ */
127
+ | undefined;
128
+ meta?: Record<string, unknown> | undefined;
129
+ }[]>;
130
+ /** 是否还有更多数据 */
131
+ hasMore: import("vue").ComputedRef<boolean>;
132
+ /** 加载更多 */
133
+ loadMore: () => Promise<void>;
134
+ /** 加载更多状态 */
135
+ isLoadingMore: import("vue").Ref<boolean, boolean>;
136
+ /** 等待初始化完成(SSR 场景使用) */
137
+ ready: Promise<void>;
138
+ /** 新建会话 */
139
+ createConversation: (title: string, meta?: Record<string, unknown>) => Promise<AiConversation>;
140
+ /** 删除会话 */
141
+ removeConversation: (id: string) => Promise<void>;
142
+ /** 更新会话属性 */
143
+ updateConversation: (id: string, updates: Partial<Omit<AiConversation, "id">>) => Promise<void>;
144
+ /** 置顶/取消置顶 */
145
+ pinConversation: (id: string, pinned?: boolean) => Promise<void>;
146
+ /** 清空全部 */
147
+ clear: () => Promise<void>;
148
+ };
@@ -0,0 +1,241 @@
1
+ import { ref, computed } from "vue";
2
+ export const localStorageAdapter = {
3
+ getItem: (key) => {
4
+ try {
5
+ return localStorage.getItem(key);
6
+ } catch {
7
+ return null;
8
+ }
9
+ },
10
+ setItem: (key, value) => {
11
+ try {
12
+ localStorage.setItem(key, value);
13
+ } catch {
14
+ }
15
+ },
16
+ removeItem: (key) => {
17
+ try {
18
+ localStorage.removeItem(key);
19
+ } catch {
20
+ }
21
+ }
22
+ };
23
+ export class IndexedDBAdapter {
24
+ db = null;
25
+ dbName;
26
+ storeName = "ai_conversations";
27
+ ready;
28
+ constructor(dbName = "yh-ui-ai") {
29
+ this.dbName = dbName;
30
+ this.ready = this.init();
31
+ }
32
+ init() {
33
+ return new Promise((resolve, reject) => {
34
+ if (typeof indexedDB === "undefined") {
35
+ resolve();
36
+ return;
37
+ }
38
+ const req = indexedDB.open(this.dbName, 1);
39
+ req.onupgradeneeded = () => {
40
+ req.result.createObjectStore(this.storeName);
41
+ };
42
+ req.onsuccess = () => {
43
+ this.db = req.result;
44
+ resolve();
45
+ };
46
+ req.onerror = () => reject(req.error);
47
+ });
48
+ }
49
+ async getItem(key) {
50
+ await this.ready;
51
+ if (!this.db) return null;
52
+ return new Promise((resolve) => {
53
+ const tx = this.db.transaction(this.storeName, "readonly");
54
+ const req = tx.objectStore(this.storeName).get(key);
55
+ req.onsuccess = () => resolve(req.result ?? null);
56
+ req.onerror = () => resolve(null);
57
+ });
58
+ }
59
+ async setItem(key, value) {
60
+ await this.ready;
61
+ if (!this.db) return;
62
+ return new Promise((resolve) => {
63
+ const tx = this.db.transaction(this.storeName, "readwrite");
64
+ tx.objectStore(this.storeName).put(value, key);
65
+ tx.oncomplete = () => resolve();
66
+ });
67
+ }
68
+ async removeItem(key) {
69
+ await this.ready;
70
+ if (!this.db) return;
71
+ return new Promise((resolve) => {
72
+ const tx = this.db.transaction(this.storeName, "readwrite");
73
+ tx.objectStore(this.storeName).delete(key);
74
+ tx.oncomplete = () => resolve();
75
+ });
76
+ }
77
+ }
78
+ function getGroupLabel(updatedAt) {
79
+ const now = Date.now();
80
+ const diff = now - updatedAt;
81
+ const oneDay = 864e5;
82
+ if (diff < oneDay) return "today";
83
+ if (diff < 7 * oneDay) return "last7Days";
84
+ if (diff < 30 * oneDay) return "last30Days";
85
+ return "earlier";
86
+ }
87
+ const GROUP_ORDER = ["today", "last7Days", "last30Days", "earlier"];
88
+ export function useAiConversations(options = {}) {
89
+ const {
90
+ idGenerator = () => Math.random().toString(36).substring(2, 9),
91
+ storage = "localStorage",
92
+ storageKey = "yh-ui-ai-conversations",
93
+ pageSize = 20
94
+ } = options;
95
+ let adapter = null;
96
+ if (storage === "localStorage") {
97
+ adapter = localStorageAdapter;
98
+ } else if (storage === "indexedDB") {
99
+ adapter = new IndexedDBAdapter();
100
+ } else if (storage && typeof storage === "object") {
101
+ adapter = storage;
102
+ }
103
+ const conversations = ref([]);
104
+ const page = ref(1);
105
+ const isLoadingMore = ref(false);
106
+ const initPromise = (async () => {
107
+ let stored = [];
108
+ if (adapter) {
109
+ try {
110
+ const raw = await adapter.getItem(storageKey);
111
+ if (raw) stored = JSON.parse(raw);
112
+ } catch {
113
+ stored = [];
114
+ }
115
+ }
116
+ const init = options.initialConversations ?? [];
117
+ const merged = [...init];
118
+ for (const s of stored) {
119
+ if (!merged.find((c) => c.id === s.id)) {
120
+ merged.push(s);
121
+ }
122
+ }
123
+ conversations.value = merged.sort((a, b) => {
124
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
125
+ return b.updatedAt - a.updatedAt;
126
+ });
127
+ })();
128
+ const persist = async () => {
129
+ if (!adapter) return;
130
+ try {
131
+ await adapter.setItem(storageKey, JSON.stringify(conversations.value));
132
+ } catch {
133
+ }
134
+ };
135
+ const groupedConversations = computed(() => {
136
+ const groups = {
137
+ today: [],
138
+ last7Days: [],
139
+ last30Days: [],
140
+ earlier: []
141
+ };
142
+ for (const c of conversations.value) {
143
+ if (c.pinned) continue;
144
+ const key = getGroupLabel(c.updatedAt);
145
+ groups[key].push(c);
146
+ }
147
+ const result = [];
148
+ const pinned = conversations.value.filter((c) => c.pinned);
149
+ if (pinned.length > 0) {
150
+ result.push({ label: "pinned", items: pinned });
151
+ }
152
+ for (const key of GROUP_ORDER) {
153
+ if (groups[key].length > 0) {
154
+ result.push({ label: key, items: groups[key] });
155
+ }
156
+ }
157
+ return result;
158
+ });
159
+ const pagedConversations = computed(() => {
160
+ return conversations.value.slice(0, page.value * pageSize);
161
+ });
162
+ const hasMore = computed(() => {
163
+ return pagedConversations.value.length < conversations.value.length;
164
+ });
165
+ const loadMore = async () => {
166
+ if (!hasMore.value || isLoadingMore.value) return;
167
+ isLoadingMore.value = true;
168
+ await new Promise((r) => setTimeout(r, 300));
169
+ page.value++;
170
+ isLoadingMore.value = false;
171
+ };
172
+ const createConversation = async (title, meta) => {
173
+ const newConv = {
174
+ id: idGenerator(),
175
+ title,
176
+ updatedAt: Date.now(),
177
+ meta
178
+ };
179
+ conversations.value.unshift(newConv);
180
+ await persist();
181
+ return newConv;
182
+ };
183
+ const removeConversation = async (id) => {
184
+ const idx = conversations.value.findIndex((c) => c.id === id);
185
+ if (idx !== -1) {
186
+ conversations.value.splice(idx, 1);
187
+ await persist();
188
+ }
189
+ };
190
+ const updateConversation = async (id, updates) => {
191
+ const idx = conversations.value.findIndex((c) => c.id === id);
192
+ if (idx !== -1) {
193
+ conversations.value[idx] = {
194
+ ...conversations.value[idx],
195
+ ...updates,
196
+ updatedAt: Date.now()
197
+ };
198
+ await persist();
199
+ }
200
+ };
201
+ const pinConversation = async (id, pinned = true) => {
202
+ await updateConversation(id, { pinned });
203
+ conversations.value.sort((a, b) => {
204
+ if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
205
+ return b.updatedAt - a.updatedAt;
206
+ });
207
+ await persist();
208
+ };
209
+ const clear = async () => {
210
+ conversations.value = [];
211
+ if (adapter) {
212
+ await adapter.removeItem(storageKey);
213
+ }
214
+ };
215
+ return {
216
+ /** 完整会话列表 */
217
+ conversations,
218
+ /** 按时间分组后的列表(置顶 / 今天 / 最近 7 天 / 更早) */
219
+ groupedConversations,
220
+ /** 分页后的列表 */
221
+ pagedConversations,
222
+ /** 是否还有更多数据 */
223
+ hasMore,
224
+ /** 加载更多 */
225
+ loadMore,
226
+ /** 加载更多状态 */
227
+ isLoadingMore,
228
+ /** 等待初始化完成(SSR 场景使用) */
229
+ ready: initPromise,
230
+ /** 新建会话 */
231
+ createConversation,
232
+ /** 删除会话 */
233
+ removeConversation,
234
+ /** 更新会话属性 */
235
+ updateConversation,
236
+ /** 置顶/取消置顶 */
237
+ pinConversation,
238
+ /** 清空全部 */
239
+ clear
240
+ };
241
+ }