refactor: move HarmonyOS project files from harmony/ to branch root
This commit is contained in:
44
entry/src/main/ets/common/Constants.ets
Normal file
44
entry/src/main/ets/common/Constants.ets
Normal file
@ -0,0 +1,44 @@
|
||||
export class Constants {
|
||||
static readonly DB_NAME: string = 'trulymem.db';
|
||||
static readonly CONFIG_PREF_NAME: string = 'trulymem_config';
|
||||
static readonly DEFAULT_BASE_URL: string = 'https://api.deepseek.com';
|
||||
static readonly DEFAULT_MODEL: string = 'deepseek-chat';
|
||||
static readonly SECURITY_LEVEL: number = 1; // S1
|
||||
|
||||
// Table names
|
||||
static readonly TABLE_NODES: string = 'nodes';
|
||||
static readonly TABLE_RELATIONS: string = 'relations';
|
||||
static readonly TABLE_CHAT: string = 'chat_records';
|
||||
|
||||
// SQL definitions
|
||||
static readonly SQL_CREATE_NODES: string = `
|
||||
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 DEFAULT (datetime('now','localtime')),
|
||||
updated_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
)`;
|
||||
|
||||
static readonly SQL_CREATE_RELATIONS: string = `
|
||||
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 DEFAULT (datetime('now','localtime')),
|
||||
FOREIGN KEY (subject_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (object_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
static readonly SQL_CREATE_CHAT: string = `
|
||||
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 DEFAULT (datetime('now','localtime'))
|
||||
)`;
|
||||
}
|
||||
36
entry/src/main/ets/entryability/EntryAbility.ets
Normal file
36
entry/src/main/ets/entryability/EntryAbility.ets
Normal file
@ -0,0 +1,36 @@
|
||||
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
|
||||
import UIAbility from '@ohos.app.ability.UIAbility';
|
||||
import Window from '@ohos.window';
|
||||
import 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');
|
||||
}
|
||||
}
|
||||
237
entry/src/main/ets/model/GraphDatabase.ets
Normal file
237
entry/src/main/ets/model/GraphDatabase.ets
Normal file
@ -0,0 +1,237 @@
|
||||
import relationalStore from '@ohos.data.relationalStore';
|
||||
import { 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;
|
||||
}
|
||||
}
|
||||
100
entry/src/main/ets/pages/ChatPage.ets
Normal file
100
entry/src/main/ets/pages/ChatPage.ets
Normal file
@ -0,0 +1,100 @@
|
||||
import http from '@ohos.net.http';
|
||||
import dataPreferences from '@ohos.data.preferences';
|
||||
import { GraphDatabase } from '../model/GraphDatabase';
|
||||
|
||||
interface ChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
@Component
|
||||
export struct ChatPage {
|
||||
@State messages: ChatMessage[] = [];
|
||||
@State inputText: string = '';
|
||||
@Prop db: GraphDatabase;
|
||||
private scrollController: Scroller = new 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));
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
List() {
|
||||
ForEach(this.messages, (msg: ChatMessage) => {
|
||||
ListItem() {
|
||||
Column() {
|
||||
Text(msg.role).fontSize(12).fontColor('#666').width('100%')
|
||||
Text(msg.content).fontSize(16).width('100%').margin({ top: 4 })
|
||||
}
|
||||
.padding(12)
|
||||
.backgroundColor(msg.role === 'user' ? '#E3F2FD' : '#F5F5F5')
|
||||
.borderRadius(8)
|
||||
.margin({ left: 8, right: 8, bottom: 8 })
|
||||
}
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
|
||||
Row() {
|
||||
TextArea({ text: this.inputText, placeholder: '输入消息...' })
|
||||
.layoutWeight(1)
|
||||
.onChange((v: string) => { this.inputText = v; })
|
||||
Button('发送')
|
||||
.onClick(() => this.sendMessage())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(8)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
22
entry/src/main/ets/pages/GraphPage.ets
Normal file
22
entry/src/main/ets/pages/GraphPage.ets
Normal file
@ -0,0 +1,22 @@
|
||||
import web_webview from '@ohos.web.webview';
|
||||
import { GraphDatabase } from '../model/GraphDatabase';
|
||||
|
||||
@Component
|
||||
export struct GraphPage {
|
||||
private controller: web_webview.WebviewController = new web_webview.WebviewController();
|
||||
@Prop db: GraphDatabase;
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Web({ src: $rawfile('graph.html'), controller: this.controller })
|
||||
.javaScriptAccess(true)
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.onAppear(() => {
|
||||
this.controller.refresh();
|
||||
})
|
||||
}
|
||||
}
|
||||
35
entry/src/main/ets/pages/Index.ets
Normal file
35
entry/src/main/ets/pages/Index.ets
Normal file
@ -0,0 +1,35 @@
|
||||
import { MainPage } from './MainPage';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { GraphDatabase } from '../model/GraphDatabase';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct Index {
|
||||
@State currentIndex: number = 0;
|
||||
private db: GraphDatabase = new GraphDatabase();
|
||||
|
||||
aboutToAppear() {
|
||||
this.db.init(getContext(this));
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Tabs({ index: this.currentIndex, barPosition: BarPosition.End }) {
|
||||
TabContent() {
|
||||
MainPage({ db: this.db })
|
||||
}
|
||||
.tabBar('🌌 TrulyMEM')
|
||||
|
||||
TabContent() {
|
||||
SettingsPage()
|
||||
}
|
||||
.tabBar('⚙ 设置')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.onChange((index: number) => { this.currentIndex = index; })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
21
entry/src/main/ets/pages/MainPage.ets
Normal file
21
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%')
|
||||
}
|
||||
}
|
||||
60
entry/src/main/ets/pages/SettingsPage.ets
Normal file
60
entry/src/main/ets/pages/SettingsPage.ets
Normal file
@ -0,0 +1,60 @@
|
||||
import dataPreferences from '@ohos.data.preferences';
|
||||
|
||||
@Component
|
||||
export struct SettingsPage {
|
||||
@State baseUrl: string = '';
|
||||
@State model: string = '';
|
||||
@State apiKey: string = '';
|
||||
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();
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Text('API 配置').fontSize(24).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 16 })
|
||||
|
||||
Text('Base URL').fontSize(14).width('100%').margin({ left: 16 })
|
||||
TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl })
|
||||
.onChange((v: string) => { this.onBaseUrlChange(v); })
|
||||
.margin({ left: 16, right: 16, bottom: 12 })
|
||||
|
||||
Text('Model ID').fontSize(14).width('100%').margin({ left: 16 })
|
||||
TextInput({ placeholder: 'deepseek-chat', text: this.model })
|
||||
.onChange((v: string) => { this.onModelChange(v); })
|
||||
.margin({ left: 16, right: 16, bottom: 12 })
|
||||
|
||||
Text('API Key').fontSize(14).width('100%').margin({ left: 16 })
|
||||
TextInput({ placeholder: 'sk-...', text: this.apiKey })
|
||||
.type(InputType.Password)
|
||||
.onChange((v: string) => { this.onApiKeyChange(v); })
|
||||
.margin({ left: 16, right: 16, bottom: 12 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
38
entry/src/main/module.json5
Normal file
38
entry/src/main/module.json5
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
8
entry/src/main/resources/base/element/color.json
Normal file
8
entry/src/main/resources/base/element/color.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"color": [
|
||||
{
|
||||
"name": "start_window_background",
|
||||
"value": "#FFFFFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
entry/src/main/resources/base/element/string.json
Normal file
20
entry/src/main/resources/base/element/string.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "app_name",
|
||||
"value": "TrulyMEM"
|
||||
},
|
||||
{
|
||||
"name": "module_desc",
|
||||
"value": "True Memory Engine"
|
||||
},
|
||||
{
|
||||
"name": "EntryAbility_desc",
|
||||
"value": "TrulyMEM Main Ability"
|
||||
},
|
||||
{
|
||||
"name": "EntryAbility_label",
|
||||
"value": "TrulyMEM"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
entry/src/main/resources/base/media/layered_image.svg
Normal file
5
entry/src/main/resources/base/media/layered_image.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<rect width="200" height="200" rx="30" fill="#6366f1"/>
|
||||
<text x="100" y="130" font-family="Arial" font-size="80" fill="white" text-anchor="middle" font-weight="bold">T</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
5
entry/src/main/resources/base/media/startIcon.svg
Normal file
5
entry/src/main/resources/base/media/startIcon.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<rect width="200" height="200" rx="20" fill="#6366f1"/>
|
||||
<text x="100" y="130" font-family="Arial" font-size="60" fill="white" text-anchor="middle" font-weight="bold">TM</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 314 B |
5
entry/src/main/resources/base/profile/main_pages.json
Normal file
5
entry/src/main/resources/base/profile/main_pages.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"src": [
|
||||
"pages/Index"
|
||||
]
|
||||
}
|
||||
302
entry/src/main/resources/rawfile/graph.html
Normal file
302
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>
|
||||
Reference in New Issue
Block a user