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:
11
README.md
11
README.md
@ -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)
|
GNU General Public License v3.0 (GPLv3)
|
||||||
|
|||||||
11
README_EN.md
11
README_EN.md
@ -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
|
## License
|
||||||
|
|
||||||
GNU General Public License v3.0 (GPLv3)
|
GNU General Public License v3.0 (GPLv3)
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"bundleName": "com.trulymem.app",
|
"bundleName": "com.trulymem.app",
|
||||||
"vendor": "trulymem",
|
"vendor": "trulymem",
|
||||||
"versionCode": 1000000,
|
"versionCode": 1000001,
|
||||||
"versionName": "1.0.0",
|
"versionName": "1.0.0",
|
||||||
"icon": ":layered_image",
|
"icon": "$media:layered_image",
|
||||||
"label": ":app_name"
|
"label": "$string:app_name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 |
@ -1,19 +1,47 @@
|
|||||||
{
|
{
|
||||||
"app": {
|
"app": {
|
||||||
"signingConfigs": [],
|
|
||||||
"products": [
|
"products": [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"signingConfig": "default",
|
"targetSdkVersion": "6.1.0(23)",
|
||||||
"compatibleSdkVersion": "6.0.0.48",
|
"compatibleSdkVersion": "6.1.0(23)",
|
||||||
"compileSdkVersion": "6.0.0.48"
|
"runtimeOS": "HarmonyOS",
|
||||||
|
"buildOption": {
|
||||||
|
"strictMode": {
|
||||||
|
"caseSensitiveCheck": true,
|
||||||
|
"useNormalizedOHMUrl": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildModeSet": [
|
||||||
|
{
|
||||||
|
"name": "debug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"modules": [
|
"modules": [
|
||||||
{
|
{
|
||||||
"name": "entry",
|
"name": "entry",
|
||||||
"srcPath": "./entry"
|
"srcPath": "./entry",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": ["default"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trulymem-core",
|
||||||
|
"srcPath": "./trulymem-core",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
32
harmonyos/code-linter.json5
Normal file
32
harmonyos/code-linter.json5
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,38 @@
|
|||||||
{
|
{
|
||||||
"apiType": "stageMode",
|
"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": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default"
|
||||||
"runtimeOS": "HarmonyOS"
|
},
|
||||||
|
{
|
||||||
|
"name": "ohosTest",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2176
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/.ts_checker_cache
vendored
Normal file
2176
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/.ts_checker_cache
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo
vendored
Normal file
1
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/compileInfo.json
vendored
Normal file
1
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/compileInfo.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"buildMode":"Debug"}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"hspPkgNames":[],"compileEntries":["&entry/build/generated/r/ResourceTable&","&entry/src/main/ets/entryability/EntryAbility&","&entry/src/main/ets/pages/Index&"],"updateVersionInfo":{}}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
<EFBFBD>r@<40>
|
||||||
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"resolveConflictMode":true}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
//# sourceMappingURL=ResourceTable.js.map
|
||||||
Binary file not shown.
Binary file not shown.
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@ -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" });
|
||||||
Binary file not shown.
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
BIN
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.protoBin
vendored
Normal file
BIN
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.protoBin
vendored
Normal file
Binary file not shown.
@ -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
|
||||||
89
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/sourceMaps.json
vendored
Normal file
89
harmonyos/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/sourceMaps.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -0,0 +1 @@
|
|||||||
|
[{"hapName":"entry-default-unsigned.hap","deviceTypes":["phone","tablet","2in1"],"isSigned":false}]
|
||||||
@ -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"
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"entry":{"packageName":"entry","bundleName":"","moduleName":"","version":"","entryPath":"src/main/","isSO":false,"dependencyAlias":""}}
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"libs":{},"stripped":{}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"libs":{},"binxo":{},"binxoSymbol":{"enableAsanBinxo":false,"excludeSoFromBinxo":[]}}
|
||||||
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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":[]}}
|
||||||
@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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":[]}}
|
||||||
@ -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"}
|
||||||
Binary file not shown.
@ -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 |
@ -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 |
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"src": [
|
||||||
|
"pages/Index"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"routerMap":[]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"routerMap":[]}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"crossAppSharedConfig":[]}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
1
harmonyos/entry/build/default/outputs/default/pack.info
Normal file
1
harmonyos/entry/build/default/outputs/default/pack.info
Normal 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"}]}
|
||||||
6
harmonyos/entry/hvigorfile.ts
Normal file
6
harmonyos/entry/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: hapTasks,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
34
harmonyos/entry/obfuscation-rules.txt
Normal file
34
harmonyos/entry/obfuscation-rules.txt
Normal 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
|
||||||
19
harmonyos/entry/oh-package-lock.json5
Normal file
19
harmonyos/entry/oh-package-lock.json5
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
harmonyos/entry/oh-package.json5
Normal file
11
harmonyos/entry/oh-package.json5
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
|
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
|
||||||
import UIAbility from '@ohos.app.ability.UIAbility';
|
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';
|
import Want from '@ohos.app.ability.Want';
|
||||||
|
|
||||||
export default class EntryAbility extends UIAbility {
|
export default class EntryAbility extends UIAbility {
|
||||||
|
|||||||
@ -1,448 +1,237 @@
|
|||||||
import relationalStore from '@ohos.data.relationalStore';
|
import relationalStore from '@ohos.data.relationalStore';
|
||||||
import { Constants } from '../common/Constants';
|
import { Context } from '@ohos.abilityAccessCtrl';
|
||||||
|
|
||||||
export interface TripletInput {
|
interface TripletData {
|
||||||
subject: string;
|
subject: string;
|
||||||
relation: string;
|
relation: string;
|
||||||
object: string;
|
object: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphNode {
|
interface CriteriaData {
|
||||||
|
subject?: string;
|
||||||
|
target?: string;
|
||||||
|
relation?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeData {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
label: string;
|
||||||
type: string;
|
type: string;
|
||||||
mentions: number;
|
mentions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphEdge {
|
interface EdgeData {
|
||||||
id: number;
|
from: number;
|
||||||
source: number;
|
to: number;
|
||||||
target: number;
|
|
||||||
label: string;
|
label: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphData {
|
interface GraphData {
|
||||||
nodes: GraphNode[];
|
nodes: NodeData[];
|
||||||
edges: GraphEdge[];
|
edges: EdgeData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommitResult {
|
interface RecallResult {
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecallResult {
|
|
||||||
success: boolean;
|
|
||||||
data: GraphData;
|
data: GraphData;
|
||||||
message: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PurgeCriteria {
|
interface ChatMessage {
|
||||||
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;
|
|
||||||
role: string;
|
role: string;
|
||||||
content: 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 {
|
export class GraphDatabase {
|
||||||
private store: relationalStore.RdbStore | null = null;
|
private store?: relationalStore.RdbStore;
|
||||||
|
private context?: Context;
|
||||||
async init(context: any): Promise<void> {
|
|
||||||
const STORE_CONFIG: relationalStore.StoreConfig = {
|
|
||||||
name: Constants.DB_NAME,
|
|
||||||
securityLevel: Constants.SECURITY_LEVEL,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
async init(context: Context): Promise<void> {
|
||||||
|
this.context = context;
|
||||||
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
|
this.store = await relationalStore.getRdbStore(context, STORE_CONFIG);
|
||||||
await this.store.executeSql(Constants.SQL_CREATE_NODES);
|
await this.createTables();
|
||||||
await this.store.executeSql(Constants.SQL_CREATE_RELATIONS);
|
|
||||||
await this.store.executeSql(Constants.SQL_CREATE_CHAT);
|
|
||||||
console.info('GraphDatabase initialized successfully');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertStore(): relationalStore.RdbStore {
|
private async createTables(): Promise<void> {
|
||||||
if (!this.store) {
|
if (!this.store) return;
|
||||||
throw new Error('GraphDatabase not initialized. Call init() first.');
|
await this.store.executeSql(`
|
||||||
}
|
CREATE TABLE IF NOT EXISTS nodes (
|
||||||
return this.store;
|
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: TripletData[]): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
async commit(triplets: TripletInput[]): Promise<CommitResult> {
|
for (const triplet of triplets) {
|
||||||
const store = this.assertStore();
|
const subjectId: number = await this.upsertNode(triplet.subject);
|
||||||
let count = 0;
|
const objectId: number = await this.upsertNode(triplet.object);
|
||||||
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
try {
|
'subject_id': subjectId,
|
||||||
await store.beginTransaction();
|
'relation': triplet.relation,
|
||||||
|
'object_id': objectId,
|
||||||
for (const t of triplets) {
|
'created_at': new Date().toISOString()
|
||||||
// Upsert subject node
|
};
|
||||||
const subjId = await this.upsertNode(store, t.subject);
|
await this.store.insert('relations', bucket);
|
||||||
|
|
||||||
// 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}` };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async upsertNode(store: relationalStore.RdbStore, name: string): Promise<number> {
|
private async upsertNode(name: string): Promise<number> {
|
||||||
// Check if node exists
|
if (!this.store) return -1;
|
||||||
const predicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES);
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
predicates.equalTo('name', name);
|
predicates.equalTo('name', name);
|
||||||
const resultSet = await store.query(predicates, ['id', 'mentions']);
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id']);
|
||||||
|
if (resultSet.goToNextRow()) {
|
||||||
let nodeId: number;
|
const id: number = resultSet.getLong(resultSet.getColumnIndex('id'));
|
||||||
if (resultSet.rowCount > 0) {
|
|
||||||
resultSet.goToFirstRow();
|
|
||||||
nodeId = resultSet.getLong(resultSet.getColumnIndex('id'));
|
|
||||||
const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions'));
|
|
||||||
resultSet.close();
|
resultSet.close();
|
||||||
|
return id;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
resultSet.close();
|
||||||
return nodeId;
|
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> {
|
async recall(queryIntent: string, seedEntities?: string[]): Promise<RecallResult> {
|
||||||
const store = this.assertStore();
|
if (!this.store) {
|
||||||
|
const empty: RecallResult = { data: { nodes: [], edges: [] } };
|
||||||
try {
|
return empty;
|
||||||
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}` };
|
|
||||||
}
|
}
|
||||||
|
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> {
|
async purge(criteria: CriteriaData): Promise<void> {
|
||||||
const store = this.assertStore();
|
if (!this.store) return;
|
||||||
let affected = 0;
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
|
await this.store.delete(predicates);
|
||||||
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 introspect(): Promise<RecallResult> {
|
async introspect(): Promise<RecallResult> {
|
||||||
const store = this.assertStore();
|
const nodes: NodeData[] = await this.getAllNodes();
|
||||||
|
const edges: EdgeData[] = await this.getAllEdges();
|
||||||
try {
|
const result: RecallResult = { data: { nodes, edges } };
|
||||||
// Get all nodes
|
return result;
|
||||||
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}` };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async archive(days: number): Promise<ArchiveResult> {
|
async archive(days: number): Promise<void> {
|
||||||
const store = this.assertStore();
|
// Archive old data - placeholder
|
||||||
|
|
||||||
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 cleanup(dryRun: boolean): Promise<CleanupResult> {
|
async cleanup(dryRun: boolean): Promise<CleanupResult> {
|
||||||
const store = this.assertStore();
|
const result: CleanupResult = { cleaned: 0 };
|
||||||
|
return result;
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Chat Operations ============
|
async saveChatMessage(role: string, content: string, tools?: string): Promise<void> {
|
||||||
|
if (!this.store) return;
|
||||||
async saveChatMessage(role: string, content: string, tools?: string): Promise<number> {
|
|
||||||
const store = this.assertStore();
|
|
||||||
const bucket: relationalStore.ValuesBucket = {
|
const bucket: relationalStore.ValuesBucket = {
|
||||||
'role': role,
|
'role': role,
|
||||||
'content': content,
|
'content': content,
|
||||||
|
'tools': tools || null,
|
||||||
|
'created_at': new Date().toISOString()
|
||||||
};
|
};
|
||||||
if (tools) {
|
await this.store.insert('chat_records', bucket);
|
||||||
bucket['tools'] = tools;
|
|
||||||
}
|
|
||||||
return await store.insert(Constants.TABLE_CHAT, bucket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChatHistory(limit: number = 50): Promise<ChatMessage[]> {
|
async getChatHistory(limit?: number): Promise<ChatMessage[]> {
|
||||||
const store = this.assertStore();
|
if (!this.store) return [];
|
||||||
const rs = await store.querySql(
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||||
`SELECT * FROM chat_records ORDER BY id DESC LIMIT ${limit}`
|
predicates.orderByDesc('created_at');
|
||||||
);
|
if (limit) {
|
||||||
|
predicates.limitAs(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
|
|
||||||
}
|
}
|
||||||
rs.close();
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['role', 'content']);
|
||||||
|
const messages: ChatMessage[] = [];
|
||||||
return messages;
|
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> {
|
async clearChatHistory(): Promise<void> {
|
||||||
const store = this.assertStore();
|
if (!this.store) return;
|
||||||
await store.executeSql('DELETE FROM chat_records');
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('chat_records');
|
||||||
|
await this.store.delete(predicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Helper Methods ============
|
private async getAllNodes(): Promise<NodeData[]> {
|
||||||
|
if (!this.store) return [];
|
||||||
private rowToNode(rs: relationalStore.ResultSet): GraphNode {
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('nodes');
|
||||||
return {
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['id', 'name', 'type', 'mentions']);
|
||||||
id: rs.getLong(rs.getColumnIndex('id')),
|
const nodes: NodeData[] = [];
|
||||||
name: rs.getString(rs.getColumnIndex('name')),
|
while (resultSet.goToNextRow()) {
|
||||||
type: rs.getString(rs.getColumnIndex('type')),
|
const node: NodeData = {
|
||||||
mentions: rs.getLong(rs.getColumnIndex('mentions')),
|
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 {
|
private async getAllEdges(): Promise<EdgeData[]> {
|
||||||
return {
|
if (!this.store) return [];
|
||||||
id: rs.getLong(rs.getColumnIndex('id')),
|
const predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates('relations');
|
||||||
source: rs.getLong(rs.getColumnIndex('subject_id')),
|
const resultSet: relationalStore.ResultSet = await this.store.query(predicates, ['subject_id', 'relation', 'object_id', 'weight']);
|
||||||
target: rs.getLong(rs.getColumnIndex('object_id')),
|
const edges: EdgeData[] = [];
|
||||||
label: rs.getString(rs.getColumnIndex('relation')),
|
while (resultSet.goToNextRow()) {
|
||||||
weight: rs.getDouble(rs.getColumnIndex('weight')),
|
const edge: EdgeData = {
|
||||||
};
|
from: resultSet.getLong(resultSet.getColumnIndex('subject_id')),
|
||||||
}
|
to: resultSet.getLong(resultSet.getColumnIndex('object_id')),
|
||||||
|
label: resultSet.getString(resultSet.getColumnIndex('relation')),
|
||||||
private getNow(): string {
|
weight: resultSet.getDouble(resultSet.getColumnIndex('weight'))
|
||||||
const d = new Date();
|
};
|
||||||
const y = d.getFullYear();
|
edges.push(edge);
|
||||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
}
|
||||||
const da = String(d.getDate()).padStart(2, '0');
|
resultSet.close();
|
||||||
const h = String(d.getHours()).padStart(2, '0');
|
return edges;
|
||||||
const mi = String(d.getMinutes()).padStart(2, '0');
|
|
||||||
const s = String(d.getSeconds()).padStart(2, '0');
|
|
||||||
return `${y}-${mo}-${da} ${h}:${mi}:${s}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,175 +1,100 @@
|
|||||||
import http from '@ohos.net.http';
|
import http from '@ohos.net.http';
|
||||||
import dataPreferences from '@ohos.data.preferences';
|
import dataPreferences from '@ohos.data.preferences';
|
||||||
import { GraphDatabase, ChatMessage } from '../model/GraphDatabase';
|
import { GraphDatabase } from '../model/GraphDatabase';
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export struct ChatPage {
|
export struct ChatPage {
|
||||||
@State messages: ChatMessage[] = [];
|
@State messages: ChatMessage[] = [];
|
||||||
@State inputText: string = '';
|
@State inputText: string = '';
|
||||||
private db: GraphDatabase;
|
@Prop db: GraphDatabase;
|
||||||
private scroller: Scroller = new Scroller();
|
private scrollController: Scroller = new Scroller();
|
||||||
|
|
||||||
aboutToAppear() {
|
async aboutToAppear() {
|
||||||
this.loadHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadHistory() {
|
|
||||||
this.messages = await this.db.getChatHistory(50);
|
this.messages = await this.db.getChatHistory(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
if (!this.inputText.trim()) {
|
if (!this.inputText.trim()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMsg = this.inputText;
|
const userMessage: string = this.inputText;
|
||||||
this.inputText = '';
|
this.inputText = '';
|
||||||
|
|
||||||
// Save user message
|
await this.db.saveChatMessage('user', userMessage);
|
||||||
await this.db.saveChatMessage('user', userMsg);
|
const userMsg: ChatMessage = { role: 'user', content: userMessage };
|
||||||
|
this.messages = [...this.messages, userMsg];
|
||||||
|
|
||||||
// Update UI immediately
|
const pref: dataPreferences.Preferences =
|
||||||
this.messages = await this.db.getChatHistory(50);
|
await dataPreferences.getPreferences(getContext(this), 'trulymem_config');
|
||||||
|
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
|
||||||
// Call AI API
|
const model: string = String(await pref.get('model', 'deepseek-chat'));
|
||||||
try {
|
const apiKey: string = String(await pref.get('api_key', ''));
|
||||||
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 });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await httpRequest.request(
|
const httpRequest: http.HttpRequest = http.createHttp();
|
||||||
baseUrl.replace(/\/$/, '') + '/v1/chat/completions',
|
const response: http.HttpResponse = await httpRequest.request(baseUrl + '/chat/completions', {
|
||||||
{
|
method: http.RequestMethod.POST,
|
||||||
method: http.RequestMethod.POST,
|
header: {
|
||||||
header: {
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Authorization': 'Bearer ' + apiKey
|
||||||
'Authorization': 'Bearer ' + apiKey,
|
},
|
||||||
},
|
extraData: {
|
||||||
extraData: JSON.stringify({
|
model: model,
|
||||||
model: model,
|
messages: [{ role: 'user', content: userMessage }]
|
||||||
messages: msgs,
|
|
||||||
max_tokens: 2048,
|
|
||||||
stream: false,
|
|
||||||
}),
|
|
||||||
connectTimeout: 30000,
|
|
||||||
readTimeout: 60000,
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
httpRequest.destroy();
|
if (response.responseCode === 200) {
|
||||||
|
const data: object = JSON.parse(String(response.result));
|
||||||
const statusCode = response.responseCode;
|
const choicesArr: object[] = data['choices'] as object[];
|
||||||
if (statusCode !== 200) {
|
if (choicesArr && choicesArr.length > 0) {
|
||||||
return `API 请求失败 (${statusCode}): ${response.result.toString()}`;
|
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) {
|
||||||
const data = JSON.parse(response.result.toString());
|
console.error('HTTP request failed: ' + JSON.stringify(err));
|
||||||
if (data.choices && data.choices.length > 0) {
|
|
||||||
return data.choices[0].message.content;
|
|
||||||
}
|
|
||||||
return 'AI 返回为空';
|
|
||||||
} catch (e) {
|
|
||||||
httpRequest.destroy();
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
Column() {
|
Column() {
|
||||||
// Message list
|
List() {
|
||||||
List({ scroller: this.scroller }) {
|
|
||||||
ForEach(this.messages, (msg: ChatMessage) => {
|
ForEach(this.messages, (msg: ChatMessage) => {
|
||||||
ListItem() {
|
ListItem() {
|
||||||
if (msg.role === 'user') {
|
Column() {
|
||||||
Row() {
|
Text(msg.role).fontSize(12).fontColor('#666').width('100%')
|
||||||
Blank()
|
Text(msg.content).fontSize(16).width('100%').margin({ top: 4 })
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor(msg.role === 'user' ? '#E3F2FD' : '#F5F5F5')
|
||||||
|
.borderRadius(8)
|
||||||
|
.margin({ left: 8, right: 8, bottom: 8 })
|
||||||
}
|
}
|
||||||
}, (msg: ChatMessage) => msg.id.toString())
|
})
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
|
|
||||||
// Input area
|
|
||||||
Row() {
|
Row() {
|
||||||
TextArea({ text: this.inputText, placeholder: '输入消息...' })
|
TextArea({ text: this.inputText, placeholder: '输入消息...' })
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
.maxLines(4)
|
.onChange((v: string) => { this.inputText = v; })
|
||||||
.onChange((v: string) => {
|
|
||||||
this.inputText = v;
|
|
||||||
})
|
|
||||||
.margin({ left: 8, right: 4 })
|
|
||||||
.backgroundColor('#1e1e2e')
|
|
||||||
.fontColor('#ffffff')
|
|
||||||
|
|
||||||
Button('发送')
|
Button('发送')
|
||||||
.onClick(() => this.sendMessage())
|
.onClick(() => this.sendMessage())
|
||||||
.margin({ left: 4, right: 8 })
|
|
||||||
.backgroundColor('#0052cc')
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(64)
|
.padding(8)
|
||||||
.backgroundColor('#16162a')
|
|
||||||
.padding({ top: 8, bottom: 8 })
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
.backgroundColor('#0a0a1a')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,33 +4,19 @@ import { GraphDatabase } from '../model/GraphDatabase';
|
|||||||
@Component
|
@Component
|
||||||
export struct GraphPage {
|
export struct GraphPage {
|
||||||
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
||||||
private db: GraphDatabase;
|
@Prop db: GraphDatabase;
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
Column() {
|
Column() {
|
||||||
Web({ src: $rawfile('graph.html'), controller: this.controller })
|
Web({ src: $rawfile('graph.html'), controller: this.controller })
|
||||||
.javaScriptAccess(true)
|
.javaScriptAccess(true)
|
||||||
.onMessageReceive((msg) => {
|
|
||||||
if (msg.data === 'request_graph_data') {
|
|
||||||
this.sendGraphData();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
}
|
.onAppear(() => {
|
||||||
|
this.controller.refresh();
|
||||||
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import GraphPage from './GraphPage';
|
import { MainPage } from './MainPage';
|
||||||
import ChatPage from './ChatPage';
|
import { SettingsPage } from './SettingsPage';
|
||||||
import SettingsPage from './SettingsPage';
|
|
||||||
import { GraphDatabase } from '../model/GraphDatabase';
|
import { GraphDatabase } from '../model/GraphDatabase';
|
||||||
|
|
||||||
@Entry
|
@Entry
|
||||||
@ -17,14 +16,9 @@ struct Index {
|
|||||||
Column() {
|
Column() {
|
||||||
Tabs({ index: this.currentIndex, barPosition: BarPosition.End }) {
|
Tabs({ index: this.currentIndex, barPosition: BarPosition.End }) {
|
||||||
TabContent() {
|
TabContent() {
|
||||||
GraphPage({ db: this.db })
|
MainPage({ db: this.db })
|
||||||
}
|
}
|
||||||
.tabBar('🌌 星图')
|
.tabBar('🌌 TrulyMEM')
|
||||||
|
|
||||||
TabContent() {
|
|
||||||
ChatPage({ db: this.db })
|
|
||||||
}
|
|
||||||
.tabBar('💬 聊天')
|
|
||||||
|
|
||||||
TabContent() {
|
TabContent() {
|
||||||
SettingsPage()
|
SettingsPage()
|
||||||
@ -33,9 +27,7 @@ struct Index {
|
|||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
.onChange((index: number) => {
|
.onChange((index: number) => { this.currentIndex = index; })
|
||||||
this.currentIndex = index;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
|
|||||||
21
harmonyos/entry/src/main/ets/pages/MainPage.ets
Normal file
21
harmonyos/entry/src/main/ets/pages/MainPage.ets
Normal 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%')
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,93 +5,56 @@ export struct SettingsPage {
|
|||||||
@State baseUrl: string = '';
|
@State baseUrl: string = '';
|
||||||
@State model: string = '';
|
@State model: string = '';
|
||||||
@State apiKey: string = '';
|
@State apiKey: string = '';
|
||||||
private pref: dataPreferences.Preferences;
|
private pref?: dataPreferences.Preferences;
|
||||||
|
|
||||||
async aboutToAppear() {
|
async aboutToAppear() {
|
||||||
const ctx = getContext(this);
|
const ctx = getContext(this);
|
||||||
this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
|
this.pref = await dataPreferences.getPreferences(ctx, 'trulymem_config');
|
||||||
this.baseUrl = await this.pref.get('base_url', 'https://api.deepseek.com');
|
this.baseUrl = String(await this.pref.get('base_url', 'https://api.deepseek.com'));
|
||||||
this.model = await this.pref.get('model', 'deepseek-chat');
|
this.model = String(await this.pref.get('model', 'deepseek-chat'));
|
||||||
this.apiKey = await this.pref.get('api_key', '');
|
this.apiKey = String(await this.pref.get('api_key', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBaseUrlChange(value: string) {
|
async onBaseUrlChange(value: string) {
|
||||||
this.baseUrl = value;
|
this.baseUrl = value;
|
||||||
await this.pref.put('base_url', value);
|
await this.pref?.put('base_url', value);
|
||||||
await this.pref.flush();
|
await this.pref?.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onModelChange(value: string) {
|
async onModelChange(value: string) {
|
||||||
this.model = value;
|
this.model = value;
|
||||||
await this.pref.put('model', value);
|
await this.pref?.put('model', value);
|
||||||
await this.pref.flush();
|
await this.pref?.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApiKeyChange(value: string) {
|
async onApiKeyChange(value: string) {
|
||||||
this.apiKey = value;
|
this.apiKey = value;
|
||||||
await this.pref.put('api_key', value);
|
await this.pref?.put('api_key', value);
|
||||||
await this.pref.flush();
|
await this.pref?.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
Column() {
|
Column() {
|
||||||
Text('API 配置')
|
Text('API 配置').fontSize(24).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 16 })
|
||||||
.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('Base URL').fontSize(14).width('100%').margin({ left: 16 })
|
||||||
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
|
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
|
||||||
.onChange((v: string) => this.onBaseUrlChange(v))
|
.onChange((v: string) => { this.onBaseUrlChange(v); })
|
||||||
.margin({ left: 16, right: 16, bottom: 16 })
|
.margin({ left: 16, right: 16, bottom: 12 })
|
||||||
.backgroundColor('#1e1e2e')
|
|
||||||
.fontColor('#ffffff')
|
|
||||||
.placeholderColor('#555566')
|
|
||||||
|
|
||||||
// Model ID
|
|
||||||
Text('Model ID')
|
|
||||||
.fontSize(14)
|
|
||||||
.fontColor('#888899')
|
|
||||||
.width('100%')
|
|
||||||
.margin({ left: 16, bottom: 4 })
|
|
||||||
|
|
||||||
|
Text('Model ID').fontSize(14).width('100%').margin({ left: 16 })
|
||||||
TextInput({ placeholder: 'deepseek-chat', text: this.model })
|
TextInput({ placeholder: 'deepseek-chat', text: this.model })
|
||||||
.onChange((v: string) => this.onModelChange(v))
|
.onChange((v: string) => { this.onModelChange(v); })
|
||||||
.margin({ left: 16, right: 16, bottom: 16 })
|
.margin({ left: 16, right: 16, bottom: 12 })
|
||||||
.backgroundColor('#1e1e2e')
|
|
||||||
.fontColor('#ffffff')
|
|
||||||
.placeholderColor('#555566')
|
|
||||||
|
|
||||||
// API Key
|
|
||||||
Text('API Key')
|
|
||||||
.fontSize(14)
|
|
||||||
.fontColor('#888899')
|
|
||||||
.width('100%')
|
|
||||||
.margin({ left: 16, bottom: 4 })
|
|
||||||
|
|
||||||
|
Text('API Key').fontSize(14).width('100%').margin({ left: 16 })
|
||||||
TextInput({ placeholder: 'sk-...', text: this.apiKey })
|
TextInput({ placeholder: 'sk-...', text: this.apiKey })
|
||||||
.type(InputType.Password)
|
.type(InputType.Password)
|
||||||
.onChange((v: string) => this.onApiKeyChange(v))
|
.onChange((v: string) => { this.onApiKeyChange(v); })
|
||||||
.margin({ left: 16, right: 16, bottom: 16 })
|
.margin({ left: 16, right: 16, bottom: 12 })
|
||||||
.backgroundColor('#1e1e2e')
|
|
||||||
.fontColor('#ffffff')
|
|
||||||
.placeholderColor('#555566')
|
|
||||||
|
|
||||||
Text('配置会自动保存')
|
|
||||||
.fontSize(12)
|
|
||||||
.fontColor('#666677')
|
|
||||||
.margin({ top: 32 })
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.backgroundColor('#0a0a1a')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"color": [
|
||||||
|
{
|
||||||
|
"name": "start_window_background",
|
||||||
|
"value": "#FFFFFF"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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 |
@ -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 |
302
harmonyos/entry/src/main/resources/rawfile/graph.html
Normal file
302
harmonyos/entry/src/main/resources/rawfile/graph.html
Normal 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>
|
||||||
@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"modelVersion": "5.0.5",
|
"modelVersion": "5.0.5",
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
"execution": {}
|
},
|
||||||
|
"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
6
harmonyos/hvigorfile.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { appTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: appTasks,
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
@ -1,2 +1,2 @@
|
|||||||
hwsdk.dir=/opt/harmonyos/ohos-sdk/linux/command-line-tools/sdk/default
|
hwsdk.dir=/home/program/tools/command-line-tools/sdk/default
|
||||||
sdk.dir=/opt/harmonyos/ohos-sdk/linux/command-line-tools/sdk/default
|
sdk.dir=/home/program/tools/command-line-tools/sdk/default
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"modelVersion": "5.0.5",
|
"modelVersion": "5.0.5",
|
||||||
"description": "TrulyMEM - True Memory Engine for HarmonyOS",
|
"description": "Please describe the basic information.",
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohos/hypium": "1.0.24",
|
"@ohos/hypium": "1.0.24",
|
||||||
"@ohos/hamock": "1.0.0"
|
"@ohos/hamock": "1.0.0"
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
17
harmonyos/trulymem-core/BuildProfile.ets
Normal file
17
harmonyos/trulymem-core/BuildProfile.ets
Normal 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;
|
||||||
|
}
|
||||||
2
harmonyos/trulymem-core/Index.ets
Normal file
2
harmonyos/trulymem-core/Index.ets
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// TrulyMEM Core - 导出核心功能
|
||||||
|
export { GraphDatabase } from './src/main/ets/GraphDatabase';
|
||||||
41
harmonyos/trulymem-core/build-profile.json5
Normal file
41
harmonyos/trulymem-core/build-profile.json5
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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
Reference in New Issue
Block a user