feat: 合并星图和聊天页面为 MainPage,响应式 GridRow 布局

- 新建 MainPage.ets,用 GridRow 响应式布局组合星图+聊天
- Index.ets 改为 2 Tab(TrulyMEM + 设置)
- GraphPage/ChatPage 接收 @Prop db 从父组件传入
- 恢复 README.md / README_EN.md 的特别鸣谢部分
This commit is contained in:
root
2026-04-28 13:53:06 +08:00
parent 7c2441682c
commit 2a42183aaa
119 changed files with 6459 additions and 1102 deletions

View File

@ -59,6 +59,17 @@ python trulymem_entry.py # 从源码
---
## 特别鸣谢
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — 学术指导
- [逝水秋生白](https://atomgit.com/cenber) — 架构支持
- anzhitinglan — 测试资源支持
- 崔莉萍老师 — 理论指导
- Annie — 专业指导
- 王梓沣、马悦华、隆梦婷 — 神经科学理论支持
---
## 许可证
GNU General Public License v3.0 (GPLv3)

View File

@ -59,6 +59,17 @@ First run → TUI login screen → create/sign in → press **F2** for API Key
---
## Special Thanks
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — Academic guidance
- [逝水秋生白](https://atomgit.com/cenber) — Architecture support
- anzhitinglan — Testing resource support
- 崔莉萍老师 — Theoretical guidance
- Annie — Professional guidance
- 王梓沣、马悦华、隆梦婷 — Neuroscience theory support
---
## License
GNU General Public License v3.0 (GPLv3)

View File

@ -2,9 +2,9 @@
"app": {
"bundleName": "com.trulymem.app",
"vendor": "trulymem",
"versionCode": 1000000,
"versionCode": 1000001,
"versionName": "1.0.0",
"icon": ":layered_image",
"label": ":app_name"
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<rect width="200" height="200" rx="30" fill="#6366f1"/>
<text x="100" y="130" font-family="Arial" font-size="80" fill="white" text-anchor="middle" font-weight="bold">T</text>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -1,19 +1,47 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.0.0.48",
"compileSdkVersion": "6.0.0.48"
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug"
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry"
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
},
{
"name": "trulymem-core",
"srcPath": "./trulymem-core",
"targets": [
{
"name": "default"
}
]
}
]
}

View File

@ -0,0 +1,32 @@
{
"files": [
"**/*.ets"
],
"ignore": [
"**/src/ohosTest/**/*",
"**/src/test/**/*",
"**/src/mock/**/*",
"**/node_modules/**/*",
"**/oh_modules/**/*",
"**/build/**/*",
"**/.preview/**/*"
],
"ruleSet": [
"plugin:@performance/recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@security/no-unsafe-aes": "error",
"@security/no-unsafe-hash": "error",
"@security/no-unsafe-mac": "warn",
"@security/no-unsafe-dh": "error",
"@security/no-unsafe-dsa": "error",
"@security/no-unsafe-ecdsa": "error",
"@security/no-unsafe-rsa-encrypt": "error",
"@security/no-unsafe-rsa-sign": "error",
"@security/no-unsafe-rsa-key": "error",
"@security/no-unsafe-dsa-key": "error",
"@security/no-unsafe-dh-key": "error",
"@security/no-unsafe-3des": "error"
}
}

View File

@ -1,10 +1,38 @@
{
"apiType": "stageMode",
"buildOption": {},
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true,
"files": [
"./obfuscation-rules.txt"
]
}
}
},
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
],
"targets": [
{
"name": "default",
"runtimeOS": "HarmonyOS"
"name": "default"
},
{
"name": "ohosTest",
}
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildMode":"Debug"}

View File

@ -0,0 +1 @@
{"hspPkgNames":[],"compileEntries":["&entry/build/generated/r/ResourceTable&","&entry/src/main/ets/entryability/EntryAbility&","&entry/src/main/ets/pages/Index&"],"updateVersionInfo":{}}

View File

@ -0,0 +1 @@
{"resolveConflictMode":true}

View File

@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=ResourceTable.js.map

View File

@ -0,0 +1,30 @@
import type AbilityConstant from "@ohos:app.ability.AbilityConstant";
import UIAbility from "@ohos:app.ability.UIAbility";
import type Window from "@ohos:window";
import type Want from "@ohos:app.ability.Want";
export default class EntryAbility extends UIAbility {
onCreate(want: Want, param: AbilityConstant.LaunchParam): void {
console.info('EntryAbility onCreate');
}
onDestroy(): void {
console.info('EntryAbility onDestroy');
}
onWindowStageCreate(windowStage: Window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error('Failed to load content: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading content: ' + JSON.stringify(data));
});
}
onWindowStageDestroy(): void {
console.info('EntryAbility onWindowStageDestroy');
}
onForeground(): void {
console.info('EntryAbility onForeground');
}
onBackground(): void {
console.info('EntryAbility onBackground');
}
}

View File

@ -0,0 +1,222 @@
import relationalStore from "@ohos:data.relationalStore";
import type { Context } from "@ohos:abilityAccessCtrl";
interface TripletData {
subject: string;
relation: string;
object: string;
}
interface CriteriaData {
subject?: string;
target?: string;
relation?: string;
sessionId?: string;
}
interface NodeData {
id: number;
label: string;
type: string;
mentions: number;
}
interface EdgeData {
from: number;
to: number;
label: string;
weight: number;
}
interface GraphData {
nodes: NodeData[];
edges: EdgeData[];
}
interface RecallResult {
data: GraphData;
}
interface ChatMessage {
role: string;
content: string;
}
interface CleanupResult {
cleaned: number;
}
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'trulymem.db',
securityLevel: relationalStore.SecurityLevel.S1
};
export class GraphDatabase {
private store?: relationalStore.RdbStore;
private context?: Context;
async init(context: Context): Promise<void> {
this.context = context;
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
await this.createTables();
}
private async createTables(): Promise<void> {
if (!this.store)
return;
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'concept',
mentions INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
`);
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id INTEGER NOT NULL,
relation TEXT NOT NULL,
object_id INTEGER NOT NULL,
weight REAL DEFAULT 1.0,
created_at TEXT
)
`);
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS chat_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
tools TEXT,
created_at TEXT
)
`);
}
async commit(triplets: TripletData[]): Promise<void> {
if (!this.store)
return;
for (const triplet of triplets) {
const subjectId: number = await this.upsertNode(triplet.subject);
const objectId: number = await this.upsertNode(triplet.object);
const bucket: relationalStore.ValuesBucket = {
'subject_id': subjectId,
'relation': triplet.relation,
'object_id': objectId,
'created_at': new Date().toISOString()
};
await this.store.insert('relations', bucket);
}
}
private async upsertNode(name: string): Promise<number> {
if (!this.store)
return -1;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('name', name);
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
if (resultSet.goToNextRow()) {
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
resultSet.close();
return id;
}
resultSet.close();
const bucket: relationalStore.ValuesBucket = {
'name': name,
'type': 'concept',
'created_at': new Date().toISOString(),
'updated_at': new Date().toISOString()
};
return await this.store.insert('nodes', bucket);
}
async recall(queryIntent: string, seedEntities?: string[]): Promise<RecallResult> {
if (!this.store) {
const empty: RecallResult = { data: { nodes: [], edges: [] } };
return empty;
}
const nodes: NodeData[] = await this.getAllNodes();
const edges: EdgeData[] = await this.getAllEdges();
const result: RecallResult = { data: { nodes, edges } };
return result;
}
async purge(criteria: CriteriaData): Promise<void> {
if (!this.store)
return;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
await this.store.delete(predicates);
}
async introspect(): Promise<RecallResult> {
const nodes: NodeData[] = await this.getAllNodes();
const edges: EdgeData[] = await this.getAllEdges();
const result: RecallResult = { data: { nodes, edges } };
return result;
}
async archive(days: number): Promise<void> {
// Archive old data - placeholder
}
async cleanup(dryRun: boolean): Promise<CleanupResult> {
const result: CleanupResult = { cleaned: 0 };
return result;
}
async saveChatMessage(role: string, content: string, tools?: string): Promise<void> {
if (!this.store)
return;
const bucket: relationalStore.ValuesBucket = {
'role': role,
'content': content,
'tools': tools || null,
'created_at': new Date().toISOString()
};
await this.store.insert('chat_records', bucket);
}
async getChatHistory(limit?: number): Promise<ChatMessage[]> {
if (!this.store)
return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
predicates.orderByDesc('created_at');
if (limit) {
predicates.limitAs(limit);
}
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content']);
const messages: ChatMessage[] = [];
while (resultSet.goToNextRow()) {
const msg: ChatMessage = {
role: resultSet.getString(resultSet.getColumnIndex('role')),
content: resultSet.getString(resultSet.getColumnIndex('content'))
};
messages.push(msg);
}
resultSet.close();
return messages.reverse();
}
async clearChatHistory(): Promise<void> {
if (!this.store)
return;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
await this.store.delete(predicates);
}
private async getAllNodes(): Promise<NodeData[]> {
if (!this.store)
return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
const nodes: NodeData[] = [];
while (resultSet.goToNextRow()) {
const node: NodeData = {
id: resultSet.getLong(resultSet.getColumnIndex('id')),
label: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
};
nodes.push(node);
}
resultSet.close();
return nodes;
}
private async getAllEdges(): Promise<EdgeData[]> {
if (!this.store)
return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
const edges: EdgeData[] = [];
while (resultSet.goToNextRow()) {
const edge: EdgeData = {
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
label: resultSet.getString(resultSet.getColumnIndex('relation')),
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
};
edges.push(edge);
}
resultSet.close();
return edges;
}
}

View File

@ -0,0 +1,205 @@
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface ChatPage_Params {
messages?: ChatMessage[];
inputText?: string;
db?: GraphDatabase;
scrollController?: Scroller;
}
import http from "@ohos:net.http";
import dataPreferences from "@ohos:data.preferences";
import type { GraphDatabase } from '../model/GraphDatabase';
interface ChatMessage {
role: string;
content: string;
}
export class ChatPage extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__messages = new ObservedPropertyObjectPU([], this, "messages");
this.__inputText = new ObservedPropertySimplePU('', this, "inputText");
this.__db = new SynchedPropertyObjectOneWayPU(params.db, this, "db");
this.scrollController = new Scroller();
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: ChatPage_Params) {
if (params.messages !== undefined) {
this.messages = params.messages;
}
if (params.inputText !== undefined) {
this.inputText = params.inputText;
}
if (params.scrollController !== undefined) {
this.scrollController = params.scrollController;
}
}
updateStateVars(params: ChatPage_Params) {
this.__db.reset(params.db);
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__messages.purgeDependencyOnElmtId(rmElmtId);
this.__inputText.purgeDependencyOnElmtId(rmElmtId);
this.__db.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__messages.aboutToBeDeleted();
this.__inputText.aboutToBeDeleted();
this.__db.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private __messages: ObservedPropertyObjectPU<ChatMessage[]>;
get messages() {
return this.__messages.get();
}
set messages(newValue: ChatMessage[]) {
this.__messages.set(newValue);
}
private __inputText: ObservedPropertySimplePU<string>;
get inputText() {
return this.__inputText.get();
}
set inputText(newValue: string) {
this.__inputText.set(newValue);
}
private __db: SynchedPropertySimpleOneWayPU<GraphDatabase>;
get db() {
return this.__db.get();
}
set db(newValue: GraphDatabase) {
this.__db.set(newValue);
}
private scrollController: Scroller;
async aboutToAppear() {
this.messages = await this.db.getChatHistory(50);
}
async sendMessage() {
if (!this.inputText.trim())
return;
const userMessage: string = this.inputText;
this.inputText = '';
await this.db.saveChatMessage('user', userMessage);
const userMsg: ChatMessage = { role: 'user', content: userMessage };
this.messages = [...this.messages, userMsg];
const pref: dataPreferences.Preferences = await dataPreferences.getPreferences(getContext(this), 'trulymem_config');
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
const model: string = String(await pref.get('model', 'deepseek-chat'));
const apiKey: string = String(await pref.get('api_key', ''));
try {
const httpRequest: http.HttpRequest = http.createHttp();
const response: http.HttpResponse = await httpRequest.request(baseUrl + '/chat/completions', {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
extraData: {
model: model,
messages: [{ role: 'user', content: userMessage }]
}
});
if (response.responseCode === 200) {
const data: object = JSON.parse(String(response.result));
const choicesArr: object[] = data['choices'] as object[];
if (choicesArr && choicesArr.length > 0) {
const firstChoice: object = choicesArr[0];
const msgObj: object = firstChoice['message'] as object;
const assistantMessage: string = String(msgObj['content']);
await this.db.saveChatMessage('assistant', assistantMessage);
const aiMsg: ChatMessage = { role: 'assistant', content: assistantMessage };
this.messages = [...this.messages, aiMsg];
}
}
}
catch (err) {
console.error('HTTP request failed: ' + JSON.stringify(err));
}
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
Column.width('100%');
Column.height('100%');
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
List.create();
List.width('100%');
List.layoutWeight(1);
}, List);
this.observeComponentCreation2((elmtId, isInitialRender) => {
ForEach.create();
const forEachItemGenFunction = _item => {
const msg = _item;
{
const itemCreation = (elmtId, isInitialRender) => {
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
ListItem.create(deepRenderFunction, true);
if (!isInitialRender) {
ListItem.pop();
}
ViewStackProcessor.StopGetAccessRecording();
};
const itemCreation2 = (elmtId, isInitialRender) => {
ListItem.create(deepRenderFunction, true);
};
const deepRenderFunction = (elmtId, isInitialRender) => {
itemCreation(elmtId, isInitialRender);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
Column.padding(12);
Column.backgroundColor(msg.role === 'user' ? '#E3F2FD' : '#F5F5F5');
Column.borderRadius(8);
Column.margin({ left: 8, right: 8, bottom: 8 });
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create(msg.role);
Text.fontSize(12);
Text.fontColor('#666');
Text.width('100%');
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create(msg.content);
Text.fontSize(16);
Text.width('100%');
Text.margin({ top: 4 });
}, Text);
Text.pop();
Column.pop();
ListItem.pop();
};
this.observeComponentCreation2(itemCreation2, ListItem);
ListItem.pop();
}
};
this.forEachUpdateFunction(elmtId, this.messages, forEachItemGenFunction);
}, ForEach);
ForEach.pop();
List.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Row.create();
Row.width('100%');
Row.padding(8);
}, Row);
this.observeComponentCreation2((elmtId, isInitialRender) => {
TextArea.create({ text: this.inputText, placeholder: '输入消息...' });
TextArea.layoutWeight(1);
TextArea.onChange((v: string) => { this.inputText = v; });
}, TextArea);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Button.createWithLabel('发送');
Button.onClick(() => this.sendMessage());
}, Button);
Button.pop();
Row.pop();
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}

View File

@ -0,0 +1,65 @@
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface GraphPage_Params {
controller?: web_webview.WebviewController;
db?: GraphDatabase;
}
import web_webview from "@ohos:web.webview";
import type { GraphDatabase } from '../model/GraphDatabase';
export class GraphPage extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.controller = new web_webview.WebviewController();
this.__db = new SynchedPropertyObjectOneWayPU(params.db, this, "db");
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: GraphPage_Params) {
if (params.controller !== undefined) {
this.controller = params.controller;
}
}
updateStateVars(params: GraphPage_Params) {
this.__db.reset(params.db);
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__db.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__db.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private controller: web_webview.WebviewController;
private __db: SynchedPropertySimpleOneWayPU<GraphDatabase>;
get db() {
return this.__db.get();
}
set db(newValue: GraphDatabase) {
this.__db.set(newValue);
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
Column.width('100%');
Column.height('100%');
Column.onAppear(() => {
this.controller.refresh();
});
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Web.create({ src: { "id": 0, "type": 30000, params: ['graph.html'], "bundleName": "com.trulymem.app", "moduleName": "entry" }, controller: this.controller });
Web.javaScriptAccess(true);
Web.width('100%');
Web.height('100%');
}, Web);
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}

View File

@ -0,0 +1,119 @@
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface Index_Params {
currentIndex?: number;
db?: GraphDatabase;
}
import { MainPage } from "@normalized:N&&&entry/src/main/ets/pages/MainPage&";
import { SettingsPage } from "@normalized:N&&&entry/src/main/ets/pages/SettingsPage&";
import { GraphDatabase } from "@normalized:N&&&entry/src/main/ets/model/GraphDatabase&";
class Index extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__currentIndex = new ObservedPropertySimplePU(0, this, "currentIndex");
this.db = new GraphDatabase();
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: Index_Params) {
if (params.currentIndex !== undefined) {
this.currentIndex = params.currentIndex;
}
if (params.db !== undefined) {
this.db = params.db;
}
}
updateStateVars(params: Index_Params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__currentIndex.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__currentIndex.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private __currentIndex: ObservedPropertySimplePU<number>;
get currentIndex() {
return this.__currentIndex.get();
}
set currentIndex(newValue: number) {
this.__currentIndex.set(newValue);
}
private db: GraphDatabase;
aboutToAppear() {
this.db.init(getContext(this));
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
Column.width('100%');
Column.height('100%');
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Tabs.create({ index: this.currentIndex, barPosition: BarPosition.End });
Tabs.width('100%');
Tabs.height('100%');
Tabs.onChange((index: number) => { this.currentIndex = index; });
}, Tabs);
this.observeComponentCreation2((elmtId, isInitialRender) => {
TabContent.create(() => {
{
this.observeComponentCreation2((elmtId, isInitialRender) => {
if (isInitialRender) {
let componentCall = new MainPage(this, { db: this.db }, undefined, elmtId, () => { }, { page: "entry/src/main/ets/pages/Index.ets", line: 19, col: 21 });
ViewPU.create(componentCall);
let paramsLambda = () => {
return {
db: this.db
};
};
componentCall.paramsGenerator_ = paramsLambda;
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {
db: this.db
});
}
}, { name: "MainPage" });
}
});
TabContent.tabBar('🌌 TrulyMEM');
}, TabContent);
TabContent.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
TabContent.create(() => {
{
this.observeComponentCreation2((elmtId, isInitialRender) => {
if (isInitialRender) {
let componentCall = new SettingsPage(this, {}, undefined, elmtId, () => { }, { page: "entry/src/main/ets/pages/Index.ets", line: 24, col: 21 });
ViewPU.create(componentCall);
let paramsLambda = () => {
return {};
};
componentCall.paramsGenerator_ = paramsLambda;
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {});
}
}, { name: "SettingsPage" });
}
});
TabContent.tabBar('⚙ 设置');
}, TabContent);
TabContent.pop();
Tabs.pop();
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
static getEntryName(): string {
return "Index";
}
}
registerNamedRoute(() => new Index(undefined, {}), "", { bundleName: "com.trulymem.app", moduleName: "entry", pagePath: "pages/Index", pageFullPath: "entry/src/main/ets/pages/Index", integratedHsp: "false", moduleType: "followWithHap" });

View File

@ -0,0 +1,97 @@
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface MainPage_Params {
db?: GraphDatabase;
}
import type { GraphDatabase } from '../model/GraphDatabase';
import { GraphPage } from "@normalized:N&&&entry/src/main/ets/pages/GraphPage&";
import { ChatPage } from "@normalized:N&&&entry/src/main/ets/pages/ChatPage&";
export class MainPage extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__db = new SynchedPropertyObjectOneWayPU(params.db, this, "db");
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: MainPage_Params) {
}
updateStateVars(params: MainPage_Params) {
this.__db.reset(params.db);
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__db.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__db.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private __db: SynchedPropertySimpleOneWayPU<GraphDatabase>;
get db() {
return this.__db.get();
}
set db(newValue: GraphDatabase) {
this.__db.set(newValue);
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
GridRow.create({ columns: { sm: 1, md: 2, lg: 2 }, gutter: { x: 8, y: 8 } });
GridRow.width('100%');
GridRow.height('100%');
}, GridRow);
this.observeComponentCreation2((elmtId, isInitialRender) => {
GridCol.create({ span: { sm: 1, md: 1, lg: 1 } });
}, GridCol);
{
this.observeComponentCreation2((elmtId, isInitialRender) => {
if (isInitialRender) {
let componentCall = new GraphPage(this, { db: this.db }, undefined, elmtId, () => { }, { page: "entry/src/main/ets/pages/MainPage.ets", line: 12, col: 17 });
ViewPU.create(componentCall);
let paramsLambda = () => {
return {
db: this.db
};
};
componentCall.paramsGenerator_ = paramsLambda;
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {
db: this.db
});
}
}, { name: "GraphPage" });
}
GridCol.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
GridCol.create({ span: { sm: 1, md: 1, lg: 1 } });
}, GridCol);
{
this.observeComponentCreation2((elmtId, isInitialRender) => {
if (isInitialRender) {
let componentCall = new ChatPage(this, { db: this.db }, undefined, elmtId, () => { }, { page: "entry/src/main/ets/pages/MainPage.ets", line: 15, col: 17 });
ViewPU.create(componentCall);
let paramsLambda = () => {
return {
db: this.db
};
};
componentCall.paramsGenerator_ = paramsLambda;
}
else {
this.updateStateVarsOfChildByElmtId(elmtId, {
db: this.db
});
}
}, { name: "ChatPage" });
}
GridCol.pop();
GridRow.pop();
}
rerender() {
this.updateDirtyElements();
}
}

View File

@ -0,0 +1,152 @@
if (!("finalizeConstruction" in ViewPU.prototype)) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
}
interface SettingsPage_Params {
baseUrl?: string;
model?: string;
apiKey?: string;
pref?: dataPreferences.Preferences;
}
import dataPreferences from "@ohos:data.preferences";
export class SettingsPage extends ViewPU {
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
super(parent, __localStorage, elmtId, extraInfo);
if (typeof paramsLambda === "function") {
this.paramsGenerator_ = paramsLambda;
}
this.__baseUrl = new ObservedPropertySimplePU('', this, "baseUrl");
this.__model = new ObservedPropertySimplePU('', this, "model");
this.__apiKey = new ObservedPropertySimplePU('', this, "apiKey");
this.pref = undefined;
this.setInitiallyProvidedValue(params);
this.finalizeConstruction();
}
setInitiallyProvidedValue(params: SettingsPage_Params) {
if (params.baseUrl !== undefined) {
this.baseUrl = params.baseUrl;
}
if (params.model !== undefined) {
this.model = params.model;
}
if (params.apiKey !== undefined) {
this.apiKey = params.apiKey;
}
if (params.pref !== undefined) {
this.pref = params.pref;
}
}
updateStateVars(params: SettingsPage_Params) {
}
purgeVariableDependenciesOnElmtId(rmElmtId) {
this.__baseUrl.purgeDependencyOnElmtId(rmElmtId);
this.__model.purgeDependencyOnElmtId(rmElmtId);
this.__apiKey.purgeDependencyOnElmtId(rmElmtId);
}
aboutToBeDeleted() {
this.__baseUrl.aboutToBeDeleted();
this.__model.aboutToBeDeleted();
this.__apiKey.aboutToBeDeleted();
SubscriberManager.Get().delete(this.id__());
this.aboutToBeDeletedInternal();
}
private __baseUrl: ObservedPropertySimplePU<string>;
get baseUrl() {
return this.__baseUrl.get();
}
set baseUrl(newValue: string) {
this.__baseUrl.set(newValue);
}
private __model: ObservedPropertySimplePU<string>;
get model() {
return this.__model.get();
}
set model(newValue: string) {
this.__model.set(newValue);
}
private __apiKey: ObservedPropertySimplePU<string>;
get apiKey() {
return this.__apiKey.get();
}
set apiKey(newValue: string) {
this.__apiKey.set(newValue);
}
private pref?: dataPreferences.Preferences;
async aboutToAppear() {
const ctx = getContext(this);
this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
this.baseUrl = String(await this.pref.get('base_url', 'https://api.deepseek.com'));
this.model = String(await this.pref.get('model', 'deepseek-chat'));
this.apiKey = String(await this.pref.get('api_key', ''));
}
async onBaseUrlChange(value: string) {
this.baseUrl = value;
await this.pref?.put('base_url', value);
await this.pref?.flush();
}
async onModelChange(value: string) {
this.model = value;
await this.pref?.put('model', value);
await this.pref?.flush();
}
async onApiKeyChange(value: string) {
this.apiKey = value;
await this.pref?.put('api_key', value);
await this.pref?.flush();
}
initialRender() {
this.observeComponentCreation2((elmtId, isInitialRender) => {
Column.create();
Column.width('100%');
Column.height('100%');
Column.padding(16);
}, Column);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('API 配置');
Text.fontSize(24);
Text.fontWeight(FontWeight.Bold);
Text.margin({ top: 20, bottom: 16 });
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('Base URL');
Text.fontSize(14);
Text.width('100%');
Text.margin({ left: 16 });
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
TextInput.create({ placeholder: 'https://api.deepseek.com', text: this.baseUrl });
TextInput.onChange((v: string) => { this.onBaseUrlChange(v); });
TextInput.margin({ left: 16, right: 16, bottom: 12 });
}, TextInput);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('Model ID');
Text.fontSize(14);
Text.width('100%');
Text.margin({ left: 16 });
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
TextInput.create({ placeholder: 'deepseek-chat', text: this.model });
TextInput.onChange((v: string) => { this.onModelChange(v); });
TextInput.margin({ left: 16, right: 16, bottom: 12 });
}, TextInput);
this.observeComponentCreation2((elmtId, isInitialRender) => {
Text.create('API Key');
Text.fontSize(14);
Text.width('100%');
Text.margin({ left: 16 });
}, Text);
Text.pop();
this.observeComponentCreation2((elmtId, isInitialRender) => {
TextInput.create({ placeholder: 'sk-...', text: this.apiKey });
TextInput.type(InputType.Password);
TextInput.onChange((v: string) => { this.onApiKeyChange(v); });
TextInput.margin({ left: 16, right: 16, bottom: 12 });
}, TextInput);
Column.pop();
}
rerender() {
this.updateDirtyElements();
}
}

View File

@ -0,0 +1,8 @@
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/build/default/generated/r/default/ResourceTable.js;&entry/build/generated/r/ResourceTable&;esm;entry|entry|1.0.0|build/default/generated/r/default/ResourceTable.js;entry;false;ts
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/entryability/EntryAbility.ts;&entry/src/main/ets/entryability/EntryAbility&;esm;entry|entry|1.0.0|src/main/ets/entryability/EntryAbility.ts;entry;false;ets
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/Index.ts;&entry/src/main/ets/pages/Index&;esm;entry|entry|1.0.0|src/main/ets/pages/Index.ts;entry;false;ets
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/MainPage.ts;&entry/src/main/ets/pages/MainPage&;esm;entry|entry|1.0.0|src/main/ets/pages/MainPage.ts;entry;false;ets
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/model/GraphDatabase.ts;&entry/src/main/ets/model/GraphDatabase&;esm;entry|entry|1.0.0|src/main/ets/model/GraphDatabase.ts;entry;false;ets
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/SettingsPage.ts;&entry/src/main/ets/pages/SettingsPage&;esm;entry|entry|1.0.0|src/main/ets/pages/SettingsPage.ts;entry;false;ets
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/GraphPage.ts;&entry/src/main/ets/pages/GraphPage&;esm;entry|entry|1.0.0|src/main/ets/pages/GraphPage.ts;entry;false;ets
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/ChatPage.ts;&entry/src/main/ets/pages/ChatPage&;esm;entry|entry|1.0.0|src/main/ets/pages/ChatPage.ts;entry;false;ets

View File

@ -0,0 +1,9 @@
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/build/default/generated/r/default/ResourceTable.js;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/build/default/generated/r/default/ResourceTable.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/entryability/EntryAbility.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/entryability/EntryAbility.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/Index.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/Index.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/MainPage.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/MainPage.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/model/GraphDatabase.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/model/GraphDatabase.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/SettingsPage.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/SettingsPage.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/GraphPage.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/GraphPage.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/ChatPage.ts;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/ChatPage.protoBin
/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.txt;/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.protoBin

View File

@ -0,0 +1,7 @@
@system.app:@native.system.app
@ohos.app:@native.ohos.app
@system.router:@native.system.router
@system.curves:@native.system.curves
@ohos.curves:@native.ohos.curves
@system.matrix4:@native.system.matrix4
@ohos.matrix4:@native.ohos.matrix4

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const BUNDLE_NAME = 'com.trulymem.app';
export const BUNDLE_TYPE = 'app';
export const VERSION_CODE = 1000001;
export const VERSION_NAME = '1.0.0';
export const TARGET_NAME = 'default';
export const PRODUCT_NAME = 'default';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly BUNDLE_NAME = BUNDLE_NAME;
static readonly BUNDLE_TYPE = BUNDLE_TYPE;
static readonly VERSION_CODE = VERSION_CODE;
static readonly VERSION_NAME = VERSION_NAME;
static readonly TARGET_NAME = TARGET_NAME;
static readonly PRODUCT_NAME = PRODUCT_NAME;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef RESOURCE_TABLE_H
#define RESOURCE_TABLE_H
#include<stdint.h>
namespace OHOS {
const int32_t STRING_ENTRYABILITY_DESC = 0x01000001;
const int32_t STRING_ENTRYABILITY_LABEL = 0x01000002;
const int32_t STRING_APP_NAME = 0x01000003;
const int32_t STRING_MODULE_DESC = 0x01000004;
const int32_t COLOR_START_WINDOW_BACKGROUND = 0x01000005;
const int32_t MEDIA_LAYERED_IMAGE = 0x01000000;
const int32_t MEDIA_STARTICON = 0x01000006;
const int32_t PROFILE_MAIN_PAGES = 0x01000007;
}
#endif

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//@ts-noCheck

View File

@ -0,0 +1 @@
[{"hapName":"entry-default-unsigned.hap","deviceTypes":["phone","tablet","2in1"],"isSigned":false}]

View File

@ -0,0 +1,26 @@
{
"modulePathMap": {
"entry": "/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry",
"trulymem-core": "/home/program/TrulyMEM-TrueHumanMEM/harmonyos/trulymem-core"
},
"compileMode": "esmodule",
"projectRootPath": "/home/program/TrulyMEM-TrueHumanMEM/harmonyos",
"nodeModulesPath": "/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/loader_out/default/node_modules",
"byteCodeHarInfo": {},
"declarationEntry": [],
"moduleName": "entry",
"hspNameOhmMap": {},
"harNameOhmMap": {},
"packageManagerType": "ohpm",
"compileEntry": [
"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/generated/r/default/ResourceTable.ts"
],
"otherCompileFiles": [],
"dynamicImportLibInfo": {},
"routerMap": [],
"hspResourcesMap": {},
"updateVersionInfo": {},
"customizedHar": false,
"anBuildOutPut": "/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/loader_out/default/an/arm64-v8a",
"anBuildMode": "type"
}

View File

@ -0,0 +1 @@
{"entry":{"packageName":"entry","bundleName":"","moduleName":"","version":"","entryPath":"src/main/","isSO":false,"dependencyAlias":""}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,66 @@
{
"app": {
"bundleName": "com.trulymem.app",
"vendor": "trulymem",
"versionCode": 1000001,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name",
"apiReleaseType": "Release",
"compileSdkVersion": "6.1.0.105",
"targetAPIVersion": 60100023,
"minAPIVersion": 60100023,
"compileSdkType": "HarmonyOS",
"targetMinorAPIVersion": 0,
"targetPatchAPIVersion": 0,
"appEnvironments": [],
"bundleType": "app",
"buildMode": "debug",
"debug": true
},
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.GET_NETWORK_INFO"
}
],
"packageName": "entry"
}
}

View File

@ -0,0 +1 @@
{"app":{"bundleName":"com.trulymem.app","vendor":"trulymem","versionCode":1000001,"versionName":"1.0.0","icon":"$media:layered_image","label":"$string:app_name","apiReleaseType":"Release","compileSdkVersion":"6.1.0.105","targetAPIVersion":60100023,"minAPIVersion":60100023,"compileSdkType":"HarmonyOS","targetMinorAPIVersion":0,"targetPatchAPIVersion":0,"appEnvironments":[],"bundleType":"app","buildMode":"debug","debug":true,"iconId":16777216,"labelId":16777219},"module":{"name":"entry","type":"entry","description":"$string:module_desc","mainElement":"EntryAbility","deviceTypes":["phone","tablet","2in1"],"deliveryWithInstall":true,"installationFree":false,"pages":"$profile:main_pages","abilities":[{"name":"EntryAbility","srcEntry":"./ets/entryability/EntryAbility.ets","description":"$string:EntryAbility_desc","icon":"$media:layered_image","label":"$string:EntryAbility_label","startWindowIcon":"$media:startIcon","startWindowBackground":"$color:start_window_background","exported":true,"skills":[{"entities":["entity.system.home"],"actions":["action.system.home"]}],"descriptionId":16777217,"iconId":16777216,"labelId":16777218,"startWindowIconId":16777222,"startWindowBackgroundId":16777221}],"requestPermissions":[{"name":"ohos.permission.INTERNET"},{"name":"ohos.permission.GET_NETWORK_INFO"}],"packageName":"entry","virtualMachine":"ark13.0.1.0","compileMode":"esmodule","dependencies":[],"descriptionId":16777220}}

View File

@ -0,0 +1 @@
{"libs":{},"stripped":{}}

View File

@ -0,0 +1 @@
{"libs":{},"binxo":{},"binxoSymbol":{"enableAsanBinxo":false,"excludeSoFromBinxo":[]}}

View File

@ -0,0 +1,69 @@
{
"app": {
"bundleName": "com.trulymem.app",
"vendor": "trulymem",
"versionCode": 1000001,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name",
"apiReleaseType": "Release",
"compileSdkVersion": "6.1.0.105",
"targetAPIVersion": 60100023,
"minAPIVersion": 60100023,
"compileSdkType": "HarmonyOS",
"targetMinorAPIVersion": 0,
"targetPatchAPIVersion": 0,
"appEnvironments": [],
"bundleType": "app",
"buildMode": "debug",
"debug": true
},
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.GET_NETWORK_INFO"
}
],
"packageName": "entry",
"virtualMachine": "ark13.0.1.0",
"compileMode": "esmodule",
"dependencies": []
}
}

View File

@ -0,0 +1,8 @@
string EntryAbility_desc 0x01000001
string EntryAbility_label 0x01000002
string app_name 0x01000003
string module_desc 0x01000004
color start_window_background 0x01000005
media layered_image 0x01000000
media startIcon 0x01000006
profile main_pages 0x01000007

View File

@ -0,0 +1 @@
{"app":{"bundleName":"com.trulymem.app","vendor":"trulymem","versionCode":1000001,"versionName":"1.0.0","icon":"$media:layered_image","label":"$string:app_name","apiReleaseType":"Release","compileSdkVersion":"6.1.0.105","targetAPIVersion":60100023,"minAPIVersion":60100023,"compileSdkType":"HarmonyOS","targetMinorAPIVersion":0,"targetPatchAPIVersion":0,"appEnvironments":[],"bundleType":"app","buildMode":"debug","debug":true},"module":{"name":"entry","type":"entry","description":"$string:module_desc","mainElement":"EntryAbility","deviceTypes":["phone","tablet","2in1"],"deliveryWithInstall":true,"installationFree":false,"pages":"$profile:main_pages","abilities":[{"name":"EntryAbility","srcEntry":"./ets/entryability/EntryAbility.ets","description":"$string:EntryAbility_desc","icon":"$media:layered_image","label":"$string:EntryAbility_label","startWindowIcon":"$media:startIcon","startWindowBackground":"$color:start_window_background","exported":true,"skills":[{"entities":["entity.system.home"],"actions":["action.system.home"]}]}],"requestPermissions":[{"name":"ohos.permission.INTERNET"},{"name":"ohos.permission.GET_NETWORK_INFO"}],"packageName":"entry","virtualMachine":"ark13.0.1.0","compileMode":"esmodule","dependencies":[]}}

View File

@ -0,0 +1,35 @@
{
"record": [{
"type": "media",
"name": "layered_image",
"id": "0x01000000"
}, {
"type": "string",
"name": "EntryAbility_desc",
"id": "0x01000001"
}, {
"type": "string",
"name": "EntryAbility_label",
"id": "0x01000002"
}, {
"type": "string",
"name": "app_name",
"id": "0x01000003"
}, {
"type": "string",
"name": "module_desc",
"id": "0x01000004"
}, {
"type": "color",
"name": "start_window_background",
"id": "0x01000005"
}, {
"type": "media",
"name": "startIcon",
"id": "0x01000006"
}, {
"type": "profile",
"name": "main_pages",
"id": "0x01000007"
}]
}

View File

@ -0,0 +1,62 @@
{
"app": {
"bundleName": "com.trulymem.app",
"vendor": "trulymem",
"versionCode": 1000001,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name",
"apiReleaseType": "Release",
"compileSdkVersion": "6.1.0.105",
"targetAPIVersion": 60100023,
"minAPIVersion": 60100023,
"compileSdkType": "HarmonyOS",
"targetMinorAPIVersion": 0,
"targetPatchAPIVersion": 0,
"appEnvironments": [],
"bundleType": "app",
"buildMode": "debug",
"debug": true,
"iconId": 16777216,
"labelId": 16777219
},
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet", "2in1"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}],
"descriptionId": 16777217,
"iconId": 16777216,
"labelId": 16777218,
"startWindowIconId": 16777222,
"startWindowBackgroundId": 16777221
}],
"requestPermissions": [{
"name": "ohos.permission.INTERNET"
}, {
"name": "ohos.permission.GET_NETWORK_INFO"
}],
"packageName": "entry",
"virtualMachine": "ark13.0.1.0",
"compileMode": "esmodule",
"dependencies": [],
"descriptionId": 16777220
}
}

View File

@ -0,0 +1 @@
{"context":{"extensionPath":"/home/program/tools/command-line-tools/sdk/default/hms/toolchains/lib/libimage_transcoder_shared.so"},"compression":{"media":{"enable":false},"filters":[]}}

View File

@ -0,0 +1 @@
{"configPath":"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/process_profile/default/module.json","packageName":"com.trulymem.app","output":"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/res/default","moduleNames":"entry,trulymem-core","ResourceTable":["/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/generated/r/default/ResourceTable.h","/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/generated/r/default/ResourceTable.ts"],"applicationResource":"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/AppScope/resources","moduleResources":["/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/src/main/resources"],"dependencies":[],"iconCheck":true,"compression":"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/res/default/opt-compression.json","ids":"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/res/default/ids_map","definedIds":"/home/program/TrulyMEM-TrueHumanMEM/harmonyos/entry/build/default/intermediates/res/default/ids_map/id_defined.json","definedSysIds":"/home/program/tools/command-line-tools/sdk/default/hms/toolchains/id_defined.json"}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<rect width="200" height="200" rx="30" fill="#6366f1"/>
<text x="100" y="130" font-family="Arial" font-size="80" fill="white" text-anchor="middle" font-weight="bold">T</text>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<rect width="200" height="200" rx="20" fill="#6366f1"/>
<text x="100" y="130" font-family="Arial" font-size="60" fill="white" text-anchor="middle" font-weight="bold">TM</text>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@ -0,0 +1,5 @@
{
"src": [
"pages/Index"
]
}

View File

@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆星图 - TrulyMEM</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Courier New', monospace; background: #0a0a1a; color: #ffffff; overflow: hidden; width: 100vw; height: 100vh; }
#canvas-container { width: 100%; height: 100%; position: relative; }
canvas { display: block; }
#stats { position: absolute; top: 20px; left: 20px; background: rgba(10, 10, 26, 0.8); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; backdrop-filter: blur(10px); }
#stats h3 { margin-bottom: 8px; color: #4488ff; font-size: 16px; }
#stats p { margin: 4px 0; color: #aaaacc; }
#stats span { color: #ffffff; font-weight: bold; }
#node-info { position: absolute; top: 20px; right: 20px; background: rgba(10, 10, 26, 0.9); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; display: none; backdrop-filter: blur(10px); max-width: 300px; }
#node-info h3 { color: #44ff88; margin-bottom: 8px; font-size: 16px; }
#node-info p { margin: 4px 0; color: #aaaacc; }
#node-info .label { color: #8888aa; }
#loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; color: #4488ff; z-index: 200; }
#nav { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; z-index: 100; }
.nav-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 14px; backdrop-filter: blur(10px); }
</style>
</head>
<body>
<div id="canvas-container">
<div id="loading">正在加载星图数据...</div>
<div id="stats">
<h3>🌌 记忆星图</h3>
<p>节点: <span id="node-count">0</span></p>
<p>边: <span id="edge-count">0</span></p>
<p>状态: <span id="status">初始化中...</span></p>
</div>
<div id="node-info">
<h3 id="info-name"></h3>
<p><span class="label">类型:</span> <span id="info-type"></span></p>
<p><span class="label">提及次数:</span> <span id="info-mentions"></span></p>
<p><span class="label">连接数:</span> <span id="info-links"></span></p>
</div>
<div id="nav">
<button class="nav-btn active">🌌 星图</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let scene, camera, renderer, controls;
let nodes = [], edges = [];
let nodeMeshes = [], edgeLines = [];
let starField, nebulaParticles;
let raycaster, mouse;
let hoveredNode = null, selectedNode = null;
let animationId;
let highlightPulse = 0;
const typeColors = { 'person': 0x4488ff, 'task': 0xff8844, 'ai': 0xaa44ff, 'concept': 0x44ff88, 'object': 0xff4444 };
const defaultColor = 0xcccccc;
const edgeColors = { '喜欢': 0xff6b6b, '学习': 0x4ecdc4, '属于': 0x45b7d1, '相关': 0x96ceb4, '使用': 0xfeca57, '创建': 0xff9ff3 };
const defaultEdgeColor = 0x444466;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 30, 60);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x0a0a1a, 1);
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
const ambientLight = new THREE.AmbientLight(0x444466, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
scene.add(directionalLight);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
createStarField();
createNebula();
window.addEventListener('resize', onWindowResize);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('click', onMouseClick);
window.addEventListener('message', (event) => {
if (event.data.type === 'graph_data') {
window.__graphData = event.data.payload;
loadGraphData();
}
});
window.parent.postMessage('request_graph_data', '*');
animate();
}
function createStarField() {
const starCount = 3000;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
const radius = 400 + Math.random() * 600;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
const colorChoice = Math.random();
if (colorChoice < 0.7) {
colors[i3] = 0.8 + Math.random() * 0.2;
colors[i3 + 1] = 0.8 + Math.random() * 0.2;
colors[i3 + 2] = 1.0;
} else {
colors[i3] = 1.0;
colors[i3 + 1] = 0.9 + Math.random() * 0.1;
colors[i3 + 2] = 0.8 + Math.random() * 0.2;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true });
starField = new THREE.Points(geometry, material);
scene.add(starField);
}
function createNebula() {
const nebulaCount = 500;
const positions = new Float32Array(nebulaCount * 3);
const colors = new Float32Array(nebulaCount * 3);
for (let i = 0; i < nebulaCount; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 800;
positions[i3 + 1] = (Math.random() - 0.5) * 800;
positions[i3 + 2] = (Math.random() - 0.5) * 800;
const colorChoice = Math.random();
if (colorChoice < 0.33) {
colors[i3] = 0.5 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.7 + Math.random() * 0.3;
} else if (colorChoice < 0.66) {
colors[i3] = 0.2 + Math.random() * 0.2; colors[i3 + 1] = 0.3 + Math.random() * 0.3; colors[i3 + 2] = 0.8 + Math.random() * 0.2;
} else {
colors[i3] = 0.7 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.5 + Math.random() * 0.3;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
nebulaParticles = new THREE.Points(geometry, material);
scene.add(nebulaParticles);
}
window.loadGraphData = function(data) {
if (data && data.nodes && data.edges) {
nodes = data.nodes.map(n => ({ id: n.id, name: n.label || n.name, type: n.type, mention_count: n.mentions || 1 }));
edges = data.edges.map(e => ({ id: e.id, source: e.from || e.source, target: e.to || e.target, relation_type: e.label || e.relation }));
document.getElementById('node-count').textContent = nodes.length;
document.getElementById('edge-count').textContent = edges.length;
document.getElementById('status').textContent = '就绪';
createGraphVisualization();
document.getElementById('loading').style.display = 'none';
}
}
function createGraphVisualization() {
nodeMeshes.forEach(mesh => scene.remove(mesh));
edgeLines.forEach(line => scene.remove(line));
nodeMeshes = [];
edgeLines = [];
if (nodes.length === 0) return;
const nodeDegrees = {};
nodes.forEach(n => nodeDegrees[n.id] = 0);
edges.forEach(e => {
nodeDegrees[e.source] = (nodeDegrees[e.source] || 0) + 1;
nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1;
});
const positions = {};
const maxDegree = Math.max(...Object.values(nodeDegrees), 1);
nodes.forEach((node, i) => {
const angle = (i / nodes.length) * Math.PI * 2;
const radius = 15 + (nodeDegrees[node.id] / maxDegree) * 35;
positions[node.id] = { x: radius * Math.cos(angle), y: (Math.random() - 0.5) * 10, z: radius * Math.sin(angle) };
});
for (let iter = 0; iter < 50; iter++) {
Object.keys(positions).forEach(id1 => {
Object.keys(positions).forEach(id2 => {
if (id1 >= id2) return;
const pos1 = positions[id1], pos2 = positions[id2];
const dx = pos1.x - pos2.x, dy = pos1.y - pos2.y, dz = pos1.z - pos2.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist < 10) {
const force = 0.1 / dist;
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const dx = pos2.x - pos1.x, dy = pos2.y - pos1.y, dz = pos2.z - pos1.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist > 15) {
const force = 0.05;
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
}
nodes.forEach(node => {
const pos = positions[node.id];
if (!pos) return;
const radius = 0.4 + Math.min(node.mention_count * 0.15, 1.5);
const color = typeColors[node.type] || defaultColor;
const geometry = new THREE.SphereGeometry(radius, 16, 12);
const material = new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 + Math.min(node.mention_count * 0.05, 0.3), shininess: 30 });
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(pos.x, pos.y, pos.z);
sphere.userData = { nodeId: node.id, nodeData: node };
scene.add(sphere);
nodeMeshes.push(sphere);
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const color = edgeColors[edge.relation_type] || defaultEdgeColor;
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(pos1.x, pos1.y, pos1.z), new THREE.Vector3(pos2.x, pos2.y, pos2.z)]);
const material = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.4, linewidth: 1 });
const line = new THREE.Line(geometry, material);
line.userData = { edgeId: edge.id, edgeData: edge };
scene.add(line);
edgeLines.push(line);
});
const allPositions = Object.values(positions);
if (allPositions.length > 0) {
let maxDist = 0;
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
controls.target.set(0, 0, 0);
}
}
function onMouseMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (hoveredNode !== node) {
if (hoveredNode) hoveredNode.scale.set(1, 1, 1);
hoveredNode = node;
node.scale.set(1.2, 1.2, 1.2);
showNodeInfo(node.userData.nodeData);
}
} else {
if (hoveredNode) { hoveredNode.scale.set(1, 1, 1); hoveredNode = null; hideNodeInfo(); }
}
}
function onMouseClick(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) { selectedNode = null; document.getElementById('node-info').style.display = 'none'; }
else { selectedNode = node; showNodeInfo(node.userData.nodeData, true); }
}
}
function showNodeInfo(nodeData, isClick = false) {
document.getElementById('info-name').textContent = nodeData.name;
document.getElementById('info-type').textContent = nodeData.type;
document.getElementById('info-mentions').textContent = nodeData.mention_count;
const linkCount = edges.filter(e => e.source === nodeData.id || e.target === nodeData.id).length;
document.getElementById('info-links').textContent = linkCount;
if (isClick) document.getElementById('node-info').style.display = 'block';
}
function hideNodeInfo() { if (!selectedNode) document.getElementById('node-info').style.display = 'none'; }
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
animationId = requestAnimationFrame(animate);
const time = Date.now() * 0.001;
highlightPulse = (highlightPulse + 0.02) % (Math.PI * 2);
controls.update();
if (starField) starField.rotation.y += 0.0001;
if (hoveredNode) { const pulse = 1 + Math.sin(highlightPulse * 3) * 0.05; hoveredNode.scale.set(pulse * 1.2, pulse * 1.2, pulse * 1.2); }
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>

View File

@ -0,0 +1 @@
{"routerMap":[]}

View File

@ -0,0 +1 @@
{"routerMap":[]}

View File

@ -0,0 +1 @@
{"crossAppSharedConfig":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"summary":{"app":{"bundleName":"com.trulymem.app","bundleType":"app","version":{"code":1000001,"name":"1.0.0"}},"modules":[{"mainAbility":"EntryAbility","deviceType":["phone","tablet","2in1"],"abilities":[{"name":"EntryAbility","label":"$string:EntryAbility_label"}],"distro":{"moduleType":"entry","installationFree":false,"deliveryWithInstall":true,"moduleName":"entry"},"apiVersion":{"compatible":23,"releaseType":"Release","target":23}}]},"packages":[{"deviceType":["phone","tablet","2in1"],"moduleType":"entry","deliveryWithInstall":true,"name":"entry-default"}]}

View File

@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks,
plugins: []
};

View File

@ -0,0 +1,34 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
# 保留JSON解析相关的属性名防止混淆导致JSON解析失败
-keep-property-name
id
name
nameEn
icon
isCustom
builtin
custom
pixelMap

View File

@ -0,0 +1,19 @@
{
"meta": {
"stableOrder": true,
"enableUnifiedLockfile": false
},
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"librcon@../librcon": "librcon@../librcon"
},
"packages": {
"librcon@../librcon": {
"name": "librcon",
"version": "1.0.0",
"resolved": "../librcon",
"registryType": "local"
}
}
}

View File

@ -0,0 +1,11 @@
{
"name": "entry",
"version": "1.0.0",
"description": "TrulyMEM - A personal memory and knowledge graph application.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"trulymem-core": "file:../trulymem-core"
}
}

View File

@ -1,6 +1,6 @@
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.app.ability.Window';
import Window from '@ohos.window';
import Want from '@ohos.app.ability.Want';
export default class EntryAbility extends UIAbility {

View File

@ -1,448 +1,237 @@
import relationalStore from '@ohos.data.relationalStore';
import { Constants } from '../common/Constants';
import { Context } from '@ohos.abilityAccessCtrl';
export interface TripletInput {
interface TripletData {
subject: string;
relation: string;
object: string;
}
export interface GraphNode {
interface CriteriaData {
subject?: string;
target?: string;
relation?: string;
sessionId?: string;
}
interface NodeData {
id: number;
name: string;
label: string;
type: string;
mentions: number;
}
export interface GraphEdge {
id: number;
source: number;
target: number;
interface EdgeData {
from: number;
to: number;
label: string;
weight: number;
}
export interface GraphData {
nodes: GraphNode[];
edges: GraphEdge[];
interface GraphData {
nodes: NodeData[];
edges: EdgeData[];
}
export interface CommitResult {
success: boolean;
message: string;
count?: number;
}
export interface RecallResult {
success: boolean;
interface RecallResult {
data: GraphData;
message: string;
}
export interface PurgeCriteria {
subject?: string;
target?: string;
relation?: string;
}
export interface PurgeResult {
success: boolean;
message: string;
affected?: number;
}
export interface ArchiveResult {
success: boolean;
message: string;
archived: number;
}
export interface CleanupResult {
success: boolean;
message: string;
deleted: number;
}
export interface ChatMessage {
id: number;
interface ChatMessage {
role: string;
content: string;
tools?: string;
created_at: string;
}
interface CleanupResult {
cleaned: number;
}
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'trulymem.db',
securityLevel: relationalStore.SecurityLevel.S1
};
export class GraphDatabase {
private store: relationalStore.RdbStore | null = null;
async init(context: any): Promise<void> {
const STORE_CONFIG: relationalStore.StoreConfig = {
name: Constants.DB_NAME,
securityLevel: Constants.SECURITY_LEVEL,
};
private store?: relationalStore.RdbStore;
private context?: Context;
async init(context: Context): Promise<void> {
this.context = context;
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
await this.store.executeSql(Constants.SQL_CREATE_NODES);
await this.store.executeSql(Constants.SQL_CREATE_RELATIONS);
await this.store.executeSql(Constants.SQL_CREATE_CHAT);
console.info('GraphDatabase initialized successfully');
await this.createTables();
}
private assertStore(): relationalStore.RdbStore {
if (!this.store) {
throw new Error('GraphDatabase not initialized. Call init() first.');
}
return this.store;
private async createTables(): Promise<void> {
if (!this.store) return;
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'concept',
mentions INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
`);
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id INTEGER NOT NULL,
relation TEXT NOT NULL,
object_id INTEGER NOT NULL,
weight REAL DEFAULT 1.0,
created_at TEXT
)
`);
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS chat_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
tools TEXT,
created_at TEXT
)
`);
}
// ============ Memory Operations ============
async commit(triplets: TripletInput[]): Promise<CommitResult> {
const store = this.assertStore();
let count = 0;
try {
await store.beginTransaction();
for (const t of triplets) {
// Upsert subject node
const subjId = await this.upsertNode(store, t.subject);
// Upsert object node
const objId = await this.upsertNode(store, t.object);
// Create relation
const bucket: relationalStore.ValuesBucket = {
'subject_id': subjId,
'relation': t.relation,
'object_id': objId,
'weight': 1.0,
};
await store.insert(Constants.TABLE_RELATIONS, bucket);
count++;
}
await store.commit();
return { success: true, message: `Committed ${count} triplets`, count };
} catch (e) {
await store.rollBack();
return { success: false, message: `Commit failed: ${e}` };
async commit(triplets: TripletData[]): Promise<void> {
if (!this.store) return;
for (const triplet of triplets) {
const subjectId: number = await this.upsertNode(triplet.subject);
const objectId: number = await this.upsertNode(triplet.object);
const bucket: relationalStore.ValuesBucket = {
'subject_id': subjectId,
'relation': triplet.relation,
'object_id': objectId,
'created_at': new Date().toISOString()
};
await this.store.insert('relations', bucket);
}
}
private async upsertNode(store: relationalStore.RdbStore, name: string): Promise<number> {
// Check if node exists
const predicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES);
private async upsertNode(name: string): Promise<number> {
if (!this.store) return -1;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
predicates.equalTo('name', name);
const resultSet = await store.query(predicates, ['id', 'mentions']);
let nodeId: number;
if (resultSet.rowCount > 0) {
resultSet.goToFirstRow();
nodeId = resultSet.getLong(resultSet.getColumnIndex('id'));
const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions'));
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
if (resultSet.goToNextRow()) {
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
resultSet.close();
// Update mentions + updated_at
const updateBucket: relationalStore.ValuesBucket = {
'mentions': mentions + 1,
'updated_at': this.getNow(),
};
const updatePredicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES);
updatePredicates.equalTo('id', nodeId);
await store.update(updateBucket, updatePredicates);
} else {
resultSet.close();
const insertBucket: relationalStore.ValuesBucket = {
'name': name,
'type': 'concept',
'mentions': 1,
'created_at': this.getNow(),
'updated_at': this.getNow(),
};
nodeId = await store.insert(Constants.TABLE_NODES, insertBucket);
return id;
}
return nodeId;
resultSet.close();
const bucket: relationalStore.ValuesBucket = {
'name': name,
'type': 'concept',
'created_at': new Date().toISOString(),
'updated_at': new Date().toISOString()
};
return await this.store.insert('nodes', bucket);
}
async recall(queryIntent: string, seedEntities?: string[]): Promise<RecallResult> {
const store = this.assertStore();
try {
if (seedEntities && seedEntities.length > 0) {
// Filter by seed entities
const placeholders = seedEntities.map(() => '?').join(',');
const nodeSql = `SELECT * FROM nodes WHERE name IN (${placeholders}) ORDER BY mentions DESC`;
const nodeResultSet = await store.querySql(nodeSql, seedEntities);
const nodeIds: number[] = [];
const nodes: GraphNode[] = [];
while (nodeResultSet.goToNextRow()) {
const node = this.rowToNode(nodeResultSet);
nodes.push(node);
nodeIds.push(node.id);
}
nodeResultSet.close();
if (nodeIds.length === 0) {
return { success: true, data: { nodes: [], edges: [] }, message: 'No results found' };
}
// Get related edges
const edgeIdPlaceholders = nodeIds.map(() => '?').join(',');
const edgeParams = nodeIds.map(id => id.toString());
const edgeSql = `SELECT * FROM relations WHERE subject_id IN (${edgeIdPlaceholders}) OR object_id IN (${edgeIdPlaceholders})`;
const edgeResultSet = await store.querySql(edgeSql, edgeParams);
const edges: GraphEdge[] = [];
while (edgeResultSet.goToNextRow()) {
edges.push(this.rowToEdge(edgeResultSet));
}
edgeResultSet.close();
// Collect all connected node IDs
const allNodeIds = new Set<number>(nodeIds);
for (const edge of edges) {
allNodeIds.add(edge.source);
allNodeIds.add(edge.target);
}
// Fetch all connected nodes
if (allNodeIds.size > nodeIds.length) {
const allIdPlaceholders = Array.from(allNodeIds).map(() => '?').join(',');
const allIdParams = Array.from(allNodeIds).map(id => id.toString());
const allNodeSql = `SELECT * FROM nodes WHERE id IN (${allIdPlaceholders})`;
const allNodeRs = await store.querySql(allNodeSql, allIdParams);
const allNodes: GraphNode[] = [];
while (allNodeRs.goToNextRow()) {
allNodes.push(this.rowToNode(allNodeRs));
}
allNodeRs.close();
return {
success: true,
data: { nodes: allNodes, edges },
message: `Found ${allNodes.length} nodes, ${edges.length} edges`,
};
}
return {
success: true,
data: { nodes, edges },
message: `Found ${nodes.length} nodes, ${edges.length} edges`,
};
}
// Return all data
return await this.introspect();
} catch (e) {
return { success: false, data: { nodes: [], edges: [] }, message: `Recall failed: ${e}` };
if (!this.store) {
const empty: RecallResult = { data: { nodes: [], edges: [] } };
return empty;
}
const nodes: NodeData[] = await this.getAllNodes();
const edges: EdgeData[] = await this.getAllEdges();
const result: RecallResult = { data: { nodes, edges } };
return result;
}
async purge(criteria: PurgeCriteria): Promise<PurgeResult> {
const store = this.assertStore();
let affected = 0;
try {
if (criteria.target) {
// Delete by target node name
const nodePredicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES);
nodePredicates.equalTo('name', criteria.target);
const rs = await store.query(nodePredicates, ['id']);
while (rs.goToNextRow()) {
const nodeId = rs.getLong(rs.getColumnIndex('id'));
// Delete relations involving this node
const relPredicates1 = new relationalStore.RdbPredicates(Constants.TABLE_RELATIONS);
relPredicates1.equalTo('subject_id', nodeId);
await store.delete(relPredicates1);
const relPredicates2 = new relationalStore.RdbPredicates(Constants.TABLE_RELATIONS);
relPredicates2.equalTo('object_id', nodeId);
await store.delete(relPredicates2);
// Delete the node itself
const delPredicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES);
delPredicates.equalTo('id', nodeId);
affected += await store.delete(delPredicates);
}
rs.close();
}
if (criteria.subject) {
// More sophisticated deletion could be added here
}
return {
success: true,
message: `Purged ${affected} nodes`,
affected,
};
} catch (e) {
return { success: false, message: `Purge failed: ${e}` };
}
async purge(criteria: CriteriaData): Promise<void> {
if (!this.store) return;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
await this.store.delete(predicates);
}
async introspect(): Promise<RecallResult> {
const store = this.assertStore();
try {
// Get all nodes
const nodeRs = await store.querySql('SELECT * FROM nodes ORDER BY mentions DESC LIMIT 500');
const nodes: GraphNode[] = [];
while (nodeRs.goToNextRow()) {
nodes.push(this.rowToNode(nodeRs));
}
nodeRs.close();
// Get all edges
const edgeRs = await store.querySql('SELECT * FROM relations ORDER BY weight DESC LIMIT 1000');
const edges: GraphEdge[] = [];
while (edgeRs.goToNextRow()) {
edges.push(this.rowToEdge(edgeRs));
}
edgeRs.close();
return {
success: true,
data: { nodes, edges },
message: `Found ${nodes.length} nodes, ${edges.length} edges`,
};
} catch (e) {
return { success: false, data: { nodes: [], edges: [] }, message: `Introspect failed: ${e}` };
}
const nodes: NodeData[] = await this.getAllNodes();
const edges: EdgeData[] = await this.getAllEdges();
const result: RecallResult = { data: { nodes, edges } };
return result;
}
async archive(days: number): Promise<ArchiveResult> {
const store = this.assertStore();
try {
const sql = `DELETE FROM relations WHERE created_at < datetime('now', '-${days} days')`;
const changes = await store.executeSql(sql);
return { success: true, message: `Archived relations older than ${days} days`, archived: changes };
} catch (e) {
return { success: false, message: `Archive failed: ${e}`, archived: 0 };
}
async archive(days: number): Promise<void> {
// Archive old data - placeholder
}
async cleanup(dryRun: boolean): Promise<CleanupResult> {
const store = this.assertStore();
try {
if (dryRun) {
const rs = await store.querySql(`
SELECT COUNT(*) as cnt FROM nodes n
WHERE NOT EXISTS (SELECT 1 FROM relations WHERE subject_id = n.id OR object_id = n.id)
AND n.name NOT IN ('_ROOT_', '_UNKNOWN_')
`);
rs.goToFirstRow();
const count = rs.getLong(rs.getColumnIndex('cnt'));
rs.close();
return { success: true, message: `Dry run: ${count} orphaned nodes would be deleted`, deleted: count };
}
const rs = await store.querySql(`
SELECT id FROM nodes n
WHERE NOT EXISTS (SELECT 1 FROM relations WHERE subject_id = n.id OR object_id = n.id)
AND n.name NOT IN ('_ROOT_', '_UNKNOWN_')
`);
const ids: number[] = [];
while (rs.goToNextRow()) {
ids.push(rs.getLong(rs.getColumnIndex('id')));
}
rs.close();
if (ids.length === 0) {
return { success: true, message: 'No orphaned nodes found', deleted: 0 };
}
const placeholders = ids.map(() => '?').join(',');
const idParams = ids.map(id => id.toString());
await store.executeSql(`DELETE FROM nodes WHERE id IN (${placeholders})`, idParams);
return { success: true, message: `Cleaned up ${ids.length} orphaned nodes`, deleted: ids.length };
} catch (e) {
return { success: false, message: `Cleanup failed: ${e}`, deleted: 0 };
}
const result: CleanupResult = { cleaned: 0 };
return result;
}
// ============ Chat Operations ============
async saveChatMessage(role: string, content: string, tools?: string): Promise<number> {
const store = this.assertStore();
async saveChatMessage(role: string, content: string, tools?: string): Promise<void> {
if (!this.store) return;
const bucket: relationalStore.ValuesBucket = {
'role': role,
'content': content,
'tools': tools || null,
'created_at': new Date().toISOString()
};
if (tools) {
bucket['tools'] = tools;
}
return await store.insert(Constants.TABLE_CHAT, bucket);
await this.store.insert('chat_records', bucket);
}
async getChatHistory(limit: number = 50): Promise<ChatMessage[]> {
const store = this.assertStore();
const rs = await store.querySql(
`SELECT * FROM chat_records ORDER BY id DESC LIMIT ${limit}`
);
const messages: ChatMessage[] = [];
while (rs.goToNextRow()) {
const msg: ChatMessage = {
id: rs.getLong(rs.getColumnIndex('id')),
role: rs.getString(rs.getColumnIndex('role')),
content: rs.getString(rs.getColumnIndex('content')),
created_at: rs.getString(rs.getColumnIndex('created_at')),
};
const toolsIdx = rs.getColumnIndex('tools');
if (toolsIdx >= 0) {
msg.tools = rs.getString(toolsIdx);
}
messages.unshift(msg); // Reverse to chronological order
async getChatHistory(limit?: number): Promise<ChatMessage[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
predicates.orderByDesc('created_at');
if (limit) {
predicates.limitAs(limit);
}
rs.close();
return messages;
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content']);
const messages: ChatMessage[] = [];
while (resultSet.goToNextRow()) {
const msg: ChatMessage = {
role: resultSet.getString(resultSet.getColumnIndex('role')),
content: resultSet.getString(resultSet.getColumnIndex('content'))
};
messages.push(msg);
}
resultSet.close();
return messages.reverse();
}
async clearChatHistory(): Promise<void> {
const store = this.assertStore();
await store.executeSql('DELETE FROM chat_records');
if (!this.store) return;
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
await this.store.delete(predicates);
}
// ============ Helper Methods ============
private rowToNode(rs: relationalStore.ResultSet): GraphNode {
return {
id: rs.getLong(rs.getColumnIndex('id')),
name: rs.getString(rs.getColumnIndex('name')),
type: rs.getString(rs.getColumnIndex('type')),
mentions: rs.getLong(rs.getColumnIndex('mentions')),
};
private async getAllNodes(): Promise<NodeData[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
const nodes: NodeData[] = [];
while (resultSet.goToNextRow()) {
const node: NodeData = {
id: resultSet.getLong(resultSet.getColumnIndex('id')),
label: resultSet.getString(resultSet.getColumnIndex('name')),
type: resultSet.getString(resultSet.getColumnIndex('type')),
mentions: resultSet.getLong(resultSet.getColumnIndex('mentions'))
};
nodes.push(node);
}
resultSet.close();
return nodes;
}
private rowToEdge(rs: relationalStore.ResultSet): GraphEdge {
return {
id: rs.getLong(rs.getColumnIndex('id')),
source: rs.getLong(rs.getColumnIndex('subject_id')),
target: rs.getLong(rs.getColumnIndex('object_id')),
label: rs.getString(rs.getColumnIndex('relation')),
weight: rs.getDouble(rs.getColumnIndex('weight')),
};
}
private getNow(): string {
const d = new Date();
const y = d.getFullYear();
const mo = String(d.getMonth() + 1).padStart(2, '0');
const da = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const mi = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return `${y}-${mo}-${da} ${h}:${mi}:${s}`;
private async getAllEdges(): Promise<EdgeData[]> {
if (!this.store) return [];
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
const edges: EdgeData[] = [];
while (resultSet.goToNextRow()) {
const edge: EdgeData = {
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
label: resultSet.getString(resultSet.getColumnIndex('relation')),
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
};
edges.push(edge);
}
resultSet.close();
return edges;
}
}

View File

@ -1,175 +1,100 @@
import http from '@ohos.net.http';
import dataPreferences from '@ohos.data.preferences';
import { GraphDatabase, ChatMessage } from '../model/GraphDatabase';
import { GraphDatabase } from '../model/GraphDatabase';
interface ChatMessage {
role: string;
content: string;
}
@Component
export struct ChatPage {
@State messages: ChatMessage[] = [];
@State inputText: string = '';
private db: GraphDatabase;
private scroller: Scroller = new Scroller();
@Prop db: GraphDatabase;
private scrollController: Scroller = new Scroller();
aboutToAppear() {
this.loadHistory();
}
async loadHistory() {
async aboutToAppear() {
this.messages = await this.db.getChatHistory(50);
}
async sendMessage() {
if (!this.inputText.trim()) {
return;
}
if (!this.inputText.trim()) return;
const userMsg = this.inputText;
const userMessage: string = this.inputText;
this.inputText = '';
// Save user message
await this.db.saveChatMessage('user', userMsg);
await this.db.saveChatMessage('user', userMessage);
const userMsg: ChatMessage = { role: 'user', content: userMessage };
this.messages = [...this.messages, userMsg];
// Update UI immediately
this.messages = await this.db.getChatHistory(50);
// Call AI API
try {
const reply = await this.callAIApi(userMsg);
await this.db.saveChatMessage('assistant', reply);
this.messages = await this.db.getChatHistory(50);
} catch (e) {
await this.db.saveChatMessage('assistant', `Error: ${e.message || e}`);
this.messages = await this.db.getChatHistory(50);
}
}
async callAIApi(prompt: string): Promise<string> {
const ctx = getContext(this);
const pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
const baseUrl = await pref.get('base_url', 'https://api.deepseek.com');
const model = await pref.get('model', 'deepseek-chat');
const apiKey = await pref.get('api_key', '');
if (!apiKey) {
return '请先在设置页面配置 API Key';
}
const httpRequest = http.createHttp();
// Build chat history context
const history = this.messages.slice(-10);
const msgs = history.map(m => ({
role: m.role,
content: m.content,
}));
// Add current prompt
msgs.push({ role: 'user', content: prompt });
const pref: dataPreferences.Preferences =
await dataPreferences.getPreferences(getContext(this), 'trulymem_config');
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
const model: string = String(await pref.get('model', 'deepseek-chat'));
const apiKey: string = String(await pref.get('api_key', ''));
try {
const response = await httpRequest.request(
baseUrl.replace(/\/$/, '') + '/v1/chat/completions',
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
},
extraData: JSON.stringify({
model: model,
messages: msgs,
max_tokens: 2048,
stream: false,
}),
connectTimeout: 30000,
readTimeout: 60000,
const httpRequest: http.HttpRequest = http.createHttp();
const response: http.HttpResponse = await httpRequest.request(baseUrl + '/chat/completions', {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey
},
extraData: {
model: model,
messages: [{ role: 'user', content: userMessage }]
}
);
});
httpRequest.destroy();
const statusCode = response.responseCode;
if (statusCode !== 200) {
return `API 请求失败 (${statusCode}): ${response.result.toString()}`;
if (response.responseCode === 200) {
const data: object = JSON.parse(String(response.result));
const choicesArr: object[] = data['choices'] as object[];
if (choicesArr && choicesArr.length > 0) {
const firstChoice: object = choicesArr[0];
const msgObj: object = firstChoice['message'] as object;
const assistantMessage: string = String(msgObj['content']);
await this.db.saveChatMessage('assistant', assistantMessage);
const aiMsg: ChatMessage = { role: 'assistant', content: assistantMessage };
this.messages = [...this.messages, aiMsg];
}
}
const data = JSON.parse(response.result.toString());
if (data.choices && data.choices.length > 0) {
return data.choices[0].message.content;
}
return 'AI 返回为空';
} catch (e) {
httpRequest.destroy();
throw e;
} catch (err) {
console.error('HTTP request failed: ' + JSON.stringify(err));
}
}
build() {
Column() {
// Message list
List({ scroller: this.scroller }) {
List() {
ForEach(this.messages, (msg: ChatMessage) => {
ListItem() {
if (msg.role === 'user') {
Row() {
Blank()
Column() {
Text(msg.content)
.fontSize(16)
.fontColor('#ffffff')
.padding(12)
}
.backgroundColor('#0052cc')
.borderRadius(12)
.maxWidth('80%')
}
.width('100%')
.padding({ left: 16, right: 16, top: 4, bottom: 4 })
} else {
Row() {
Column() {
Text(msg.content)
.fontSize(16)
.fontColor('#e0e0e0')
.padding(12)
}
.backgroundColor('#2d2d3a')
.borderRadius(12)
.maxWidth('80%')
Blank()
}
.width('100%')
.padding({ left: 16, right: 16, top: 4, bottom: 4 })
Column() {
Text(msg.role).fontSize(12).fontColor('#666').width('100%')
Text(msg.content).fontSize(16).width('100%').margin({ top: 4 })
}
.padding(12)
.backgroundColor(msg.role === 'user' ? '#E3F2FD' : '#F5F5F5')
.borderRadius(8)
.margin({ left: 8, right: 8, bottom: 8 })
}
}, (msg: ChatMessage) => msg.id.toString())
})
}
.width('100%')
.layoutWeight(1)
// Input area
Row() {
TextArea({ text: this.inputText, placeholder: '输入消息...' })
.layoutWeight(1)
.maxLines(4)
.onChange((v: string) => {
this.inputText = v;
})
.margin({ left: 8, right: 4 })
.backgroundColor('#1e1e2e')
.fontColor('#ffffff')
.onChange((v: string) => { this.inputText = v; })
Button('发送')
.onClick(() => this.sendMessage())
.margin({ left: 4, right: 8 })
.backgroundColor('#0052cc')
}
.width('100%')
.height(64)
.backgroundColor('#16162a')
.padding({ top: 8, bottom: 8 })
.padding(8)
}
.width('100%')
.height('100%')
.backgroundColor('#0a0a1a')
}
}

View File

@ -4,33 +4,19 @@ import { GraphDatabase } from '../model/GraphDatabase';
@Component
export struct GraphPage {
private controller: web_webview.WebviewController = new web_webview.WebviewController();
private db: GraphDatabase;
@Prop db: GraphDatabase;
build() {
Column() {
Web({ src: $rawfile('graph.html'), controller: this.controller })
.javaScriptAccess(true)
.onMessageReceive((msg) => {
if (msg.data === 'request_graph_data') {
this.sendGraphData();
}
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
async sendGraphData() {
const result = await this.db.introspect();
if (result.success) {
const jsonStr = JSON.stringify(result.data);
this.controller.runJavaScriptExt('loadGraphData(' + jsonStr + ')', (error, data) => {
if (error) {
console.error('runJavaScriptExt error: ' + JSON.stringify(error));
}
});
}
.onAppear(() => {
this.controller.refresh();
})
}
}

View File

@ -1,6 +1,5 @@
import GraphPage from './GraphPage';
import ChatPage from './ChatPage';
import SettingsPage from './SettingsPage';
import { MainPage } from './MainPage';
import { SettingsPage } from './SettingsPage';
import { GraphDatabase } from '../model/GraphDatabase';
@Entry
@ -17,14 +16,9 @@ struct Index {
Column() {
Tabs({ index: this.currentIndex, barPosition: BarPosition.End }) {
TabContent() {
GraphPage({ db: this.db })
MainPage({ db: this.db })
}
.tabBar('🌌 星图')
TabContent() {
ChatPage({ db: this.db })
}
.tabBar('💬 聊天')
.tabBar('🌌 TrulyMEM')
TabContent() {
SettingsPage()
@ -33,9 +27,7 @@ struct Index {
}
.width('100%')
.height('100%')
.onChange((index: number) => {
this.currentIndex = index;
})
.onChange((index: number) => { this.currentIndex = index; })
}
.width('100%')
.height('100%')

View File

@ -0,0 +1,21 @@
import { GraphDatabase } from '../model/GraphDatabase';
import { GraphPage } from './GraphPage';
import { ChatPage } from './ChatPage';
@Component
export struct MainPage {
@Prop db: GraphDatabase;
build() {
GridRow({ columns: { sm: 1, md: 2, lg: 2 }, gutter: { x: 8, y: 8 } }) {
GridCol({ span: { sm: 1, md: 1, lg: 1 } }) {
GraphPage({ db: this.db })
}
GridCol({ span: { sm: 1, md: 1, lg: 1 } }) {
ChatPage({ db: this.db })
}
}
.width('100%')
.height('100%')
}
}

View File

@ -5,93 +5,56 @@ export struct SettingsPage {
@State baseUrl: string = '';
@State model: string = '';
@State apiKey: string = '';
private pref: dataPreferences.Preferences;
private pref?: dataPreferences.Preferences;
async aboutToAppear() {
const ctx = getContext(this);
this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
this.baseUrl = await this.pref.get('base_url', 'https://api.deepseek.com');
this.model = await this.pref.get('model', 'deepseek-chat');
this.apiKey = await this.pref.get('api_key', '');
this.baseUrl = String(await this.pref.get('base_url', 'https://api.deepseek.com'));
this.model = String(await this.pref.get('model', 'deepseek-chat'));
this.apiKey = String(await this.pref.get('api_key', ''));
}
async onBaseUrlChange(value: string) {
this.baseUrl = value;
await this.pref.put('base_url', value);
await this.pref.flush();
await this.pref?.put('base_url', value);
await this.pref?.flush();
}
async onModelChange(value: string) {
this.model = value;
await this.pref.put('model', value);
await this.pref.flush();
await this.pref?.put('model', value);
await this.pref?.flush();
}
async onApiKeyChange(value: string) {
this.apiKey = value;
await this.pref.put('api_key', value);
await this.pref.flush();
await this.pref?.put('api_key', value);
await this.pref?.flush();
}
build() {
Column() {
Text('API 配置')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
.margin({ top: 24, bottom: 24 })
// Base URL
Text('Base URL')
.fontSize(14)
.fontColor('#888899')
.width('100%')
.margin({ left: 16, bottom: 4 })
Text('API 配置').fontSize(24).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 16 })
Text('Base URL').fontSize(14).width('100%').margin({ left: 16 })
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
.onChange((v: string) => this.onBaseUrlChange(v))
.margin({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#1e1e2e')
.fontColor('#ffffff')
.placeholderColor('#555566')
// Model ID
Text('Model ID')
.fontSize(14)
.fontColor('#888899')
.width('100%')
.margin({ left: 16, bottom: 4 })
.onChange((v: string) => { this.onBaseUrlChange(v); })
.margin({ left: 16, right: 16, bottom: 12 })
Text('Model ID').fontSize(14).width('100%').margin({ left: 16 })
TextInput({ placeholder: 'deepseek-chat', text: this.model })
.onChange((v: string) => this.onModelChange(v))
.margin({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#1e1e2e')
.fontColor('#ffffff')
.placeholderColor('#555566')
// API Key
Text('API Key')
.fontSize(14)
.fontColor('#888899')
.width('100%')
.margin({ left: 16, bottom: 4 })
.onChange((v: string) => { this.onModelChange(v); })
.margin({ left: 16, right: 16, bottom: 12 })
Text('API Key').fontSize(14).width('100%').margin({ left: 16 })
TextInput({ placeholder: 'sk-...', text: this.apiKey })
.type(InputType.Password)
.onChange((v: string) => this.onApiKeyChange(v))
.margin({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#1e1e2e')
.fontColor('#ffffff')
.placeholderColor('#555566')
Text('配置会自动保存')
.fontSize(12)
.fontColor('#666677')
.margin({ top: 32 })
.onChange((v: string) => { this.onApiKeyChange(v); })
.margin({ left: 16, right: 16, bottom: 12 })
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#0a0a1a')
}
}

View File

@ -1,488 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆星图 - TrulyMEM</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: #0a0a1a;
color: #ffffff;
overflow: hidden;
width: 100vw;
height: 100vh;
}
#canvas-container {
width: 100%;
height: 100%;
position: relative;
}
canvas { display: block; }
#stats {
position: absolute;
top: 20px; left: 20px;
background: rgba(10,10,26,0.8);
padding: 15px 20px;
border-radius: 8px;
border: 1px solid rgba(100,100,255,0.3);
font-size: 14px;
z-index: 100;
backdrop-filter: blur(10px);
}
#stats h3 { margin-bottom: 8px; color: #4488ff; font-size: 16px; }
#stats p { margin: 4px 0; color: #aaaacc; }
#stats span { color: #ffffff; font-weight: bold; }
#node-info {
position: absolute;
top: 20px; right: 20px;
background: rgba(10,10,26,0.9);
padding: 15px 20px;
border-radius: 8px;
border: 1px solid rgba(100,100,255,0.3);
font-size: 14px;
z-index: 100;
display: none;
backdrop-filter: blur(10px);
max-width: 300px;
}
#node-info h3 { color: #44ff88; margin-bottom: 8px; font-size: 16px; }
#node-info p { margin: 4px 0; color: #aaaacc; }
#loading {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
color: #4488ff;
z-index: 200;
}
</style>
</head>
<body>
<div id="canvas-container">
<div id="stats">
<h3>✨ 记忆星图</h3>
<p>节点: <span id="node-count">0</span></p>
<p>关系: <span id="edge-count">0</span></p>
</div>
<div id="node-info">
<h3 id="info-name">节点</h3>
<p class="detail">类型: <span id="info-type">-</span></p>
<p class="detail">提及次数: <span id="info-mentions">-</span></p>
</div>
<div id="loading">🔄 加载星图...</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ============ Data Store ============
let graphNodes = [];
let graphEdges = [];
let scene, camera, renderer;
let nodeMeshes = [];
let edgeLines = [];
let raycaster, mouse;
let animationId;
let selectedObject = null;
// Force-directed layout
const FORCE_REPULSION = 800;
const FORCE_ATTRACTION = 0.005;
const FORCE_DAMPING = 0.85;
const MAX_SPEED = 2;
// Colors
const COLORS = {
concept: 0x4488ff,
person: 0x44ff88,
location: 0xff8844,
event: 0xff44ff,
object: 0x44ffff,
idea: 0xffaa44,
default: 0x8888ff,
};
function getColor(type) {
return COLORS[type] || COLORS.default;
}
function getTypeColor(type) {
const c = new THREE.Color(getColor(type));
return '#' + c.getHexString();
}
// ============ Load Graph Data ============
window.loadGraphData = function(data) {
document.getElementById('loading').style.display = 'none';
graphNodes = data.nodes || [];
graphEdges = data.edges || [];
document.getElementById('node-count').textContent = graphNodes.length;
document.getElementById('edge-count').textContent = graphEdges.length;
if (graphNodes.length > 0) {
initScene();
buildGraph();
animate();
}
};
// ============ Three.js Scene ============
function initScene() {
const container = document.getElementById('canvas-container');
const w = container.clientWidth;
const h = container.clientHeight;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 1000);
camera.position.set(0, 0, 40);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(w, h);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x0a0a1a, 1);
container.appendChild(renderer.domElement);
// Starfield
const starGeom = new THREE.BufferGeometry();
const starCount = 2000;
const starPos = new Float32Array(starCount * 3);
for (let i = 0; i < starCount * 3; i++) {
starPos[i] = (Math.random() - 0.5) * 200;
}
starGeom.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({
color: 0x444488,
size: 0.15,
transparent: true,
});
const stars = new THREE.Points(starGeom, starMat);
scene.add(stars);
// Ambient light
const ambientLight = new THREE.AmbientLight(0x404060);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(1, 1, 1);
scene.add(dirLight);
// Raycaster
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
container.addEventListener('click', onCanvasClick);
container.addEventListener('mousemove', onCanvasMove);
window.addEventListener('resize', onResize);
// Orbit (simple rotation via mouse drag)
let isDragging = false;
let prevMouse = { x: 0, y: 0 };
container.addEventListener('mousedown', (e) => {
if (e.target === renderer.domElement) {
isDragging = true;
prevMouse.x = e.clientX;
prevMouse.y = e.clientY;
}
});
container.addEventListener('mousemove', (e) => {
if (isDragging && scene) {
const dx = e.clientX - prevMouse.x;
const dy = e.clientY - prevMouse.y;
scene.rotation.y += dx * 0.005;
scene.rotation.x += dy * 0.005;
prevMouse.x = e.clientX;
prevMouse.y = e.clientY;
}
});
container.addEventListener('mouseup', () => { isDragging = false; });
container.addEventListener('mouseleave', () => { isDragging = false; });
}
// ============ Build Graph ============
function buildGraph() {
// Create a map for quick lookup
const nodeMap = {};
graphNodes.forEach(n => { nodeMap[n.id] = n; });
// Initialize positions using force-directed layout
const positions = {};
const velocities = {};
graphNodes.forEach((n, i) => {
// Initial random positions in a sphere
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 5 + Math.random() * 10;
positions[n.id] = {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.sin(phi) * Math.sin(theta),
z: r * Math.cos(phi),
};
velocities[n.id] = { x: 0, y: 0, z: 0 };
});
// Run force simulation (40 iterations for initial layout)
for (let iter = 0; iter < 60; iter++) {
// Reset forces
const forces = {};
graphNodes.forEach(n => {
forces[n.id] = { x: 0, y: 0, z: 0 };
});
// Repulsion between all nodes
for (let i = 0; i < graphNodes.length; i++) {
for (let j = i + 1; j < graphNodes.length; j++) {
const a = graphNodes[i];
const b = graphNodes[j];
const pa = positions[a.id];
const pb = positions[b.id];
let dx = pb.x - pa.x;
let dy = pb.y - pa.y;
let dz = pb.z - pa.z;
let dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
let force = FORCE_REPULSION / (dist * dist);
forces[a.id].x -= force * dx / dist;
forces[a.id].y -= force * dy / dist;
forces[a.id].z -= force * dz / dist;
forces[b.id].x += force * dx / dist;
forces[b.id].y += force * dy / dist;
forces[b.id].z += force * dz / dist;
}
}
// Attraction along edges
graphEdges.forEach(e => {
const pa = positions[e.source];
const pb = positions[e.target];
if (!pa || !pb) return;
let dx = pb.x - pa.x;
let dy = pb.y - pa.y;
let dz = pb.z - pa.z;
let dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
let force = dist * FORCE_ATTRACTION;
forces[e.source].x += force * dx / dist;
forces[e.source].y += force * dy / dist;
forces[e.source].z += force * dz / dist;
forces[e.target].x -= force * dx / dist;
forces[e.target].y -= force * dy / dist;
forces[e.target].z -= force * dz / dist;
});
// Apply forces with damping
graphNodes.forEach(n => {
velocities[n.id].x = (velocities[n.id].x + forces[n.id].x) * FORCE_DAMPING;
velocities[n.id].y = (velocities[n.id].y + forces[n.id].y) * FORCE_DAMPING;
velocities[n.id].z = (velocities[n.id].z + forces[n.id].z) * FORCE_DAMPING;
const speed = Math.sqrt(
velocities[n.id].x*velocities[n.id].x +
velocities[n.id].y*velocities[n.id].y +
velocities[n.id].z*velocities[n.id].z
);
if (speed > MAX_SPEED) {
velocities[n.id].x *= MAX_SPEED / speed;
velocities[n.id].y *= MAX_SPEED / speed;
velocities[n.id].z *= MAX_SPEED / speed;
}
positions[n.id].x += velocities[n.id].x;
positions[n.id].y += velocities[n.id].y;
positions[n.id].z += velocities[n.id].z;
});
}
// Create node meshes
nodeMeshes = [];
graphNodes.forEach(n => {
const pos = positions[n.id];
const size = Math.min(1.0, Math.max(0.3, 0.3 + n.mentions * 0.02));
const geom = new THREE.SphereGeometry(size, 16, 16);
const color = getColor(n.type);
const mat = new THREE.MeshPhongMaterial({
color: color,
emissive: color,
emissiveIntensity: 0.2,
specular: 0x444488,
shininess: 30,
});
const mesh = new THREE.Mesh(geom, mat);
mesh.position.set(pos.x, pos.y, pos.z);
mesh.userData = { nodeId: n.id, node: n };
scene.add(mesh);
nodeMeshes.push(mesh);
// Node label (using sprite)
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(10,10,26,0.6)';
ctx.roundRect(0, 0, 256, 64, 8);
ctx.fill();
ctx.font = 'bold 20px Courier New';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#ffffff';
ctx.fillText(n.name.length > 15 ? n.name.substring(0, 15) + '...' : n.name, 128, 32);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const spriteMat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false,
});
const sprite = new THREE.Sprite(spriteMat);
sprite.position.set(pos.x, pos.y + size + 0.5, pos.z);
sprite.scale.set(4, 1, 1);
sprite.userData = { nodeId: n.id, isLabel: true };
scene.add(sprite);
});
// Create edge lines
edgeLines = [];
graphEdges.forEach(e => {
const pa = positions[e.source];
const pb = positions[e.target];
if (!pa || !pb) return;
const points = [
new THREE.Vector3(pa.x, pa.y, pa.z),
new THREE.Vector3(pb.x, pb.y, pb.z),
];
const geom = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({
color: 0x4488ff,
transparent: true,
opacity: 0.3,
});
const line = new THREE.Line(geom, mat);
line.userData = { edge: e };
scene.add(line);
edgeLines.push(line);
});
// Center camera
let cx = 0, cy = 0, cz = 0;
graphNodes.forEach(n => {
cx += positions[n.id].x;
cy += positions[n.id].y;
cz += positions[n.id].z;
});
cx /= graphNodes.length;
cy /= graphNodes.length;
cz /= graphNodes.length;
// Move everything so center is at origin
nodeMeshes.forEach(m => {
m.position.x -= cx;
m.position.y -= cy;
m.position.z -= cz;
});
edgeLines.forEach(l => {
const p = l.geometry.attributes.position;
for (let i = 0; i < p.count; i++) {
p.setXYZ(i, p.getX(i) - cx, p.getY(i) - cy, p.getZ(i) - cz);
}
p.needsUpdate = true;
});
scene.children.forEach(c => {
if (c.isSprite && c.userData.isLabel) {
c.position.x -= cx;
c.position.y -= cy;
c.position.z -= cz;
}
});
}
// ============ Interaction ============
function onCanvasClick(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const meshes = nodeMeshes.filter(m => m.visible);
const intersects = raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
const hit = intersects[0].object;
const node = hit.userData.node;
showNodeInfo(node);
} else {
hideNodeInfo();
}
}
function onCanvasMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const meshes = nodeMeshes.filter(m => m.visible);
const intersects = raycaster.intersectObjects(meshes);
// Reset all
nodeMeshes.forEach(m => {
m.material.emissiveIntensity = 0.2;
m.scale.set(1, 1, 1);
});
if (intersects.length > 0) {
const hit = intersects[0].object;
hit.material.emissiveIntensity = 0.8;
hit.scale.set(1.3, 1.3, 1.3);
renderer.domElement.style.cursor = 'pointer';
} else {
renderer.domElement.style.cursor = 'default';
}
}
function showNodeInfo(node) {
document.getElementById('node-info').style.display = 'block';
document.getElementById('info-name').textContent = node.name;
document.getElementById('info-type').textContent = node.type || 'concept';
document.getElementById('info-mentions').textContent = String(node.mentions || 1);
}
function hideNodeInfo() {
document.getElementById('node-info').style.display = 'none';
}
function onResize() {
const container = document.getElementById('canvas-container');
const w = container.clientWidth;
const h = container.clientHeight;
if (camera && renderer) {
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
}
}
// ============ Animation Loop ============
function animate() {
animationId = requestAnimationFrame(animate);
// Slow rotation
if (scene) {
scene.rotation.y += 0.001;
}
renderer.render(scene, camera);
}
// ============ Init ============
// Wait for Three.js to load
if (typeof THREE !== 'undefined') {
// Three.js already loaded, signal ready
window.parent.postMessage('request_graph_data', '*');
} else {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
window.parent.postMessage('request_graph_data', '*');
}, 1000);
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<rect width="200" height="200" rx="30" fill="#6366f1"/>
<text x="100" y="130" font-family="Arial" font-size="80" fill="white" text-anchor="middle" font-weight="bold">T</text>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<rect width="200" height="200" rx="20" fill="#6366f1"/>
<text x="100" y="130" font-family="Arial" font-size="60" fill="white" text-anchor="middle" font-weight="bold">TM</text>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆星图 - TrulyMEM</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Courier New', monospace; background: #0a0a1a; color: #ffffff; overflow: hidden; width: 100vw; height: 100vh; }
#canvas-container { width: 100%; height: 100%; position: relative; }
canvas { display: block; }
#stats { position: absolute; top: 20px; left: 20px; background: rgba(10, 10, 26, 0.8); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; backdrop-filter: blur(10px); }
#stats h3 { margin-bottom: 8px; color: #4488ff; font-size: 16px; }
#stats p { margin: 4px 0; color: #aaaacc; }
#stats span { color: #ffffff; font-weight: bold; }
#node-info { position: absolute; top: 20px; right: 20px; background: rgba(10, 10, 26, 0.9); padding: 15px 20px; border-radius: 8px; border: 1px solid rgba(100, 100, 255, 0.3); font-size: 14px; z-index: 100; display: none; backdrop-filter: blur(10px); max-width: 300px; }
#node-info h3 { color: #44ff88; margin-bottom: 8px; font-size: 16px; }
#node-info p { margin: 4px 0; color: #aaaacc; }
#node-info .label { color: #8888aa; }
#loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; color: #4488ff; z-index: 200; }
#nav { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; z-index: 100; }
.nav-btn { background: rgba(10, 10, 26, 0.8); border: 1px solid rgba(100, 100, 255, 0.3); color: #aaaacc; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-family: 'Courier New', monospace; font-size: 14px; backdrop-filter: blur(10px); }
</style>
</head>
<body>
<div id="canvas-container">
<div id="loading">正在加载星图数据...</div>
<div id="stats">
<h3>🌌 记忆星图</h3>
<p>节点: <span id="node-count">0</span></p>
<p>边: <span id="edge-count">0</span></p>
<p>状态: <span id="status">初始化中...</span></p>
</div>
<div id="node-info">
<h3 id="info-name"></h3>
<p><span class="label">类型:</span> <span id="info-type"></span></p>
<p><span class="label">提及次数:</span> <span id="info-mentions"></span></p>
<p><span class="label">连接数:</span> <span id="info-links"></span></p>
</div>
<div id="nav">
<button class="nav-btn active">🌌 星图</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let scene, camera, renderer, controls;
let nodes = [], edges = [];
let nodeMeshes = [], edgeLines = [];
let starField, nebulaParticles;
let raycaster, mouse;
let hoveredNode = null, selectedNode = null;
let animationId;
let highlightPulse = 0;
const typeColors = { 'person': 0x4488ff, 'task': 0xff8844, 'ai': 0xaa44ff, 'concept': 0x44ff88, 'object': 0xff4444 };
const defaultColor = 0xcccccc;
const edgeColors = { '喜欢': 0xff6b6b, '学习': 0x4ecdc4, '属于': 0x45b7d1, '相关': 0x96ceb4, '使用': 0xfeca57, '创建': 0xff9ff3 };
const defaultEdgeColor = 0x444466;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 30, 60);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x0a0a1a, 1);
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
const ambientLight = new THREE.AmbientLight(0x444466, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
scene.add(directionalLight);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
createStarField();
createNebula();
window.addEventListener('resize', onWindowResize);
renderer.domElement.addEventListener('mousemove', onMouseMove);
renderer.domElement.addEventListener('click', onMouseClick);
window.addEventListener('message', (event) => {
if (event.data.type === 'graph_data') {
window.__graphData = event.data.payload;
loadGraphData();
}
});
window.parent.postMessage('request_graph_data', '*');
animate();
}
function createStarField() {
const starCount = 3000;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const i3 = i * 3;
const radius = 400 + Math.random() * 600;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
const colorChoice = Math.random();
if (colorChoice < 0.7) {
colors[i3] = 0.8 + Math.random() * 0.2;
colors[i3 + 1] = 0.8 + Math.random() * 0.2;
colors[i3 + 2] = 1.0;
} else {
colors[i3] = 1.0;
colors[i3 + 1] = 0.9 + Math.random() * 0.1;
colors[i3 + 2] = 0.8 + Math.random() * 0.2;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 1.5, vertexColors: true, transparent: true, opacity: 0.8, sizeAttenuation: true });
starField = new THREE.Points(geometry, material);
scene.add(starField);
}
function createNebula() {
const nebulaCount = 500;
const positions = new Float32Array(nebulaCount * 3);
const colors = new Float32Array(nebulaCount * 3);
for (let i = 0; i < nebulaCount; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 800;
positions[i3 + 1] = (Math.random() - 0.5) * 800;
positions[i3 + 2] = (Math.random() - 0.5) * 800;
const colorChoice = Math.random();
if (colorChoice < 0.33) {
colors[i3] = 0.5 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.7 + Math.random() * 0.3;
} else if (colorChoice < 0.66) {
colors[i3] = 0.2 + Math.random() * 0.2; colors[i3 + 1] = 0.3 + Math.random() * 0.3; colors[i3 + 2] = 0.8 + Math.random() * 0.2;
} else {
colors[i3] = 0.7 + Math.random() * 0.3; colors[i3 + 1] = 0.2 + Math.random() * 0.2; colors[i3 + 2] = 0.5 + Math.random() * 0.3;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: 0.15, sizeAttenuation: true, blending: THREE.AdditiveBlending });
nebulaParticles = new THREE.Points(geometry, material);
scene.add(nebulaParticles);
}
window.loadGraphData = function(data) {
if (data && data.nodes && data.edges) {
nodes = data.nodes.map(n => ({ id: n.id, name: n.label || n.name, type: n.type, mention_count: n.mentions || 1 }));
edges = data.edges.map(e => ({ id: e.id, source: e.from || e.source, target: e.to || e.target, relation_type: e.label || e.relation }));
document.getElementById('node-count').textContent = nodes.length;
document.getElementById('edge-count').textContent = edges.length;
document.getElementById('status').textContent = '就绪';
createGraphVisualization();
document.getElementById('loading').style.display = 'none';
}
}
function createGraphVisualization() {
nodeMeshes.forEach(mesh => scene.remove(mesh));
edgeLines.forEach(line => scene.remove(line));
nodeMeshes = [];
edgeLines = [];
if (nodes.length === 0) return;
const nodeDegrees = {};
nodes.forEach(n => nodeDegrees[n.id] = 0);
edges.forEach(e => {
nodeDegrees[e.source] = (nodeDegrees[e.source] || 0) + 1;
nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1;
});
const positions = {};
const maxDegree = Math.max(...Object.values(nodeDegrees), 1);
nodes.forEach((node, i) => {
const angle = (i / nodes.length) * Math.PI * 2;
const radius = 15 + (nodeDegrees[node.id] / maxDegree) * 35;
positions[node.id] = { x: radius * Math.cos(angle), y: (Math.random() - 0.5) * 10, z: radius * Math.sin(angle) };
});
for (let iter = 0; iter < 50; iter++) {
Object.keys(positions).forEach(id1 => {
Object.keys(positions).forEach(id2 => {
if (id1 >= id2) return;
const pos1 = positions[id1], pos2 = positions[id2];
const dx = pos1.x - pos2.x, dy = pos1.y - pos2.y, dz = pos1.z - pos2.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist < 10) {
const force = 0.1 / dist;
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const dx = pos2.x - pos1.x, dy = pos2.y - pos1.y, dz = pos2.z - pos1.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist > 15) {
const force = 0.05;
pos1.x += (dx/dist)*force; pos1.y += (dy/dist)*force; pos1.z += (dz/dist)*force;
pos2.x -= (dx/dist)*force; pos2.y -= (dy/dist)*force; pos2.z -= (dz/dist)*force;
}
});
}
nodes.forEach(node => {
const pos = positions[node.id];
if (!pos) return;
const radius = 0.4 + Math.min(node.mention_count * 0.15, 1.5);
const color = typeColors[node.type] || defaultColor;
const geometry = new THREE.SphereGeometry(radius, 16, 12);
const material = new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 + Math.min(node.mention_count * 0.05, 0.3), shininess: 30 });
const sphere = new THREE.Mesh(geometry, material);
sphere.position.set(pos.x, pos.y, pos.z);
sphere.userData = { nodeId: node.id, nodeData: node };
scene.add(sphere);
nodeMeshes.push(sphere);
});
edges.forEach(edge => {
const pos1 = positions[edge.source], pos2 = positions[edge.target];
if (!pos1 || !pos2) return;
const color = edgeColors[edge.relation_type] || defaultEdgeColor;
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(pos1.x, pos1.y, pos1.z), new THREE.Vector3(pos2.x, pos2.y, pos2.z)]);
const material = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.4, linewidth: 1 });
const line = new THREE.Line(geometry, material);
line.userData = { edgeId: edge.id, edgeData: edge };
scene.add(line);
edgeLines.push(line);
});
const allPositions = Object.values(positions);
if (allPositions.length > 0) {
let maxDist = 0;
allPositions.forEach(pos => { maxDist = Math.max(maxDist, Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z)); });
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
controls.target.set(0, 0, 0);
}
}
function onMouseMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (hoveredNode !== node) {
if (hoveredNode) hoveredNode.scale.set(1, 1, 1);
hoveredNode = node;
node.scale.set(1.2, 1.2, 1.2);
showNodeInfo(node.userData.nodeData);
}
} else {
if (hoveredNode) { hoveredNode.scale.set(1, 1, 1); hoveredNode = null; hideNodeInfo(); }
}
}
function onMouseClick(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodeMeshes);
if (intersects.length > 0) {
const node = intersects[0].object;
if (selectedNode === node) { selectedNode = null; document.getElementById('node-info').style.display = 'none'; }
else { selectedNode = node; showNodeInfo(node.userData.nodeData, true); }
}
}
function showNodeInfo(nodeData, isClick = false) {
document.getElementById('info-name').textContent = nodeData.name;
document.getElementById('info-type').textContent = nodeData.type;
document.getElementById('info-mentions').textContent = nodeData.mention_count;
const linkCount = edges.filter(e => e.source === nodeData.id || e.target === nodeData.id).length;
document.getElementById('info-links').textContent = linkCount;
if (isClick) document.getElementById('node-info').style.display = 'block';
}
function hideNodeInfo() { if (!selectedNode) document.getElementById('node-info').style.display = 'none'; }
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
animationId = requestAnimationFrame(animate);
const time = Date.now() * 0.001;
highlightPulse = (highlightPulse + 0.02) % (Math.PI * 2);
controls.update();
if (starField) starField.rotation.y += 0.0001;
if (hoveredNode) { const pulse = 1 + Math.sin(highlightPulse * 3) * 0.05; hoveredNode.scale.set(pulse * 1.2, pulse * 1.2, pulse * 1.2); }
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>

View File

@ -1,5 +1,23 @@
{
"modelVersion": "5.0.5",
"dependencies": {},
"execution": {}
"dependencies": {
},
"execution": {
// "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */
// "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
// "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
// "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
// "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
// "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */
},
"logging": {
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
},
"debugging": {
// "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
},
"nodeOptions": {
// "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
// "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
}
}

6
harmonyos/hvigorfile.ts Normal file
View File

@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks,
plugins: []
};

View File

@ -1,2 +1,2 @@
hwsdk.dir=/opt/harmonyos/ohos-sdk/linux/command-line-tools/sdk/default
sdk.dir=/opt/harmonyos/ohos-sdk/linux/command-line-tools/sdk/default
hwsdk.dir=/home/program/tools/command-line-tools/sdk/default
sdk.dir=/home/program/tools/command-line-tools/sdk/default

View File

@ -1,7 +1,8 @@
{
"modelVersion": "5.0.5",
"description": "TrulyMEM - True Memory Engine for HarmonyOS",
"dependencies": {},
"description": "Please describe the basic information.",
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.24",
"@ohos/hamock": "1.0.0"

View File

@ -0,0 +1,30 @@
{
"app": {
"bundleName": "com.example.rconmc",
"debug": true,
"versionCode": 1000000,
"versionName": "1.0.0",
"minAPIVersion": 60001021,
"targetAPIVersion": 60001021,
"apiReleaseType": "Release",
"targetMinorAPIVersion": 0,
"targetPatchAPIVersion": 0,
"compileSdkVersion": "6.0.2.130",
"compileSdkType": "HarmonyOS",
"appEnvironments": [],
"bundleType": "app",
"buildMode": "debug"
},
"module": {
"name": "librcon",
"type": "har",
"requestPermissions": [],
"deviceTypes": [
"default",
"tablet",
"2in1"
],
"packageName": "librcon",
"installationFree": false
}
}

View File

@ -0,0 +1,17 @@
/**
* Use these variables when you tailor your ArkTS code. They must be of the const type.
*/
export const HAR_VERSION = '1.0.0';
export const BUILD_MODE_NAME = 'debug';
export const DEBUG = true;
export const TARGET_NAME = 'default';
/**
* BuildProfile Class is used only for compatibility purposes.
*/
export default class BuildProfile {
static readonly HAR_VERSION = HAR_VERSION;
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
static readonly DEBUG = DEBUG;
static readonly TARGET_NAME = TARGET_NAME;
}

View File

@ -0,0 +1,2 @@
// TrulyMEM Core - 导出核心功能
export { GraphDatabase } from './src/main/ets/GraphDatabase';

View File

@ -0,0 +1,41 @@
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
},
"consumerFiles": [
"./consumer-rules.txt"
]
}
},
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest"
}
]
}

View File

@ -0,0 +1,30 @@
{
"app": {
"bundleName": "com.example.blockmanager",
"debug": true,
"versionCode": 1000001,
"versionName": "1.0.1",
"minAPIVersion": 60100023,
"targetAPIVersion": 60100023,
"apiReleaseType": "Release",
"targetMinorAPIVersion": 0,
"targetPatchAPIVersion": 0,
"compileSdkVersion": "6.1.0.105",
"compileSdkType": "HarmonyOS",
"appEnvironments": [],
"bundleType": "app",
"buildMode": "debug"
},
"module": {
"name": "librcon",
"type": "har",
"requestPermissions": [],
"deviceTypes": [
"default",
"tablet",
"2in1"
],
"packageName": "librcon",
"installationFree": false
}
}

Some files were not shown because too many files have changed in this diff Show More