diff --git a/harmonyos/AppScope/app.json5 b/harmonyos/AppScope/app.json5 new file mode 100644 index 0000000..fffa386 --- /dev/null +++ b/harmonyos/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.trulymem.app", + "vendor": "trulymem", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": ":layered_image", + "label": ":app_name" + } +} diff --git a/harmonyos/build-profile.json5 b/harmonyos/build-profile.json5 new file mode 100644 index 0000000..0009856 --- /dev/null +++ b/harmonyos/build-profile.json5 @@ -0,0 +1,19 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "6.0.0.48", + "compileSdkVersion": "6.0.0.48" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry" + } + ] +} diff --git a/harmonyos/entry/build-profile.json5 b/harmonyos/entry/build-profile.json5 new file mode 100644 index 0000000..bc7f9f9 --- /dev/null +++ b/harmonyos/entry/build-profile.json5 @@ -0,0 +1,10 @@ +{ + "apiType": "stageMode", + "buildOption": {}, + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + } + ] +} diff --git a/harmonyos/entry/src/main/ets/common/Constants.ets b/harmonyos/entry/src/main/ets/common/Constants.ets new file mode 100644 index 0000000..374db53 --- /dev/null +++ b/harmonyos/entry/src/main/ets/common/Constants.ets @@ -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')) + )`; +} diff --git a/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets b/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..aedb0a4 --- /dev/null +++ b/harmonyos/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,36 @@ +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; +import UIAbility from '@ohos.app.ability.UIAbility'; +import Window from '@ohos.app.ability.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'); + } +} diff --git a/harmonyos/entry/src/main/ets/model/GraphDatabase.ets b/harmonyos/entry/src/main/ets/model/GraphDatabase.ets new file mode 100644 index 0000000..ec30b8f --- /dev/null +++ b/harmonyos/entry/src/main/ets/model/GraphDatabase.ets @@ -0,0 +1,448 @@ +import relationalStore from '@ohos.data.relationalStore'; +import { Constants } from '../common/Constants'; + +export interface TripletInput { + subject: string; + relation: string; + object: string; +} + +export interface GraphNode { + id: number; + name: string; + type: string; + mentions: number; +} + +export interface GraphEdge { + id: number; + source: number; + target: number; + label: string; + weight: number; +} + +export interface GraphData { + nodes: GraphNode[]; + edges: GraphEdge[]; +} + +export interface CommitResult { + success: boolean; + message: string; + count?: number; +} + +export interface RecallResult { + success: boolean; + data: GraphData; + message: string; +} + +export interface PurgeCriteria { + subject?: string; + target?: string; + relation?: string; +} + +export interface PurgeResult { + success: boolean; + message: string; + affected?: number; +} + +export interface ArchiveResult { + success: boolean; + message: string; + archived: number; +} + +export interface CleanupResult { + success: boolean; + message: string; + deleted: number; +} + +export interface ChatMessage { + id: number; + role: string; + content: string; + tools?: string; + created_at: string; +} + +export class GraphDatabase { + private store: relationalStore.RdbStore | null = null; + + async init(context: any): Promise { + const STORE_CONFIG: relationalStore.StoreConfig = { + name: Constants.DB_NAME, + securityLevel: Constants.SECURITY_LEVEL, + }; + + this.store = await relationalStore.getRdbStore(context, STORE_CONFIG); + await this.store.executeSql(Constants.SQL_CREATE_NODES); + await this.store.executeSql(Constants.SQL_CREATE_RELATIONS); + await this.store.executeSql(Constants.SQL_CREATE_CHAT); + console.info('GraphDatabase initialized successfully'); + } + + private assertStore(): relationalStore.RdbStore { + if (!this.store) { + throw new Error('GraphDatabase not initialized. Call init() first.'); + } + return this.store; + } + + // ============ Memory Operations ============ + + async commit(triplets: TripletInput[]): Promise { + const store = this.assertStore(); + let count = 0; + + try { + await store.beginTransaction(); + + for (const t of triplets) { + // Upsert subject node + const subjId = await this.upsertNode(store, t.subject); + + // Upsert object node + const objId = await this.upsertNode(store, t.object); + + // Create relation + const bucket: relationalStore.ValuesBucket = { + 'subject_id': subjId, + 'relation': t.relation, + 'object_id': objId, + 'weight': 1.0, + }; + await store.insert(Constants.TABLE_RELATIONS, bucket); + count++; + } + + await store.commit(); + return { success: true, message: `Committed ${count} triplets`, count }; + } catch (e) { + await store.rollBack(); + return { success: false, message: `Commit failed: ${e}` }; + } + } + + private async upsertNode(store: relationalStore.RdbStore, name: string): Promise { + // Check if node exists + const predicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES); + predicates.equalTo('name', name); + const resultSet = await store.query(predicates, ['id', 'mentions']); + + let nodeId: number; + if (resultSet.rowCount > 0) { + resultSet.goToFirstRow(); + nodeId = resultSet.getLong(resultSet.getColumnIndex('id')); + const mentions = resultSet.getLong(resultSet.getColumnIndex('mentions')); + resultSet.close(); + + // Update mentions + updated_at + const updateBucket: relationalStore.ValuesBucket = { + 'mentions': mentions + 1, + 'updated_at': this.getNow(), + }; + const updatePredicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES); + updatePredicates.equalTo('id', nodeId); + await store.update(updateBucket, updatePredicates); + } else { + resultSet.close(); + const insertBucket: relationalStore.ValuesBucket = { + 'name': name, + 'type': 'concept', + 'mentions': 1, + 'created_at': this.getNow(), + 'updated_at': this.getNow(), + }; + nodeId = await store.insert(Constants.TABLE_NODES, insertBucket); + } + + return nodeId; + } + + async recall(queryIntent: string, seedEntities?: string[]): Promise { + const store = this.assertStore(); + + try { + if (seedEntities && seedEntities.length > 0) { + // Filter by seed entities + const placeholders = seedEntities.map(() => '?').join(','); + const nodeSql = `SELECT * FROM nodes WHERE name IN (${placeholders}) ORDER BY mentions DESC`; + const nodeResultSet = await store.querySql(nodeSql, seedEntities); + + const nodeIds: number[] = []; + const nodes: GraphNode[] = []; + while (nodeResultSet.goToNextRow()) { + const node = this.rowToNode(nodeResultSet); + nodes.push(node); + nodeIds.push(node.id); + } + nodeResultSet.close(); + + if (nodeIds.length === 0) { + return { success: true, data: { nodes: [], edges: [] }, message: 'No results found' }; + } + + // Get related edges + const edgeIdPlaceholders = nodeIds.map(() => '?').join(','); + const edgeParams = nodeIds.map(id => id.toString()); + const edgeSql = `SELECT * FROM relations WHERE subject_id IN (${edgeIdPlaceholders}) OR object_id IN (${edgeIdPlaceholders})`; + const edgeResultSet = await store.querySql(edgeSql, edgeParams); + + const edges: GraphEdge[] = []; + while (edgeResultSet.goToNextRow()) { + edges.push(this.rowToEdge(edgeResultSet)); + } + edgeResultSet.close(); + + // Collect all connected node IDs + const allNodeIds = new Set(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}` }; + } + } + + async purge(criteria: PurgeCriteria): Promise { + const store = this.assertStore(); + let affected = 0; + + try { + if (criteria.target) { + // Delete by target node name + const nodePredicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES); + nodePredicates.equalTo('name', criteria.target); + const rs = await store.query(nodePredicates, ['id']); + while (rs.goToNextRow()) { + const nodeId = rs.getLong(rs.getColumnIndex('id')); + // Delete relations involving this node + const relPredicates1 = new relationalStore.RdbPredicates(Constants.TABLE_RELATIONS); + relPredicates1.equalTo('subject_id', nodeId); + await store.delete(relPredicates1); + + const relPredicates2 = new relationalStore.RdbPredicates(Constants.TABLE_RELATIONS); + relPredicates2.equalTo('object_id', nodeId); + await store.delete(relPredicates2); + + // Delete the node itself + const delPredicates = new relationalStore.RdbPredicates(Constants.TABLE_NODES); + delPredicates.equalTo('id', nodeId); + affected += await store.delete(delPredicates); + } + rs.close(); + } + + if (criteria.subject) { + // More sophisticated deletion could be added here + } + + return { + success: true, + message: `Purged ${affected} nodes`, + affected, + }; + } catch (e) { + return { success: false, message: `Purge failed: ${e}` }; + } + } + + async introspect(): Promise { + const store = this.assertStore(); + + try { + // Get all nodes + const nodeRs = await store.querySql('SELECT * FROM nodes ORDER BY mentions DESC LIMIT 500'); + const nodes: GraphNode[] = []; + while (nodeRs.goToNextRow()) { + nodes.push(this.rowToNode(nodeRs)); + } + nodeRs.close(); + + // Get all edges + const edgeRs = await store.querySql('SELECT * FROM relations ORDER BY weight DESC LIMIT 1000'); + const edges: GraphEdge[] = []; + while (edgeRs.goToNextRow()) { + edges.push(this.rowToEdge(edgeRs)); + } + edgeRs.close(); + + return { + success: true, + data: { nodes, edges }, + message: `Found ${nodes.length} nodes, ${edges.length} edges`, + }; + } catch (e) { + return { success: false, data: { nodes: [], edges: [] }, message: `Introspect failed: ${e}` }; + } + } + + async archive(days: number): Promise { + const store = this.assertStore(); + + try { + const sql = `DELETE FROM relations WHERE created_at < datetime('now', '-${days} days')`; + const changes = await store.executeSql(sql); + return { success: true, message: `Archived relations older than ${days} days`, archived: changes }; + } catch (e) { + return { success: false, message: `Archive failed: ${e}`, archived: 0 }; + } + } + + async cleanup(dryRun: boolean): Promise { + const store = this.assertStore(); + + try { + if (dryRun) { + const rs = await store.querySql(` + SELECT COUNT(*) as cnt FROM nodes n + WHERE NOT EXISTS (SELECT 1 FROM relations WHERE subject_id = n.id OR object_id = n.id) + AND n.name NOT IN ('_ROOT_', '_UNKNOWN_') + `); + rs.goToFirstRow(); + const count = rs.getLong(rs.getColumnIndex('cnt')); + rs.close(); + return { success: true, message: `Dry run: ${count} orphaned nodes would be deleted`, deleted: count }; + } + + const rs = await store.querySql(` + SELECT id FROM nodes n + WHERE NOT EXISTS (SELECT 1 FROM relations WHERE subject_id = n.id OR object_id = n.id) + AND n.name NOT IN ('_ROOT_', '_UNKNOWN_') + `); + + const ids: number[] = []; + while (rs.goToNextRow()) { + ids.push(rs.getLong(rs.getColumnIndex('id'))); + } + rs.close(); + + if (ids.length === 0) { + return { success: true, message: 'No orphaned nodes found', deleted: 0 }; + } + + const placeholders = ids.map(() => '?').join(','); + const idParams = ids.map(id => id.toString()); + await store.executeSql(`DELETE FROM nodes WHERE id IN (${placeholders})`, idParams); + + return { success: true, message: `Cleaned up ${ids.length} orphaned nodes`, deleted: ids.length }; + } catch (e) { + return { success: false, message: `Cleanup failed: ${e}`, deleted: 0 }; + } + } + + // ============ Chat Operations ============ + + async saveChatMessage(role: string, content: string, tools?: string): Promise { + const store = this.assertStore(); + const bucket: relationalStore.ValuesBucket = { + 'role': role, + 'content': content, + }; + if (tools) { + bucket['tools'] = tools; + } + return await store.insert(Constants.TABLE_CHAT, bucket); + } + + async getChatHistory(limit: number = 50): Promise { + const store = this.assertStore(); + const rs = await store.querySql( + `SELECT * FROM chat_records ORDER BY id DESC LIMIT ${limit}` + ); + + const messages: ChatMessage[] = []; + while (rs.goToNextRow()) { + const msg: ChatMessage = { + id: rs.getLong(rs.getColumnIndex('id')), + role: rs.getString(rs.getColumnIndex('role')), + content: rs.getString(rs.getColumnIndex('content')), + created_at: rs.getString(rs.getColumnIndex('created_at')), + }; + + const toolsIdx = rs.getColumnIndex('tools'); + if (toolsIdx >= 0) { + msg.tools = rs.getString(toolsIdx); + } + + messages.unshift(msg); // Reverse to chronological order + } + rs.close(); + + return messages; + } + + async clearChatHistory(): Promise { + const store = this.assertStore(); + await store.executeSql('DELETE FROM chat_records'); + } + + // ============ Helper Methods ============ + + private rowToNode(rs: relationalStore.ResultSet): GraphNode { + return { + id: rs.getLong(rs.getColumnIndex('id')), + name: rs.getString(rs.getColumnIndex('name')), + type: rs.getString(rs.getColumnIndex('type')), + mentions: rs.getLong(rs.getColumnIndex('mentions')), + }; + } + + private rowToEdge(rs: relationalStore.ResultSet): GraphEdge { + return { + id: rs.getLong(rs.getColumnIndex('id')), + source: rs.getLong(rs.getColumnIndex('subject_id')), + target: rs.getLong(rs.getColumnIndex('object_id')), + label: rs.getString(rs.getColumnIndex('relation')), + weight: rs.getDouble(rs.getColumnIndex('weight')), + }; + } + + private getNow(): string { + const d = new Date(); + const y = d.getFullYear(); + const mo = String(d.getMonth() + 1).padStart(2, '0'); + const da = String(d.getDate()).padStart(2, '0'); + const h = String(d.getHours()).padStart(2, '0'); + const mi = String(d.getMinutes()).padStart(2, '0'); + const s = String(d.getSeconds()).padStart(2, '0'); + return `${y}-${mo}-${da} ${h}:${mi}:${s}`; + } +} diff --git a/harmonyos/entry/src/main/ets/pages/ChatPage.ets b/harmonyos/entry/src/main/ets/pages/ChatPage.ets new file mode 100644 index 0000000..d332cf3 --- /dev/null +++ b/harmonyos/entry/src/main/ets/pages/ChatPage.ets @@ -0,0 +1,175 @@ +import http from '@ohos.net.http'; +import dataPreferences from '@ohos.data.preferences'; +import { GraphDatabase, ChatMessage } from '../model/GraphDatabase'; + +@Component +export struct ChatPage { + @State messages: ChatMessage[] = []; + @State inputText: string = ''; + private db: GraphDatabase; + private scroller: Scroller = new Scroller(); + + aboutToAppear() { + this.loadHistory(); + } + + async loadHistory() { + this.messages = await this.db.getChatHistory(50); + } + + async sendMessage() { + if (!this.inputText.trim()) { + return; + } + + const userMsg = this.inputText; + this.inputText = ''; + + // Save user message + await this.db.saveChatMessage('user', userMsg); + + // Update UI immediately + this.messages = await this.db.getChatHistory(50); + + // Call AI API + try { + const reply = await this.callAIApi(userMsg); + await this.db.saveChatMessage('assistant', reply); + this.messages = await this.db.getChatHistory(50); + } catch (e) { + await this.db.saveChatMessage('assistant', `Error: ${e.message || e}`); + this.messages = await this.db.getChatHistory(50); + } + } + + async callAIApi(prompt: string): Promise { + 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 { + const response = await httpRequest.request( + baseUrl.replace(/\/$/, '') + '/v1/chat/completions', + { + method: http.RequestMethod.POST, + header: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + apiKey, + }, + extraData: JSON.stringify({ + model: model, + messages: msgs, + max_tokens: 2048, + stream: false, + }), + connectTimeout: 30000, + readTimeout: 60000, + } + ); + + httpRequest.destroy(); + + const statusCode = response.responseCode; + if (statusCode !== 200) { + return `API 请求失败 (${statusCode}): ${response.result.toString()}`; + } + + const data = JSON.parse(response.result.toString()); + if (data.choices && data.choices.length > 0) { + return data.choices[0].message.content; + } + return 'AI 返回为空'; + } catch (e) { + httpRequest.destroy(); + throw e; + } + } + + build() { + Column() { + // Message list + List({ scroller: this.scroller }) { + ForEach(this.messages, (msg: ChatMessage) => { + ListItem() { + if (msg.role === 'user') { + Row() { + Blank() + Column() { + Text(msg.content) + .fontSize(16) + .fontColor('#ffffff') + .padding(12) + } + .backgroundColor('#0052cc') + .borderRadius(12) + .maxWidth('80%') + } + .width('100%') + .padding({ left: 16, right: 16, top: 4, bottom: 4 }) + } else { + Row() { + Column() { + Text(msg.content) + .fontSize(16) + .fontColor('#e0e0e0') + .padding(12) + } + .backgroundColor('#2d2d3a') + .borderRadius(12) + .maxWidth('80%') + Blank() + } + .width('100%') + .padding({ left: 16, right: 16, top: 4, bottom: 4 }) + } + } + }, (msg: ChatMessage) => msg.id.toString()) + } + .width('100%') + .layoutWeight(1) + + // Input area + Row() { + TextArea({ text: this.inputText, placeholder: '输入消息...' }) + .layoutWeight(1) + .maxLines(4) + .onChange((v: string) => { + this.inputText = v; + }) + .margin({ left: 8, right: 4 }) + .backgroundColor('#1e1e2e') + .fontColor('#ffffff') + + Button('发送') + .onClick(() => this.sendMessage()) + .margin({ left: 4, right: 8 }) + .backgroundColor('#0052cc') + } + .width('100%') + .height(64) + .backgroundColor('#16162a') + .padding({ top: 8, bottom: 8 }) + } + .width('100%') + .height('100%') + .backgroundColor('#0a0a1a') + } +} diff --git a/harmonyos/entry/src/main/ets/pages/GraphPage.ets b/harmonyos/entry/src/main/ets/pages/GraphPage.ets new file mode 100644 index 0000000..3417f7d --- /dev/null +++ b/harmonyos/entry/src/main/ets/pages/GraphPage.ets @@ -0,0 +1,36 @@ +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(); + private db: GraphDatabase; + + build() { + Column() { + Web({ src: $rawfile('graph.html'), controller: this.controller }) + .javaScriptAccess(true) + .onMessageReceive((msg) => { + if (msg.data === 'request_graph_data') { + this.sendGraphData(); + } + }) + .width('100%') + .height('100%') + } + .width('100%') + .height('100%') + } + + async sendGraphData() { + const result = await this.db.introspect(); + if (result.success) { + const jsonStr = JSON.stringify(result.data); + this.controller.runJavaScriptExt('loadGraphData(' + jsonStr + ')', (error, data) => { + if (error) { + console.error('runJavaScriptExt error: ' + JSON.stringify(error)); + } + }); + } + } +} diff --git a/harmonyos/entry/src/main/ets/pages/Index.ets b/harmonyos/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..5c2221e --- /dev/null +++ b/harmonyos/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,43 @@ +import GraphPage from './GraphPage'; +import ChatPage from './ChatPage'; +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() { + GraphPage({ db: this.db }) + } + .tabBar('🌌 星图') + + TabContent() { + ChatPage({ db: this.db }) + } + .tabBar('💬 聊天') + + TabContent() { + SettingsPage() + } + .tabBar('⚙ 设置') + } + .width('100%') + .height('100%') + .onChange((index: number) => { + this.currentIndex = index; + }) + } + .width('100%') + .height('100%') + } +} diff --git a/harmonyos/entry/src/main/ets/pages/SettingsPage.ets b/harmonyos/entry/src/main/ets/pages/SettingsPage.ets new file mode 100644 index 0000000..c10ea5a --- /dev/null +++ b/harmonyos/entry/src/main/ets/pages/SettingsPage.ets @@ -0,0 +1,97 @@ +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 = await this.pref.get('base_url', 'https://api.deepseek.com'); + this.model = await this.pref.get('model', 'deepseek-chat'); + this.apiKey = await this.pref.get('api_key', ''); + } + + 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) + .fontColor('#ffffff') + .margin({ top: 24, bottom: 24 }) + + // Base URL + Text('Base URL') + .fontSize(14) + .fontColor('#888899') + .width('100%') + .margin({ left: 16, bottom: 4 }) + + TextInput({ placeholder: 'https://api.deepseek.com', text: this.baseUrl }) + .onChange((v: string) => this.onBaseUrlChange(v)) + .margin({ left: 16, right: 16, bottom: 16 }) + .backgroundColor('#1e1e2e') + .fontColor('#ffffff') + .placeholderColor('#555566') + + // Model ID + Text('Model ID') + .fontSize(14) + .fontColor('#888899') + .width('100%') + .margin({ left: 16, bottom: 4 }) + + TextInput({ placeholder: 'deepseek-chat', text: this.model }) + .onChange((v: string) => this.onModelChange(v)) + .margin({ left: 16, right: 16, bottom: 16 }) + .backgroundColor('#1e1e2e') + .fontColor('#ffffff') + .placeholderColor('#555566') + + // API Key + Text('API Key') + .fontSize(14) + .fontColor('#888899') + .width('100%') + .margin({ left: 16, bottom: 4 }) + + TextInput({ placeholder: 'sk-...', text: this.apiKey }) + .type(InputType.Password) + .onChange((v: string) => this.onApiKeyChange(v)) + .margin({ left: 16, right: 16, bottom: 16 }) + .backgroundColor('#1e1e2e') + .fontColor('#ffffff') + .placeholderColor('#555566') + + Text('配置会自动保存') + .fontSize(12) + .fontColor('#666677') + .margin({ top: 32 }) + } + .width('100%') + .height('100%') + .padding(16) + .backgroundColor('#0a0a1a') + } +} diff --git a/harmonyos/entry/src/main/ets/resources/rawfile/graph.html b/harmonyos/entry/src/main/ets/resources/rawfile/graph.html new file mode 100644 index 0000000..55965e4 --- /dev/null +++ b/harmonyos/entry/src/main/ets/resources/rawfile/graph.html @@ -0,0 +1,488 @@ + + + + + + 记忆星图 - TrulyMEM + + + +
+
+

✨ 记忆星图

+

节点: 0

+

关系: 0

+
+
+

节点

+

类型: -

+

提及次数: -

+
+
🔄 加载星图...
+
+ + + + + diff --git a/harmonyos/entry/src/main/module.json5 b/harmonyos/entry/src/main/module.json5 new file mode 100644 index 0000000..e0a326d --- /dev/null +++ b/harmonyos/entry/src/main/module.json5 @@ -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" + } + ] + } +} diff --git a/harmonyos/entry/src/main/resources/base/element/string.json b/harmonyos/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..951fcd4 --- /dev/null +++ b/harmonyos/entry/src/main/resources/base/element/string.json @@ -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" + } + ] +} diff --git a/harmonyos/entry/src/main/resources/base/profile/main_pages.json b/harmonyos/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/harmonyos/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/harmonyos/hvigor/hvigor-config.json5 b/harmonyos/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..5658db0 --- /dev/null +++ b/harmonyos/hvigor/hvigor-config.json5 @@ -0,0 +1,5 @@ +{ + "modelVersion": "5.0.5", + "dependencies": {}, + "execution": {} +} diff --git a/harmonyos/local.properties b/harmonyos/local.properties new file mode 100644 index 0000000..ff41e1a --- /dev/null +++ b/harmonyos/local.properties @@ -0,0 +1,2 @@ +hwsdk.dir=/opt/harmonyos/ohos-sdk/linux/command-line-tools/sdk/default +sdk.dir=/opt/harmonyos/ohos-sdk/linux/command-line-tools/sdk/default diff --git a/harmonyos/oh-package.json5 b/harmonyos/oh-package.json5 new file mode 100644 index 0000000..72f0572 --- /dev/null +++ b/harmonyos/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "modelVersion": "5.0.5", + "description": "TrulyMEM - True Memory Engine for HarmonyOS", + "dependencies": {}, + "devDependencies": { + "@ohos/hypium": "1.0.24", + "@ohos/hamock": "1.0.0" + } +}