From 7fb040c25feefa455cb3e29e86283f185fad5663 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 04:17:32 +0000 Subject: [PATCH 1/2] Adapt meeting-notes Android and WinUI clients Co-authored-by: Enrico Piovesan --- apps/meeting-notes/android-compose/.gitignore | 9 + apps/meeting-notes/android-compose/README.md | 48 +++ .../android-compose/app/build.gradle.kts | 75 ++++ .../android-compose/app/proguard-rules.pro | 1 + .../app/src/main/AndroidManifest.xml | 23 ++ .../meeting-notes/manifests/app.manifest.json | 117 ++++++ .../process/component.manifest.json | 30 ++ .../runtime/runtime-release.json | 1 + .../meeting-notes/runtime/runtime.wasm | Bin 0 -> 993 bytes .../meetingnotes/BundleAssets.kt | 38 ++ .../meetingnotes/EmbeddedHost.kt | 146 +++++++ .../meetingnotes/ExecutionUiState.kt | 31 ++ .../meetingnotes/ExecutionViewModel.kt | 75 ++++ .../meetingnotes/ExecutionViewModelFactory.kt | 17 + .../meetingnotes/MainActivity.kt | 63 +++ .../traverseframework/meetingnotes/Models.kt | 49 +++ .../meetingnotes/SettingsRepository.kt | 29 ++ .../meetingnotes/ui/MainScreen.kt | 243 +++++++++++ .../meetingnotes/ui/SettingsScreen.kt | 56 +++ .../meetingnotes/ui/Theme.kt | 13 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../app/src/main/res/values/colors.xml | 5 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 3 + .../main/res/xml/network_security_config.xml | 8 + .../meetingnotes/ExecutionUiStateTest.kt | 19 + .../meetingnotes/ExecutionViewModelTest.kt | 87 ++++ .../android-compose/build.gradle.kts | 6 + .../android-compose/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + apps/meeting-notes/android-compose/gradlew | 252 ++++++++++++ .../android-compose/settings.gradle.kts | 29 ++ apps/meeting-notes/windows-winui/.gitignore | 7 + .../ExecutionViewModelTests.cs | 97 +++++ .../MeetingNotes.Tests.csproj | 34 ++ .../windows-winui/MeetingNotes.sln | 55 +++ .../windows-winui/MeetingNotes/App.xaml | 13 + .../windows-winui/MeetingNotes/App.xaml.cs | 25 ++ .../MeetingNotes/AppConstants.cs | 9 + .../bundles/meeting-notes/app.manifest.json | 117 ++++++ .../process/component.manifest.json | 30 ++ .../runtime/runtime-release.json | 1 + .../meeting-notes/runtime/runtime.wasm | Bin 0 -> 993 bytes .../MeetingNotes/EmbeddedHost.cs | 387 ++++++++++++++++++ .../MeetingNotes/ExecutionViewModel.cs | 143 +++++++ .../windows-winui/MeetingNotes/HomePage.xaml | 62 +++ .../MeetingNotes/HomePage.xaml.cs | 156 +++++++ .../MeetingNotes/ISettingsRepository.cs | 7 + .../MeetingNotes/MainWindow.xaml | 41 ++ .../MeetingNotes/MainWindow.xaml.cs | 62 +++ .../MeetingNotes/MeetingNotes.csproj | 38 ++ .../windows-winui/MeetingNotes/Models.cs | 68 +++ .../MeetingNotes/Package.appxmanifest | 43 ++ .../windows-winui/MeetingNotes/Program.cs | 13 + .../MeetingNotes/SettingsPage.xaml | 30 ++ .../MeetingNotes/SettingsPage.xaml.cs | 39 ++ .../MeetingNotes/SettingsRepository.cs | 40 ++ .../windows-winui/MeetingNotes/app.manifest | 15 + apps/meeting-notes/windows-winui/README.md | 58 +++ 60 files changed, 3082 insertions(+) create mode 100644 apps/meeting-notes/android-compose/.gitignore create mode 100644 apps/meeting-notes/android-compose/README.md create mode 100644 apps/meeting-notes/android-compose/app/build.gradle.kts create mode 100644 apps/meeting-notes/android-compose/app/proguard-rules.pro create mode 100644 apps/meeting-notes/android-compose/app/src/main/AndroidManifest.xml create mode 100644 apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/app.manifest.json create mode 100644 apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/components/process/component.manifest.json create mode 100644 apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime-release.json create mode 100644 apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime.wasm create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/BundleAssets.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/EmbeddedHost.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionUiState.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModel.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModelFactory.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/MainActivity.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/Models.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/SettingsRepository.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/MainScreen.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/SettingsScreen.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/Theme.kt create mode 100644 apps/meeting-notes/android-compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 apps/meeting-notes/android-compose/app/src/main/res/values/colors.xml create mode 100644 apps/meeting-notes/android-compose/app/src/main/res/values/strings.xml create mode 100644 apps/meeting-notes/android-compose/app/src/main/res/values/themes.xml create mode 100644 apps/meeting-notes/android-compose/app/src/main/res/xml/network_security_config.xml create mode 100644 apps/meeting-notes/android-compose/app/src/test/java/com/traverseframework/meetingnotes/ExecutionUiStateTest.kt create mode 100644 apps/meeting-notes/android-compose/app/src/test/java/com/traverseframework/meetingnotes/ExecutionViewModelTest.kt create mode 100644 apps/meeting-notes/android-compose/build.gradle.kts create mode 100644 apps/meeting-notes/android-compose/gradle.properties create mode 100644 apps/meeting-notes/android-compose/gradle/wrapper/gradle-wrapper.jar create mode 100644 apps/meeting-notes/android-compose/gradle/wrapper/gradle-wrapper.properties create mode 100755 apps/meeting-notes/android-compose/gradlew create mode 100644 apps/meeting-notes/android-compose/settings.gradle.kts create mode 100644 apps/meeting-notes/windows-winui/.gitignore create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes.Tests/ExecutionViewModelTests.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes.Tests/MeetingNotes.Tests.csproj create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes.sln create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/App.xaml create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/App.xaml.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/AppConstants.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Assets/bundles/meeting-notes/app.manifest.json create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Assets/bundles/meeting-notes/components/process/component.manifest.json create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Assets/bundles/meeting-notes/runtime/runtime-release.json create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Assets/bundles/meeting-notes/runtime/runtime.wasm create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/EmbeddedHost.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/ExecutionViewModel.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/HomePage.xaml create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/HomePage.xaml.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/ISettingsRepository.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/MainWindow.xaml create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/MainWindow.xaml.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/MeetingNotes.csproj create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Models.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Package.appxmanifest create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/Program.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/SettingsPage.xaml create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/SettingsPage.xaml.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/SettingsRepository.cs create mode 100644 apps/meeting-notes/windows-winui/MeetingNotes/app.manifest create mode 100644 apps/meeting-notes/windows-winui/README.md diff --git a/apps/meeting-notes/android-compose/.gitignore b/apps/meeting-notes/android-compose/.gitignore new file mode 100644 index 0000000..e4f1161 --- /dev/null +++ b/apps/meeting-notes/android-compose/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +build/ +local.properties +.idea/ +*.iml +.DS_Store +captures/ +.externalNativeBuild/ +.cxx/ diff --git a/apps/meeting-notes/android-compose/README.md b/apps/meeting-notes/android-compose/README.md new file mode 100644 index 0000000..3fe47e6 --- /dev/null +++ b/apps/meeting-notes/android-compose/README.md @@ -0,0 +1,48 @@ +# meeting-notes (Android Compose) + +**Runtime mode: Embedded** - public Kotlin `TraverseEmbedder` (`dev.traverse.embedder`) with digest-pinned `runtime/runtime.wasm`. No `traverse-cli serve` sidecar is required. + +Native Android client for the `meeting-notes` reference app. + +## Prerequisites + +- Android Studio Ladybug+ (or compatible AGP 8.7 / Kotlin 2.0) +- Android emulator API 28+ (or physical device) +- Traverse checkout with the Kotlin embedder package and certified runtime artifact: + +```bash +export TRAVERSE_REPO=/path/to/Traverse +bash scripts/ci/sync_android_meeting_notes_bundle.sh +``` + +`settings.gradle.kts` composites `$TRAVERSE_REPO/packages/kotlin/TraverseEmbedder` as `:traverse-embedder`. + +## Bundle configuration + +Assets live under `app/src/main/assets/bundles/meeting-notes/` (including `runtime/runtime.wasm` + `runtime-release.json` after sync). The app copies them into `filesDir` on launch. + +Settings -> Workspace only (no sidecar URL). Default workspace is `local-default`. + +## Build and test + +```bash +export TRAVERSE_REPO=/path/to/Traverse +cd apps/meeting-notes/android-compose +./gradlew test +./gradlew :app:assembleDebug +``` + +Unit tests inject `InMemoryTraverseEmbedder` via `InMemoryMeetingNotesHost`; fixtures use only the runtime-owned `MeetingNotesOutput` fields. + +## Architecture + +| File | Role | +|---|---| +| `EmbeddedHost.kt` | Production + in-memory embedded hosts | +| `ExecutionViewModel.kt` | Submit transcript -> render runtime-owned output | +| `BundleAssets.kt` | Materialize asset bundle into filesDir | +| `ui/MainScreen.kt` | Embedded runtime status, transcript input, output, trace | + +## Design language + +Follow [docs/design-language.md](../../../docs/design-language.md). Zone 1 shows **Embedded** runtime mode. diff --git a/apps/meeting-notes/android-compose/app/build.gradle.kts b/apps/meeting-notes/android-compose/app/build.gradle.kts new file mode 100644 index 0000000..4a04f42 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/build.gradle.kts @@ -0,0 +1,75 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.kotlin.plugin.compose") +} + +android { + namespace = "com.traverseframework.meetingnotes" + compileSdk = 35 + + defaultConfig { + applicationId = "com.traverseframework.meetingnotes" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2024.10.01") + implementation(composeBom) + androidTestImplementation(composeBom) + + implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.activity:activity-compose:1.9.3") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7") + implementation("androidx.navigation:navigation-compose:2.8.4") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") + + // Public Traverse Kotlin embedder (composite via settings.gradle.kts + TRAVERSE_REPO) + implementation(project(":traverse-embedder")) + + debugImplementation("androidx.compose.ui:ui-tooling") + + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") +} diff --git a/apps/meeting-notes/android-compose/app/proguard-rules.pro b/apps/meeting-notes/android-compose/app/proguard-rules.pro new file mode 100644 index 0000000..60ba34d --- /dev/null +++ b/apps/meeting-notes/android-compose/app/proguard-rules.pro @@ -0,0 +1 @@ +# traverse-starter Android — default keep rules diff --git a/apps/meeting-notes/android-compose/app/src/main/AndroidManifest.xml b/apps/meeting-notes/android-compose/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8e50f4a --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/app.manifest.json b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/app.manifest.json new file mode 100644 index 0000000..06486a2 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/app.manifest.json @@ -0,0 +1,117 @@ +{ + "app_id": "meeting-notes", + "version": "1.0.0", + "schema_version": "1.0.0", + "workspace_defaults": { + "workspace_id": "local-default", + "registry_scope": "private" + }, + "components": [ + { + "component_id": "meeting-notes.process-component", + "version": "1.0.0", + "digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "manifest_path": "components/process/component.manifest.json" + } + ], + "workflows": [ + { + "workflow_id": "meeting-notes.process", + "workflow_version": "1.0.0", + "path": "_traverse/workflows/examples/meeting-notes/process/workflow.json" + } + ], + "model_dependencies": [], + "config_schema": { + "type": "object", + "required": [ + "workspace_id" + ], + "properties": { + "workspace_id": { + "type": "string" + }, + "processing_mode": { + "type": "string", + "enum": [ + "deterministic" + ] + } + }, + "additionalProperties": false + }, + "default_config": { + "workspace_id": "local-default", + "processing_mode": "deterministic" + }, + "placement_policy": { + "preferred_targets": [ + "local" + ], + "allow_fallback": false + }, + "public_surfaces": [ + "cli", + "http_json" + ], + "state_machine": { + "initial_state": "idle", + "list_context_fields": [ + "output.action_items", + "output.decisions", + "output.follow_ups", + "output.summary" + ], + "states": [ + { + "id": "idle", + "transitions": [ + { + "on": "submit", + "to": "processing" + } + ] + }, + { + "id": "processing", + "invoke": { + "capability_id": "meeting-notes.process", + "input_from": "command.payload" + }, + "transitions": [ + { + "on": "capability_succeeded", + "to": "results" + }, + { + "on": "capability_failed", + "to": "error" + } + ] + }, + { + "id": "results", + "transitions": [ + { + "on": "reset", + "to": "idle" + } + ] + }, + { + "id": "error", + "transitions": [ + { + "on": "retry", + "to": "processing", + "with_last_payload": true + }, + { + "on": "reset", + "to": "idle" + } + ] + } + ] + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/components/process/component.manifest.json b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/components/process/component.manifest.json new file mode 100644 index 0000000..dec21bd --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/manifests/components/process/component.manifest.json @@ -0,0 +1,30 @@ +{ + "component_id": "meeting-notes.process-component", + "version": "1.0.0", + "schema_version": "1.0.0", + "capability_id": "meeting-notes.process", + "capability_version": "1.0.0", + "registry_ref": { + "namespace": "meeting-notes", + "id": "meeting-notes.process", + "version_range": "^1.0.0" + }, + "runtime_constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + }, + "permitted_targets": [ + "local", + "device" + ], + "dependencies": [], + "connector_requirements": [], + "validation_evidence": [ + { + "evidence_type": "checked_in_fixture", + "status": "passed", + "produced_by": "meeting_notes_example_smoke" + } + ] +} diff --git a/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime-release.json b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime-release.json new file mode 100644 index 0000000..b3354d7 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime-release.json @@ -0,0 +1 @@ +{"runtime_version":"0.8.1","bridge_version":"1.1.0","bridge_abi_version":10100,"sha256":"aa801023ba4eb20b8c1b4004bdd964a78fed9540478b252b77eac04c80811852"} diff --git a/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime.wasm b/apps/meeting-notes/android-compose/app/src/main/assets/bundles/meeting-notes/runtime/runtime.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9587c31e71e17ac8e0b51f19008a7f484f9d0d1b GIT binary patch literal 993 zcmb7D!EVz)5S>{&N$I9(tMotxQiGEtJ#axNCoCkyx#I3@>{YU@y|(OiN>oMeAMi_Q zQ*q$Hg7@Rp70k*ls2=N%J{_XC@ajnHMq`=PH`WK#Wyj0hMaNPR)uq zPD5ZvH4w=STg*xyNAsW=9{{}tO^=s^!(GrIZiWslK{H^Kk$4fru) z)rS|@C+>1UDo$Jndd$CFf`4`dG{G*F0TQ$%Xa#HR0{S%E_wHM=T;75I-NHCP9}hV( zxMl9a|6&=pWS#g7E>19(95NW z=H5$B=6RZXLtV!4f=o#Dj#RR+FE>z;RhHVQM4oTK^H+4Lwh>_{Gb5vao$i2tyz$ID zljLFbG6<(aPuAK;8$`kgZ@9^84Jd?Gdg3i?$a~-)YU@yB^gYJP%mbxo>D7PvukBXA zFLv4GvM6I?pZ2kF{7##eCMyl)UaXBDfS>#|^1`H9M)4N##?>N`*hFyX2s?Ga0gMnc XhMF-zO-mv%YIPX6Qz + destDir.outputStream().use { output -> input.copyTo(output) } + } + return + } + destDir.mkdirs() + for (child in children) { + val childAsset = "$assetPath/$child" + val childDest = File(destDir, child) + val grand = context.assets.list(childAsset) + if (grand.isNullOrEmpty()) { + context.assets.open(childAsset).use { input -> + childDest.outputStream().use { output -> input.copyTo(output) } + } + } else { + copyAssetTree(context, childAsset, childDest) + } + } + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/EmbeddedHost.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/EmbeddedHost.kt new file mode 100644 index 0000000..b388067 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/EmbeddedHost.kt @@ -0,0 +1,146 @@ +package com.traverseframework.meetingnotes + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import java.io.File + +/** + * Embedded Traverse host for meeting-notes. + * + * Production uses the public Kotlin `dev.traverse.embedder` package. + * Unit tests inject [InMemoryMeetingNotesHost] with scripted runtime-owned output. + */ +interface MeetingNotesHost { + val runtimeMode: String + val isReady: Boolean + fun submitTranscript(transcript: String): HostRunResult +} + +data class HostRunResult( + val sessionId: String, + val output: MeetingNotesOutput?, + val events: List, + val error: String?, +) + +/** Deterministic test double wrapping [dev.traverse.embedder.InMemoryTraverseEmbedder]. */ +class InMemoryMeetingNotesHost( + private val scriptedOutputJson: String, +) : MeetingNotesHost { + override val runtimeMode: String = AppConstants.RUNTIME_MODE_EMBEDDED + override val isReady: Boolean = true + + private val embedder = dev.traverse.embedder.InMemoryTraverseEmbedder() + .withTargetOutput(scriptedOutputJson) + .also { + it.initialize( + dev.traverse.embedder.TraverseBundle( + rootPath = "test-bundle", + runtimeWasmDigest = "sha256:test", + ), + ) + } + + override fun submitTranscript(transcript: String): HostRunResult { + val inputJson = buildJsonObject { put("transcript", transcript) }.toString() + val result = embedder.submit( + dev.traverse.embedder.TraverseSubmission(AppConstants.CAPABILITY_ID, inputJson), + ) + val events = embedder.subscribe() + val outputJson = events.firstOrNull { it.eventType == "capability_result" }?.output + val output = outputJson?.let { parseOutput(it) } + return HostRunResult( + sessionId = result.sessionId, + output = output, + events = events.map { + TraceEvent( + event_type = it.eventType ?: it.status, + timestamp = it.sequence.toString(), + data = null, + ) + }, + error = if (output == null) "embedder emitted no capability_result output" else null, + ) + } + + companion object { + private val json = Json { ignoreUnknownKeys = true } + + fun parseOutput(raw: String): MeetingNotesOutput? = try { + json.decodeFromString(MeetingNotesOutput.serializer(), raw) + } catch (_: Exception) { + null + } + + fun withScriptedOutput(output: MeetingNotesOutput): InMemoryMeetingNotesHost = + InMemoryMeetingNotesHost(json.encodeToString(MeetingNotesOutput.serializer(), output)) + } +} + +/** + * Production host: digest-pinned `runtime/runtime.wasm` via public [RuntimeTraverseEmbedder] + * constructed from [TraverseBundle] (public constructor). + */ +class ProductionMeetingNotesHost private constructor( + private val embedder: dev.traverse.embedder.RuntimeTraverseEmbedder, +) : MeetingNotesHost { + override val runtimeMode: String = AppConstants.RUNTIME_MODE_EMBEDDED + override val isReady: Boolean = true + + override fun submitTranscript(transcript: String): HostRunResult = try { + val inputJson = buildJsonObject { put("transcript", transcript) }.toString() + val result = embedder.submit( + dev.traverse.embedder.TraverseSubmission(AppConstants.CAPABILITY_ID, inputJson), + ) + val runtimeEvents = embedder.subscribe() + val events = runtimeEvents.map { + TraceEvent( + event_type = it.eventType ?: it.status, + timestamp = it.sequence.toString(), + data = null, + ) + } + val output = runtimeEvents + .firstOrNull { it.eventType == "capability_result" || it.output != null } + ?.output + ?.let { InMemoryMeetingNotesHost.parseOutput(it) } + HostRunResult( + sessionId = result.sessionId, + output = output, + events = events, + error = if (output == null) { + "runtime returned no meeting-notes output" + } else { + null + }, + ) + } catch (e: Exception) { + HostRunResult("", null, emptyList(), e.message ?: "submit failed") + } + + companion object { + fun createOrNull(bundleRoot: File): ProductionMeetingNotesHost? { + val wasm = File(bundleRoot, "runtime/runtime.wasm") + val release = File(bundleRoot, "runtime/runtime-release.json") + if (!wasm.isFile || !release.isFile) return null + val digestHex = Regex("\"sha256\"\\s*:\\s*\"([^\"]+)\"") + .find(release.readText()) + ?.groupValues + ?.get(1) + ?: return null + return try { + val bundle = dev.traverse.embedder.TraverseBundle( + rootPath = bundleRoot.absolutePath, + runtimeWasmDigest = "sha256:$digestHex", + ) + val embedder = dev.traverse.embedder.RuntimeTraverseEmbedder(bundle) + embedder.initialize("{}") + ProductionMeetingNotesHost(embedder) + } catch (_: Exception) { + null + } + } + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionUiState.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionUiState.kt new file mode 100644 index 0000000..c380be7 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionUiState.kt @@ -0,0 +1,31 @@ +package com.traverseframework.meetingnotes + +sealed interface ExecutionPhase { + data object Idle : ExecutionPhase + data object Loading : ExecutionPhase + data class Succeeded(val output: MeetingNotesOutput, val trace: List) : ExecutionPhase + data class Failed(val error: String) : ExecutionPhase +} + +enum class RuntimeStatus { + Starting, + Ready, + Unavailable, +} + +data class ExecutionUiState( + val phase: ExecutionPhase = ExecutionPhase.Idle, + val transcript: String = "", + val runtimeStatus: RuntimeStatus = RuntimeStatus.Starting, + val runtimeMode: String = AppConstants.RUNTIME_MODE_EMBEDDED, + val workspace: String = AppConstants.DEFAULT_WORKSPACE, + val showTrace: Boolean = false, +) { + val isRunning: Boolean + get() = phase is ExecutionPhase.Loading + + val canSubmit: Boolean + get() = runtimeStatus == RuntimeStatus.Ready && + transcript.trim().isNotEmpty() && + !isRunning +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModel.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModel.kt new file mode 100644 index 0000000..ebf074a --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModel.kt @@ -0,0 +1,75 @@ +package com.traverseframework.meetingnotes + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ExecutionViewModel( + private val host: MeetingNotesHost, + private val settings: RuntimeSettings, + private val computeDispatcher: CoroutineDispatcher = Dispatchers.Default, +) : ViewModel() { + private val _uiState = MutableStateFlow( + ExecutionUiState( + runtimeStatus = if (host.isReady) RuntimeStatus.Ready else RuntimeStatus.Unavailable, + runtimeMode = host.runtimeMode, + workspace = AppConstants.DEFAULT_WORKSPACE, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + private var submitJob: Job? = null + + init { + viewModelScope.launch { + settings.workspace.collect { workspace -> + _uiState.update { it.copy(workspace = workspace) } + } + } + } + + fun updateTranscript(transcript: String) { + _uiState.update { it.copy(transcript = transcript.take(AppConstants.TRANSCRIPT_MAX_LENGTH)) } + } + + fun toggleTrace(show: Boolean) { + _uiState.update { it.copy(showTrace = show) } + } + + fun submit() { + val state = _uiState.value + if (!state.canSubmit) return + submitJob?.cancel() + _uiState.update { it.copy(phase = ExecutionPhase.Loading) } + val transcript = state.transcript.trim() + submitJob = viewModelScope.launch { + val result = withContext(computeDispatcher) { host.submitTranscript(transcript) } + if (result.error != null && result.output == null) { + _uiState.update { it.copy(phase = ExecutionPhase.Failed(result.error)) } + } else { + _uiState.update { + it.copy( + phase = ExecutionPhase.Succeeded( + result.output ?: MeetingNotesOutput.EMPTY, + result.events, + ), + ) + } + } + } + } + + fun reset() { + submitJob?.cancel() + submitJob = null + _uiState.update { it.copy(phase = ExecutionPhase.Idle, showTrace = false) } + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModelFactory.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModelFactory.kt new file mode 100644 index 0000000..e9e7a1d --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ExecutionViewModelFactory.kt @@ -0,0 +1,17 @@ +package com.traverseframework.meetingnotes + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class ExecutionViewModelFactory( + private val host: MeetingNotesHost, + private val settings: RuntimeSettings, +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(ExecutionViewModel::class.java)) { + return ExecutionViewModel(host, settings) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/MainActivity.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/MainActivity.kt new file mode 100644 index 0000000..0bf2a4d --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/MainActivity.kt @@ -0,0 +1,63 @@ +package com.traverseframework.meetingnotes + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.traverseframework.meetingnotes.ui.MainScreen +import com.traverseframework.meetingnotes.ui.MeetingNotesTheme +import com.traverseframework.meetingnotes.ui.SettingsScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + val settings = SettingsRepository(applicationContext) + val bundleRoot = BundleAssets.materialize(applicationContext) + val host: MeetingNotesHost = ProductionMeetingNotesHost.createOrNull(bundleRoot) + ?: UnavailableMeetingNotesHost + setContent { + MeetingNotesTheme { + val navController = rememberNavController() + val viewModel: ExecutionViewModel = viewModel( + factory = ExecutionViewModelFactory(host, settings), + ) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + NavHost(navController = navController, startDestination = "main") { + composable("main") { + MainScreen( + uiState = uiState, + onTranscriptChange = viewModel::updateTranscript, + onSubmit = viewModel::submit, + onReset = viewModel::reset, + onOpenSettings = { navController.navigate("settings") }, + onTraceToggle = viewModel::toggleTrace, + ) + } + composable("settings") { + SettingsScreen( + settings = settings, + currentWorkspace = uiState.workspace, + onBack = { navController.popBackStack() }, + ) + } + } + } + } + } +} + +/** Fallback when the digest-pinned runtime bundle is missing from assets. */ +object UnavailableMeetingNotesHost : MeetingNotesHost { + override val runtimeMode: String = AppConstants.RUNTIME_MODE_EMBEDDED + override val isReady: Boolean = false + override fun submitTranscript(transcript: String): HostRunResult = + HostRunResult("", null, emptyList(), "embedded runtime unavailable - sync the app bundle") +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/Models.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/Models.kt new file mode 100644 index 0000000..91f8168 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/Models.kt @@ -0,0 +1,49 @@ +package com.traverseframework.meetingnotes + +import kotlinx.serialization.json.JsonElement + +@kotlinx.serialization.Serializable +data class ActionItem( + val task: String, + val owner: String? = null, + val due: String? = null, +) + +@kotlinx.serialization.Serializable +data class Decision( + val text: String, + val made_by: String? = null, +) + +@kotlinx.serialization.Serializable +data class MeetingNotesOutput( + val action_items: List, + val decisions: List, + val follow_ups: List, + val summary: String, +) { + companion object { + val EMPTY = MeetingNotesOutput( + action_items = emptyList(), + decisions = emptyList(), + follow_ups = emptyList(), + summary = "", + ) + } +} + +@kotlinx.serialization.Serializable +data class TraceEvent( + val event_type: String, + val timestamp: String, + val data: JsonElement? = null, +) + +object AppConstants { + const val CAPABILITY_ID = "meeting-notes.process" + const val RUNTIME_MODE_EMBEDDED = "Embedded" + const val DEFAULT_WORKSPACE = "local-default" + const val TRANSCRIPT_MAX_LENGTH = 5000 + /** Asset-relative bundle root (must include runtime/runtime.wasm after sync). */ + const val BUNDLE_ASSET_DIR = "bundles/meeting-notes" +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/SettingsRepository.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/SettingsRepository.kt new file mode 100644 index 0000000..9d770c7 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/SettingsRepository.kt @@ -0,0 +1,29 @@ +package com.traverseframework.meetingnotes + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface RuntimeSettings { + val workspace: Flow + suspend fun setWorkspace(workspace: String) +} + +private val Context.settingsDataStore: DataStore by preferencesDataStore(name = "meeting_notes_settings") + +class SettingsRepository(private val context: Context) : RuntimeSettings { + private val workspaceKey = stringPreferencesKey("workspace") + + override val workspace: Flow = context.settingsDataStore.data.map { prefs -> + prefs[workspaceKey] ?: AppConstants.DEFAULT_WORKSPACE + } + + override suspend fun setWorkspace(workspace: String) { + context.settingsDataStore.edit { it[workspaceKey] = workspace } + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/MainScreen.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/MainScreen.kt new file mode 100644 index 0000000..89c6984 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/MainScreen.kt @@ -0,0 +1,243 @@ +package com.traverseframework.meetingnotes.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.traverseframework.meetingnotes.ActionItem +import com.traverseframework.meetingnotes.AppConstants +import com.traverseframework.meetingnotes.Decision +import com.traverseframework.meetingnotes.ExecutionPhase +import com.traverseframework.meetingnotes.ExecutionUiState +import com.traverseframework.meetingnotes.MeetingNotesOutput +import com.traverseframework.meetingnotes.RuntimeStatus + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen( + uiState: ExecutionUiState, + onTranscriptChange: (String) -> Unit, + onSubmit: () -> Unit, + onReset: () -> Unit, + onOpenSettings: () -> Unit, + onTraceToggle: (Boolean) -> Unit, +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("meeting-notes") }, + actions = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(end = 12.dp), + ) { + StatusDot(uiState.runtimeStatus) + Text( + text = statusLabel(uiState.runtimeStatus), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(start = 8.dp), + ) + Button(onClick = onOpenSettings, modifier = Modifier.padding(start = 8.dp)) { + Text("Settings") + } + } + }, + ) + }, + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + RuntimeCard(uiState) + InputCard(uiState, onTranscriptChange, onSubmit) + OutputCard(uiState, onReset, onTraceToggle) + } + } +} + +@Composable +private fun RuntimeCard(uiState: ExecutionUiState) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Runtime Environment", style = MaterialTheme.typography.titleMedium) + Text("mode: ${uiState.runtimeMode}", style = MaterialTheme.typography.bodySmall) + Text("workspace: ${uiState.workspace}", style = MaterialTheme.typography.bodySmall) + Text("capability: ${AppConstants.CAPABILITY_ID}", style = MaterialTheme.typography.bodySmall) + } + } +} + +@Composable +private fun InputCard( + uiState: ExecutionUiState, + onTranscriptChange: (String) -> Unit, + onSubmit: () -> Unit, +) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Process Transcript", style = MaterialTheme.typography.titleMedium) + OutlinedTextField( + value = uiState.transcript, + onValueChange = onTranscriptChange, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + placeholder = { Text("Paste meeting transcript...") }, + ) + Text("${uiState.transcript.length}/${AppConstants.TRANSCRIPT_MAX_LENGTH}") + Button(onClick = onSubmit, enabled = uiState.canSubmit, modifier = Modifier.fillMaxWidth()) { + Text(if (uiState.isRunning) "Processing..." else "Submit Transcript") + } + if (uiState.runtimeStatus == RuntimeStatus.Unavailable) { + Text( + "Embedded runtime unavailable - sync the bundle with scripts/ci/sync_android_meeting_notes_bundle.sh (requires TRAVERSE_REPO).", + style = MaterialTheme.typography.bodySmall, + ) + } + } + } +} + +@Composable +private fun OutputCard( + uiState: ExecutionUiState, + onReset: () -> Unit, + onTraceToggle: (Boolean) -> Unit, +) { + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Output", style = MaterialTheme.typography.titleMedium) + when (val phase = uiState.phase) { + ExecutionPhase.Idle -> { + Text( + if (uiState.runtimeStatus == RuntimeStatus.Unavailable) { + "Initialize the embedded runtime to see meeting notes output here." + } else { + "Submit a transcript above to run meeting-notes.process." + }, + style = MaterialTheme.typography.bodyMedium, + ) + } + ExecutionPhase.Loading -> Text("Running embedded workflow...") + is ExecutionPhase.Failed -> Text("Error: ${phase.error}", color = MaterialTheme.colorScheme.error) + is ExecutionPhase.Succeeded -> { + OutputFields(phase.output) + if (phase.trace.isNotEmpty()) { + Button(onClick = { onTraceToggle(!uiState.showTrace) }) { + Text("Trace (${phase.trace.size} events)") + } + if (uiState.showTrace) { + phase.trace.forEach { event -> + Text("${event.timestamp} - ${event.event_type}", style = MaterialTheme.typography.bodySmall) + } + } + } + Button(onClick = onReset) { Text("Reset") } + } + } + } + } +} + +@Composable +private fun OutputFields(output: MeetingNotesOutput) { + ActionItemsSection(output.action_items) + Spacer(modifier = Modifier.height(8.dp)) + DecisionsSection(output.decisions) + Spacer(modifier = Modifier.height(8.dp)) + StringListSection("Follow-ups", output.follow_ups) + Spacer(modifier = Modifier.height(8.dp)) + Text("Summary", style = MaterialTheme.typography.titleSmall) + Text(output.summary) +} + +@Composable +private fun ActionItemsSection(items: List) { + Text("Action Items", style = MaterialTheme.typography.titleSmall) + if (items.isEmpty()) { + Text("None recorded", color = MaterialTheme.colorScheme.onSurfaceVariant) + return + } + items.forEach { item -> + Text("- ${item.task}") + val details = listOfNotNull(item.owner, item.due?.let { "due $it" }) + if (details.isNotEmpty()) { + Text(details.joinToString(" | "), style = MaterialTheme.typography.bodySmall) + } + } +} + +@Composable +private fun DecisionsSection(items: List) { + Text("Decisions", style = MaterialTheme.typography.titleSmall) + if (items.isEmpty()) { + Text("None recorded", color = MaterialTheme.colorScheme.onSurfaceVariant) + return + } + items.forEach { item -> + Text("- ${item.text}") + item.made_by?.let { madeBy -> + Text("decided by $madeBy", style = MaterialTheme.typography.bodySmall) + } + } +} + +@Composable +private fun StringListSection(label: String, items: List) { + Text(label, style = MaterialTheme.typography.titleSmall) + if (items.isEmpty()) { + Text("None recorded", color = MaterialTheme.colorScheme.onSurfaceVariant) + return + } + items.forEach { item -> Text("- $item") } +} + +@Composable +private fun StatusDot(status: RuntimeStatus) { + val color = when (status) { + RuntimeStatus.Ready -> Color(0xFF06B6D4) + RuntimeStatus.Unavailable -> Color(0xFFEF4444) + RuntimeStatus.Starting -> Color.Gray + } + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(color), + ) +} + +private fun statusLabel(status: RuntimeStatus): String = when (status) { + RuntimeStatus.Ready -> "Ready" + RuntimeStatus.Unavailable -> "Unavailable" + RuntimeStatus.Starting -> "Starting..." +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/SettingsScreen.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/SettingsScreen.kt new file mode 100644 index 0000000..928d6ac --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/SettingsScreen.kt @@ -0,0 +1,56 @@ +package com.traverseframework.meetingnotes.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.traverseframework.meetingnotes.RuntimeSettings +import kotlinx.coroutines.launch + +@Composable +fun SettingsScreen( + settings: RuntimeSettings, + currentWorkspace: String, + onBack: () -> Unit, +) { + var workspace by remember(currentWorkspace) { mutableStateOf(currentWorkspace) } + val scope = rememberCoroutineScope() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Embedded runtime settings") + Text("Runtime mode is Embedded - no HTTP sidecar URL.") + OutlinedTextField( + value = workspace, + onValueChange = { workspace = it }, + label = { Text("Workspace") }, + modifier = Modifier.fillMaxWidth(), + ) + Button( + onClick = { + scope.launch { + settings.setWorkspace(workspace.trim()) + onBack() + } + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Save") + } + } +} diff --git a/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/Theme.kt b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/Theme.kt new file mode 100644 index 0000000..312b369 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/java/com/traverseframework/meetingnotes/ui/Theme.kt @@ -0,0 +1,13 @@ +package com.traverseframework.meetingnotes.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +@Composable +fun MeetingNotesTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = lightColorScheme(), + content = content, + ) +} diff --git a/apps/meeting-notes/android-compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/meeting-notes/android-compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..99310e9 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/meeting-notes/android-compose/app/src/main/res/values/colors.xml b/apps/meeting-notes/android-compose/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..27c0ce3 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #1E1B4B + #8B5CF6 + diff --git a/apps/meeting-notes/android-compose/app/src/main/res/values/strings.xml b/apps/meeting-notes/android-compose/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..1009c08 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + meeting-notes + diff --git a/apps/meeting-notes/android-compose/app/src/main/res/values/themes.xml b/apps/meeting-notes/android-compose/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bf431c0 --- /dev/null +++ b/apps/meeting-notes/android-compose/app/src/main/res/values/themes.xml @@ -0,0 +1,3 @@ + +