feat(assets): add chat typing indicator lottie animation

This commit is contained in:
2026-06-08 17:26:09 +08:00
parent f4e1c30051
commit 90fe892995
22 changed files with 988 additions and 3 deletions
+163
View File
@@ -0,0 +1,163 @@
"use client";
/**
* LocalChatStorage 完整实现(示范 2IndexedDB via Dexie
*
* 对齐 Dart 端 `LocalChatStorage`lib/data/services/storage/chat/local_chat_storage.dart):
* - init / saveMessage / saveMessages / getAllMessages / clearAll / deleteMessage / close
* - 额外提供 `getMessageCount`(异步)替代 Dart 的同步 `messageCount` getter
* - 额外提供 `getAllMessagesBySession(sessionId)`filter 模拟,等下一轮加索引后再用 where)
*
* 单例挂在 class 静态字段。
* Dexie 实例可注入,便于测试用 fake-indexeddb 跑真 Dexie。
*
* 注意:当前表无次级索引,所有过滤(sessionId)都是 `toArray().then(filter)`O(n)。
* 数据量大时考虑升级 schema 加索引。
*/
import { Result, type Result as ResultT } from "../result";
import { LocalChatDB } from "./local_chat_db";
import { LocalMessage } from "./local_message";
export class LocalChatStorage {
private readonly db: LocalChatDB;
private static _instance: LocalChatStorage | null = null;
constructor(db?: LocalChatDB) {
this.db = db ?? new LocalChatDB();
}
static getInstance(): LocalChatStorage {
if (!LocalChatStorage._instance) {
LocalChatStorage._instance = new LocalChatStorage();
}
return LocalChatStorage._instance;
}
/** @internal */
static _resetForTests(): void {
LocalChatStorage._instance = null;
}
/**
* 触发 Dexie 打开数据库。
* 幂等:Dexie.open() 多次调用无副作用。
* 在 bootstrap 中 await 此方法可保证后续操作不会因连接未开而延迟。
*/
async init(): Promise<ResultT<void>> {
try {
await this.db.open();
return Result.success(undefined);
} catch (e) {
return Result.failure(e);
}
}
// ---- create / append ----
async saveMessage(message: LocalMessage): Promise<ResultT<void>> {
try {
await this.db.messages.add(message.toRow());
return Result.success(undefined);
} catch (e) {
return Result.failure(e);
}
}
async saveMessages(messages: readonly LocalMessage[]): Promise<ResultT<void>> {
try {
await this.db.transaction(
"rw",
this.db.messages,
async () => {
for (const m of messages) {
await this.db.messages.add(m.toRow());
}
},
);
return Result.success(undefined);
} catch (e) {
return Result.failure(e);
}
}
// ---- read ----
async getAllMessages(): Promise<ResultT<LocalMessage[]>> {
try {
const rows = await this.db.messages.toArray();
// 按 dbId 升序(与 Dart `box.values.toList()` 插入序语义一致)
rows.sort((a, b) => (a.dbId ?? 0) - (b.dbId ?? 0));
return Result.success(rows.map((r) => LocalMessage.fromRow(r)));
} catch (e) {
return Result.failure(e);
}
}
async getMessageCount(): Promise<ResultT<number>> {
try {
return Result.success(await this.db.messages.count());
} catch (e) {
return Result.failure(e);
}
}
async getAllMessagesBySession(
sessionId: string,
): Promise<ResultT<LocalMessage[]>> {
try {
const rows = await this.db.messages
.filter((r) => r.sessionId === sessionId)
.toArray();
rows.sort((a, b) => (a.dbId ?? 0) - (b.dbId ?? 0));
return Result.success(rows.map((r) => LocalMessage.fromRow(r)));
} catch (e) {
return Result.failure(e);
}
}
// ---- delete ----
async clearAll(): Promise<ResultT<void>> {
try {
await this.db.messages.clear();
return Result.success(undefined);
} catch (e) {
return Result.failure(e);
}
}
/**
* 按数组下标删除(保持 Dart 端语义:`box.values.toList()[index].delete()`)。
* O(n) 取下标后单条 delete,与 Dart 行为一致。
*/
async deleteMessage(index: number): Promise<ResultT<void>> {
try {
if (!Number.isInteger(index) || index < 0) {
return Result.failure(
new RangeError(`deleteMessage: index ${index} out of range`),
);
}
const rows = await this.db.messages.toArray();
if (index >= rows.length) {
return Result.failure(
new RangeError(`deleteMessage: index ${index} out of range`),
);
}
const target = rows[index]!;
await this.db.messages.delete(target.dbId!);
return Result.success(undefined);
} catch (e) {
return Result.failure(e);
}
}
async close(): Promise<ResultT<void>> {
try {
this.db.close();
return Result.success(undefined);
} catch (e) {
return Result.failure(e);
}
}
}