diff --git a/.claude/guides/09-expo-ondevice-ai.md b/.claude/guides/09-expo-ondevice-ai.md index 1ab9c0f..b777e4d 100644 --- a/.claude/guides/09-expo-ondevice-ai.md +++ b/.claude/guides/09-expo-ondevice-ai.md @@ -4,7 +4,7 @@ Location: `libraries/expo-ondevice-ai/` -Expo module wrapping the Locanara native SDKs for React Native/Expo apps. Provides TypeScript API for all 7 AI features plus model management, with native modules bridging to Locanara chains on iOS and Android. +Expo module wrapping the Locanara native SDKs for React Native/Expo apps. Provides TypeScript API for all 8 AI features plus model management, with native modules bridging to Locanara chains on iOS, Android, and web (Chrome Built-in AI). ## Requirements @@ -12,6 +12,7 @@ Expo module wrapping the Locanara native SDKs for React Native/Expo apps. Provid - Bun 1.1+ - iOS 17+ (for llama.cpp engine) - Android API 26+ (for ML Kit GenAI) +- Web: Chrome 138+ (Chrome Built-in AI / Gemini Nano) ## Build Commands @@ -31,6 +32,7 @@ libraries/expo-ondevice-ai/ ├── src/ │ ├── index.ts # Public API exports │ ├── ExpoOndeviceAiModule.ts # Native module bridge +│ ├── ExpoOndeviceAiModule.web.ts # Web implementation (Chrome Built-in AI) │ ├── types.ts # TypeScript type definitions │ ├── log.ts # Logging utilities │ └── __tests__/ # Unit tests @@ -68,28 +70,28 @@ libraries/expo-ondevice-ai/ Each TypeScript function maps to a built-in Locanara chain: -| TypeScript API | iOS Chain | Android | -|---------------|-----------|---------| -| `summarize(text, opts)` | `SummarizeChain(bulletCount:).run(text)` | ML Kit Summarization | -| `classify(text, opts)` | `ClassifyChain(categories:).run(text)` | Prompt API | -| `extract(text, opts)` | `ExtractChain(entityTypes:).run(text)` | Prompt API | -| `chat(message, opts)` | `ChatChain(memory:).run(message)` | Prompt API | -| `chatStream(message, opts)` | `ChatChain(memory:).streamRun(message)` | Prompt API | -| `translate(text, opts)` | `TranslateChain(source:target:).run(text)` | Prompt API | -| `rewrite(text, opts)` | `RewriteChain(style:).run(text)` | ML Kit Rewriting | -| `proofread(text, opts)` | `ProofreadChain().run(text)` | ML Kit Proofreading | +| TypeScript API | iOS Chain | Android | Web (Chrome Built-in AI) | +| --------------------------- | ------------------------------------------ | -------------------- | ------------------------------------- | +| `summarize(text, opts)` | `SummarizeChain(bulletCount:).run(text)` | ML Kit Summarization | `Summarizer` API (key-points) | +| `classify(text, opts)` | `ClassifyChain(categories:).run(text)` | Prompt API | `LanguageModel` API | +| `extract(text, opts)` | `ExtractChain(entityTypes:).run(text)` | Prompt API | `LanguageModel` API | +| `chat(message, opts)` | `ChatChain(memory:).run(message)` | Prompt API | `LanguageModel` API | +| `chatStream(message, opts)` | `ChatChain(memory:).streamRun(message)` | Prompt API | `LanguageModel.promptStreaming()` | +| `translate(text, opts)` | `TranslateChain(source:target:).run(text)` | Prompt API | `Translator` API | +| `rewrite(text, opts)` | `RewriteChain(style:).run(text)` | ML Kit Rewriting | `Rewriter` API | +| `proofread(text, opts)` | `ProofreadChain().run(text)` | ML Kit Proofreading | `LanguageModel` API (structured JSON) | ### Model Management API (iOS) -| TypeScript API | Native call | -|---------------|-------------| -| `getAvailableModels()` | `LocanaraClient.shared.getAvailableModels()` | -| `getDownloadedModels()` | `LocanaraClient.shared.getDownloadedModels()` | -| `downloadModel(id)` | `LocanaraClient.shared.downloadModelWithProgress(id)` | -| `loadModel(id)` | `LocanaraClient.shared.loadModel(id)` → auto-switches engine | -| `deleteModel(id)` | `LocanaraClient.shared.deleteModel(id)` | -| `getLoadedModel()` | `LocanaraClient.shared.getLoadedModel()` | -| `getCurrentEngine()` | `LocanaraClient.shared.getCurrentEngine()` | +| TypeScript API | Native call | +| ----------------------- | ------------------------------------------------------------ | +| `getAvailableModels()` | `LocanaraClient.shared.getAvailableModels()` | +| `getDownloadedModels()` | `LocanaraClient.shared.getDownloadedModels()` | +| `downloadModel(id)` | `LocanaraClient.shared.downloadModelWithProgress(id)` | +| `loadModel(id)` | `LocanaraClient.shared.loadModel(id)` → auto-switches engine | +| `deleteModel(id)` | `LocanaraClient.shared.deleteModel(id)` | +| `getLoadedModel()` | `LocanaraClient.shared.getLoadedModel()` | +| `getCurrentEngine()` | `LocanaraClient.shared.getCurrentEngine()` | ### Native Module Architecture @@ -97,6 +99,26 @@ Each TypeScript function maps to a built-in Locanara chain: - All AI features use built-in chains directly (not `LocanaraClient.executeFeature()`) - `PrefilledMemory` adapts JS chat history `[{role, content}]` to the `Memory` protocol +### Web Implementation (`ExpoOndeviceAiModule.web.ts`) + +Metro auto-resolves `.web.ts` over `.ts` for the web platform. The web module uses Chrome Built-in AI APIs (Gemini Nano) directly — no native bridge needed. + +**Chrome APIs used:** + +- `Summarizer` — text summarization (key-points mode, post-processed to match bullet count) +- `LanguageModel` — classify, extract, chat, chatStream, proofread (via structured JSON prompts) +- `Translator` — language translation +- `Rewriter` — text rewriting (tone/length mapping) +- `Writer` — fallback for proofread if LanguageModel unavailable + +**Key implementation details:** + +- **Availability detection**: Lenient checks with 3s timeout; accepts `readily`, `available`, `downloadable`, `after-download` statuses; falls back to API object existence +- **Streaming**: Uses `LanguageModel.promptStreaming()` with auto-detection of cumulative vs delta chunk format (varies by Chrome version) +- **Event emitter**: Web polyfill for Expo's native `addListener`/`removeListeners` pattern using a `Map>` +- **Instance caching**: Summarizer, LanguageModel, Translator, Rewriter, Writer instances are cached and reused +- **Model management**: No-op on web (Chrome manages models automatically) + ## Config Plugin (`withOndeviceAi.ts`) The Expo config plugin automates native setup at prebuild time. @@ -164,15 +186,18 @@ The bridge is discovered at runtime by `LlamaCppBridge.findBridge()` using `NSCl ### Key Build Settings Bridge pod (`pod_target_xcconfig`): + - `SWIFT_INCLUDE_PATHS` / `FRAMEWORK_SEARCH_PATHS` → `$(PODS_CONFIGURATION_BUILD_DIR)` (for SPM modules) - `IPHONEOS_DEPLOYMENT_TARGET` → `17.0` (LocalLLMClient requirement) - `OTHER_SWIFT_FLAGS` → `-cxx-interoperability-mode=default -Xcc -std=c++20` App target (`user_target_xcconfig`): + - `OTHER_LDFLAGS` → `-framework "llama"` (link dynamic framework) - `FRAMEWORK_SEARCH_PATHS` → `$(PODS_CONFIGURATION_BUILD_DIR)` (find llama.framework) Embed phase: + - Copies `llama.framework` from `PackageFrameworks/` to app's `Frameworks/` - Re-signs with `EXPANDED_CODE_SIGN_IDENTITY` @@ -191,6 +216,9 @@ bun ios --device # Run on Android bun android + +# Run on Web (Chrome 138+ required for AI features) +bun web ``` ### App Structure diff --git a/.claude/guides/09-platform-differences.md b/.claude/guides/09-platform-differences.md index addaf26..7c28f0f 100644 --- a/.claude/guides/09-platform-differences.md +++ b/.claude/guides/09-platform-differences.md @@ -1,41 +1,51 @@ # Platform Feature Differences -This guide documents feature availability and implementation differences across iOS and Android platforms. +This guide documents feature availability and implementation differences across iOS, Android, and Web platforms. ## Feature Availability Matrix -| Feature | iOS | Android | Notes | -|---------|-----|---------|-------| -| **Core Framework** | ✅ | ✅ | Identical API across platforms | -| Chains (7 built-in) | ✅ | ✅ | Same chain implementations | -| Pipeline DSL | ✅ | ✅ | Identical syntax | -| Memory (Buffer/Summary) | ✅ | ✅ | Same memory implementations | -| Guardrails | ✅ | ✅ | Same guardrail implementations | -| Tools | ✅ | ✅ | Same tool protocol | -| Agent (ReAct-lite) | ✅ | ✅ | Same agent implementation | -| Session Management | ✅ | ✅ | Same session API | -| **On-Device AI Backends** | | | | -| Apple Intelligence | ✅ | ❌ | iOS 26+, macOS 26+ only | -| Gemini Nano | ❌ | ✅ | Android 14+ only | -| **External Model Support** | | | | -| llama.cpp (GGUF) | ✅ | ❌ | iOS 17+ via LocalLLMClient | -| ExecuTorch (GGUF) | ❌ | ✅ | Android API 26+ | -| **Engine System** | ✅ | ✅ | Both platforms support external models | -| InferenceRouter | ✅ | ✅ | Auto-routing to active engine | -| ModelManager | ✅ | ✅ | Download/load/unload GGUF models | -| ModelRegistry | ✅ | ✅ | Available model catalog | -| DeviceCapabilityDetector | ✅ | ❌ | iOS-only hardware detection | -| **RAG** | ✅ | ✅ | Both platforms | -| VectorStore | ✅ | ✅ | In-memory vector storage | -| DocumentChunker | ✅ | ✅ | Multiple chunking strategies | -| EmbeddingEngine | ✅ | ✅ | Text embedding generation | -| RAGManager | ✅ | ✅ | Collection management | -| RAGQueryEngine | ✅ | ✅ | Query pipeline | -| **Personalization** | ✅ | ✅ | Both platforms | -| PersonalizationManager | ✅ | ✅ | Feedback orchestration | -| FeedbackCollector | ✅ | ✅ | User feedback collection | -| PreferenceAnalyzer | ✅ | ✅ | Pattern analysis | -| PromptOptimizer | ✅ | ✅ | Adaptive prompts | +| Feature | iOS | Android | Web | Notes | +| ------------------------------- | --- | ------- | --- | ------------------------------------------ | +| **Core Framework** | ✅ | ✅ | ❌ | Native SDK only (iOS/Android) | +| Chains (7 built-in) | ✅ | ✅ | ❌ | Native SDK only | +| Pipeline DSL | ✅ | ✅ | ❌ | Native SDK only | +| Memory (Buffer/Summary) | ✅ | ✅ | ❌ | Native SDK only | +| Guardrails | ✅ | ✅ | ❌ | Native SDK only | +| Tools | ✅ | ✅ | ❌ | Native SDK only | +| Agent (ReAct-lite) | ✅ | ✅ | ❌ | Native SDK only | +| Session Management | ✅ | ✅ | ❌ | Native SDK only | +| **AI Features (via Libraries)** | | | | | +| Summarize | ✅ | ✅ | ✅ | Web: Chrome Summarizer API | +| Classify | ✅ | ✅ | ✅ | Web: Chrome LanguageModel API | +| Extract | ✅ | ✅ | ✅ | Web: Chrome LanguageModel API | +| Chat | ✅ | ✅ | ✅ | Web: Chrome LanguageModel API | +| Chat Stream | ✅ | ✅ | ✅ | Web: LanguageModel.promptStreaming() | +| Translate | ✅ | ✅ | ✅ | Web: Chrome Translator API | +| Rewrite | ✅ | ✅ | ✅ | Web: Chrome Rewriter API | +| Proofread | ✅ | ✅ | ✅ | Web: Chrome LanguageModel API | +| **On-Device AI Backends** | | | | | +| Apple Intelligence | ✅ | ❌ | ❌ | iOS 26+, macOS 26+ only | +| Gemini Nano | ❌ | ✅ | ✅ | Android 14+ / Chrome 138+ | +| Chrome Built-in AI | ❌ | ❌ | ✅ | Chrome 138+ (Summarizer, Translator, etc.) | +| **External Model Support** | | | | | +| llama.cpp (GGUF) | ✅ | ❌ | ❌ | iOS 17+ via LocalLLMClient | +| ExecuTorch (GGUF) | ❌ | ✅ | ❌ | Android API 26+ | +| **Engine System** | ✅ | ✅ | ❌ | Native SDK only | +| InferenceRouter | ✅ | ✅ | ❌ | Auto-routing to active engine | +| ModelManager | ✅ | ✅ | ❌ | Download/load/unload GGUF models | +| ModelRegistry | ✅ | ✅ | ❌ | Available model catalog | +| DeviceCapabilityDetector | ✅ | ❌ | ❌ | iOS-only hardware detection | +| **RAG** | ✅ | ✅ | ❌ | Native SDK only | +| VectorStore | ✅ | ✅ | ❌ | In-memory vector storage | +| DocumentChunker | ✅ | ✅ | ❌ | Multiple chunking strategies | +| EmbeddingEngine | ✅ | ✅ | ❌ | Text embedding generation | +| RAGManager | ✅ | ✅ | ❌ | Collection management | +| RAGQueryEngine | ✅ | ✅ | ❌ | Query pipeline | +| **Personalization** | ✅ | ✅ | ❌ | Native SDK only | +| PersonalizationManager | ✅ | ✅ | ❌ | Feedback orchestration | +| FeedbackCollector | ✅ | ✅ | ❌ | User feedback collection | +| PreferenceAnalyzer | ✅ | ✅ | ❌ | Pattern analysis | +| PromptOptimizer | ✅ | ✅ | ❌ | Adaptive prompts | ## Platform-Specific APIs @@ -125,6 +135,34 @@ val result = SummarizeChain(model).run("text") **Available:** Android API 26+ +### Web-Only Features (via expo-ondevice-ai) + +#### Chrome Built-in AI + +Web support is available via `expo-ondevice-ai` using Chrome Built-in AI APIs. The web module (`ExpoOndeviceAiModule.web.ts`) maps each feature to the appropriate Chrome API: + +```typescript +// All 8 AI features work on web via Chrome Built-in AI +import { summarize, classify, chat, translate } from "expo-ondevice-ai"; + +const result = await summarize("Long text...", { outputType: "THREE_BULLETS" }); +const translation = await translate("Hello", { targetLanguage: "ko" }); +``` + +**Chrome APIs used:** + +| Chrome API | Features | Notes | +| --------------------------------- | ---------------------------------- | --------------------------------------------- | +| `Summarizer` | summarize | key-points mode, bullet count post-processing | +| `LanguageModel` | classify, extract, chat, proofread | Structured JSON prompts | +| `LanguageModel.promptStreaming()` | chatStream | Auto-detects cumulative vs delta chunks | +| `Translator` | translate | Per-language-pair caching | +| `Rewriter` | rewrite | Tone/length mapping from SDK types | + +**Available:** Chrome 138+ with `chrome://flags/#optimization-guide-on-device-model` enabled + +**Note:** `react-native-ondevice-ai` does NOT support web (Nitro Modules are native-only). Web users should use `expo-ondevice-ai` or `packages/web` standalone SDK. + ## Implementation Differences ### Error Handling @@ -203,21 +241,28 @@ When a feature is only available on one platform, use suffixes: ### iOS -| Requirement | Minimum | Recommended | -|-------------|---------|-------------| -| iOS Version | 17.0 | 26.0 (for Apple Intelligence) | -| macOS Version | 14.0 | 26.0 (for Apple Intelligence) | -| Xcode | 16.0 | 16.0+ | -| Swift | 6.0 | 6.0+ | +| Requirement | Minimum | Recommended | +| ------------- | ------- | ----------------------------- | +| iOS Version | 17.0 | 26.0 (for Apple Intelligence) | +| macOS Version | 14.0 | 26.0 (for Apple Intelligence) | +| Xcode | 16.0 | 16.0+ | +| Swift | 6.0 | 6.0+ | ### Android -| Requirement | Minimum | Recommended | -|-------------|---------|-------------| -| Android API | 26 | 34 (for Prompt API) | -| Kotlin | 2.0 | 2.0+ | -| Android Studio | 2024.1.1 | Latest | -| Gradle | 8.0 | 8.0+ | +| Requirement | Minimum | Recommended | +| -------------- | -------- | ------------------- | +| Android API | 26 | 34 (for Prompt API) | +| Kotlin | 2.0 | 2.0+ | +| Android Studio | 2024.1.1 | Latest | +| Gradle | 8.0 | 8.0+ | + +### Web + +| Requirement | Minimum | Recommended | +| ----------- | ----------------- | ----------------- | +| Chrome | 138 | Latest | +| Gemini Nano | Enabled via flags | Enabled via flags | ## Testing Platform-Specific Features @@ -258,11 +303,23 @@ adb install -r example/build/outputs/apk/debug/example-debug.apk 3. Replace `Flow` → `AsyncThrowingStream` 4. Add `@available` annotations for iOS 26+ APIs +### Web (Expo) + +```bash +# Run Expo example app on web +cd libraries/expo-ondevice-ai/example +bun web + +# Requires Chrome 138+ with chrome://flags/#optimization-guide-on-device-model enabled +``` + ## Summary -- **Core framework** is identical across platforms (Chains, Pipeline, Memory, Guardrails, Tools, Agent, Session) -- **Engine, RAG, Personalization** layers available on both platforms -- **On-device AI backends** differ by platform (Apple Intelligence vs Gemini Nano) -- **External models** supported on both (llama.cpp on iOS, ExecuTorch on Android) +- **Core framework** is identical across iOS and Android (Chains, Pipeline, Memory, Guardrails, Tools, Agent, Session) +- **Engine, RAG, Personalization** layers available on iOS and Android +- **AI features** (summarize, classify, etc.) available on all 3 platforms via library wrappers +- **On-device AI backends** differ by platform (Apple Intelligence / Gemini Nano / Chrome Built-in AI) +- **External models** supported on iOS (llama.cpp) and Android (ExecuTorch), not on web +- **Web support** available via `expo-ondevice-ai` only (not `react-native-ondevice-ai`) - **API naming** is identical for shared features, suffixed for platform-specific features - Always test on **real devices** for accurate on-device AI behavior diff --git a/.github/workflows/ci-expo.yml b/.github/workflows/ci-expo.yml index 2889f04..3214d4a 100644 --- a/.github/workflows/ci-expo.yml +++ b/.github/workflows/ci-expo.yml @@ -54,3 +54,135 @@ jobs: echo "Build output missing!" exit 1 fi + + build-android: + name: Build Android + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.1.38' + + - name: Install library dependencies + working-directory: libraries/expo-ondevice-ai + run: bun install + + - name: Build library and plugin + working-directory: libraries/expo-ondevice-ai + run: bun run build + + - name: Install example dependencies + working-directory: libraries/expo-ondevice-ai/example + run: bun install + + - name: Prebuild Android + working-directory: libraries/expo-ondevice-ai/example + run: bunx expo prebuild --platform android --clean + + - name: Build Android + working-directory: libraries/expo-ondevice-ai/example/android + run: ./gradlew assembleDebug --no-daemon + + build-ios: + name: Build iOS + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.1.38' + + - name: Install library dependencies + working-directory: libraries/expo-ondevice-ai + run: bun install + + - name: Build library and plugin + working-directory: libraries/expo-ondevice-ai + run: bun run build + + - name: Install example dependencies + working-directory: libraries/expo-ondevice-ai/example + run: bun install + + - name: Prebuild iOS + working-directory: libraries/expo-ondevice-ai/example + run: bunx expo prebuild --platform ios --clean + + - name: Cache CocoaPods + uses: actions/cache@v4 + with: + path: libraries/expo-ondevice-ai/example/ios/Pods + key: ${{ runner.os }}-pods-expo-${{ hashFiles('libraries/expo-ondevice-ai/example/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods-expo- + + - name: Install CocoaPods + working-directory: libraries/expo-ondevice-ai/example/ios + run: pod install --no-repo-update + + - name: Build iOS + working-directory: libraries/expo-ondevice-ai/example/ios + run: | + xcodebuild \ + -workspace expoondeviceaiexample.xcworkspace \ + -scheme expoondeviceaiexample \ + -destination 'generic/platform=iOS Simulator' \ + -configuration Debug \ + build \ + CODE_SIGNING_ALLOWED=NO + + build-web: + name: Build Web + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.1.38' + + - name: Install library dependencies + working-directory: libraries/expo-ondevice-ai + run: bun install + + - name: Build library + working-directory: libraries/expo-ondevice-ai + run: bun run build + + - name: Install example dependencies + working-directory: libraries/expo-ondevice-ai/example + run: bun install + + - name: Export Web + working-directory: libraries/expo-ondevice-ai/example + run: bunx expo export --platform web diff --git a/.github/workflows/ci-react-native.yml b/.github/workflows/ci-react-native.yml index df90779..d9ec9b4 100644 --- a/.github/workflows/ci-react-native.yml +++ b/.github/workflows/ci-react-native.yml @@ -61,3 +61,96 @@ jobs: echo "Nitrogen generated files missing!" exit 1 fi + + build-android: + name: Build Android + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.1.38' + + - name: Install library dependencies + working-directory: libraries/react-native-ondevice-ai + run: bun install + + - name: Install example dependencies + working-directory: libraries/react-native-ondevice-ai/example + run: bun install + + - name: Build Android + working-directory: libraries/react-native-ondevice-ai/example/android + run: ./gradlew assembleDebug --no-daemon + + build-ios: + name: Build iOS + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.1.38' + + - name: Install library dependencies + working-directory: libraries/react-native-ondevice-ai + run: bun install + + - name: Install example dependencies + working-directory: libraries/react-native-ondevice-ai/example + run: bun install + + - name: Fix circular symlink + working-directory: libraries/react-native-ondevice-ai/example + run: | + # bun creates a symlink or directory copy for node_modules/react-native-ondevice-ai + # which includes example/ creating an infinite loop that breaks CocoaPods. + # Replace with a shallow copy excluding example/ to break the cycle. + rm -rf node_modules/react-native-ondevice-ai + rsync -a --exclude='example' --exclude='node_modules' ../../ node_modules/react-native-ondevice-ai/ + + - name: Cache CocoaPods + uses: actions/cache@v4 + with: + path: libraries/react-native-ondevice-ai/example/ios/Pods + key: ${{ runner.os }}-pods-rn-${{ hashFiles('libraries/react-native-ondevice-ai/example/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods-rn- + + - name: Install CocoaPods + working-directory: libraries/react-native-ondevice-ai/example/ios + run: pod install --no-repo-update + + - name: Build iOS + working-directory: libraries/react-native-ondevice-ai/example/ios + run: | + xcodebuild \ + -workspace OndeviceAiExample.xcworkspace \ + -scheme OndeviceAiExample \ + -destination 'generic/platform=iOS Simulator' \ + -configuration Debug \ + build \ + CODE_SIGNING_ALLOWED=NO + diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml new file mode 100644 index 0000000..d1bda08 --- /dev/null +++ b/.github/workflows/ci-web.yml @@ -0,0 +1,48 @@ +name: CI Web + +on: + push: + branches: [main] + paths: + - 'packages/web/**' + - '.github/workflows/ci-web.yml' + pull_request: + branches: [main] + paths: + - 'packages/web/**' + - '.github/workflows/ci-web.yml' + +jobs: + lint-and-test: + name: Lint, Test & Build + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/web + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install --legacy-peer-deps --ignore-scripts + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Verify build output + run: | + if [ ! -f "dist/index.js" ] || [ ! -f "dist/index.d.ts" ]; then + echo "Build output missing!" + exit 1 + fi diff --git a/bun.lock b/bun.lock index 3cdf5f7..44bb906 100644 --- a/bun.lock +++ b/bun.lock @@ -75,12 +75,27 @@ "vitest": "^4.0.18", }, }, + "packages/web": { + "name": "locanara", + "version": "1.0.0", + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "esbuild": "^0.24.0", + "jsdom": "^25.0.1", + "terser": "^5.37.0", + "typescript": "^5.9.2", + "vite": "^6.0.7", + "vitest": "^3.0.0", + }, + }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@ardatan/relay-compiler": ["@ardatan/relay-compiler@12.0.3", "", { "dependencies": { "@babel/generator": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/runtime": "^7.26.10", "chalk": "^4.0.0", "fb-watchman": "^2.0.0", "immutable": "~3.7.6", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "relay-runtime": "12.0.0", "signedsource": "^1.0.0" }, "peerDependencies": { "graphql": "*" }, "bin": { "relay-compiler": "bin/relay-compiler" } }, "sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + "@auth/core": ["@auth/core@0.37.0", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "@types/cookie": "0.6.0", "cookie": "0.7.1", "jose": "^5.9.3", "oauth4webapi": "^3.0.0", "preact": "10.11.3", "preact-render-to-string": "5.2.3" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-LybAgfFC5dta3Mu3al0UbnzMGVBpZRqLMvvXupQOfETtPNlL7rXgTO13EVRTCdvPqMQrVYjODUDvgVfQM1M3Qg=="], "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], @@ -127,10 +142,38 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@convex-dev/auth": ["@convex-dev/auth@0.0.90", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "cookie": "^1.0.1", "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", "oauth4webapi": "^3.1.2", "path-to-regexp": "^6.3.0", "server-only": "^0.0.1" }, "peerDependencies": { "@auth/core": "^0.37.0", "convex": "^1.17.0", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["react"], "bin": { "auth": "dist/bin.cjs" } }, "sha512-aqw88EB042HvnaF4wcf/f/wTocmT2Bus2VDQRuV79cM0+8kORM0ICK/ByZ6XsHgQ9qr6TmidNbXm6QAgndrdpQ=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@envelop/core": ["@envelop/core@5.5.0", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-nsU1EyJQAStaKHR1ZkB/ug9XBm+WPTliYtdedbJ/L1ykrp7dbbn0srqBeDnZ2mbZVp4hH3d0Fy+Og9OgPWZx+g=="], @@ -139,57 +182,57 @@ "@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="], "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -577,6 +620,8 @@ "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -607,6 +652,8 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "auto-bind": ["auto-bind@4.0.0", "", {}, "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ=="], "autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="], @@ -631,6 +678,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -649,7 +698,7 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -667,6 +716,8 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -685,6 +736,8 @@ "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], @@ -713,10 +766,14 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], @@ -729,14 +786,20 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "dependency-graph": ["dependency-graph@1.0.0", "", {}, "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -765,6 +828,8 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -785,7 +850,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -853,6 +918,8 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -923,19 +990,25 @@ "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "i18next": ["i18next@24.2.3", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -1007,6 +1080,8 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-relative": ["is-relative@1.0.0", "", { "dependencies": { "is-unc-path": "^1.0.0" } }, "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA=="], @@ -1057,6 +1132,8 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsdom": ["jsdom@25.0.1", "", { "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.12", "parse5": "^7.1.2", "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^2.11.2" }, "optionalPeers": ["canvas"] }, "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -1089,6 +1166,8 @@ "load-json-file": ["load-json-file@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw=="], + "locanara": ["locanara@workspace:packages/web"], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], @@ -1105,6 +1184,8 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], "lower-case-first": ["lower-case-first@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg=="], @@ -1223,6 +1304,10 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -1263,6 +1348,8 @@ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], + "oauth4webapi": ["oauth4webapi@3.8.4", "", {}, "sha512-EKlVEgav8zH31IXxvhCqjEgQws6S9QmnmJyLXmeV5REf59g7VmqRVa5l/rhGWtUqGm2rLVTNwukn9hla5kJ2WQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1297,6 +1384,8 @@ "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], "path-case": ["path-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg=="], @@ -1317,6 +1406,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -1423,6 +1514,8 @@ "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -1433,6 +1526,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -1527,6 +1622,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1539,6 +1636,8 @@ "swap-case": ["swap-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + "sync-fetch": ["sync-fetch@0.6.0", "", { "dependencies": { "node-fetch": "^3.3.2", "timeout-signal": "^2.0.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IELLEvzHuCfc1uTsshPK58ViSdNqXxlml1U+fmwJIKLYKOr/rAtBrorE2RYm5IHaMpDNlmC0fr1LAvdXvyheEQ=="], "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], @@ -1559,13 +1658,23 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "title-case": ["title-case@3.0.3", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA=="], + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -1641,17 +1750,23 @@ "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1671,6 +1786,10 @@ "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1689,6 +1808,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1803,6 +1924,8 @@ "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "@locanara/site/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], "@locanara/site/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], @@ -1813,6 +1936,8 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "@whatwg-node/disposablestack/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@whatwg-node/node-fetch/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1845,6 +1970,8 @@ "cross-inspect/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + "dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "dot-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1873,6 +2000,8 @@ "load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + "locanara/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "lower-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1921,6 +2050,8 @@ "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "swap-case/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1939,6 +2070,8 @@ "upper-case-first/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -2011,6 +2144,8 @@ "convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], + "cross-fetch/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "graphql-config/@graphql-tools/url-loader/@graphql-tools/executor-graphql-ws": ["@graphql-tools/executor-graphql-ws@2.0.7", "", { "dependencies": { "@graphql-tools/executor-common": "^0.0.6", "@graphql-tools/utils": "^10.9.1", "@whatwg-node/disposablestack": "^0.0.6", "graphql-ws": "^6.0.6", "isomorphic-ws": "^5.0.0", "tslib": "^2.8.1", "ws": "^8.18.3" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-J27za7sKF6RjhmvSOwOQFeNhNHyP4f4niqPnerJmq73OtLx9Y2PGOhkXOEB0PjhvPJceuttkD2O1yMgEkTGs3Q=="], "graphql-config/@graphql-tools/url-loader/@graphql-tools/executor-http": ["@graphql-tools/executor-http@1.3.3", "", { "dependencies": { "@graphql-hive/signal": "^1.0.0", "@graphql-tools/executor-common": "^0.0.4", "@graphql-tools/utils": "^10.8.1", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/fetch": "^0.10.4", "@whatwg-node/promise-helpers": "^1.3.0", "meros": "^1.2.1", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A=="], @@ -2023,6 +2158,24 @@ "graphql-config/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "locanara/vitest/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "locanara/vitest/@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "locanara/vitest/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "locanara/vitest/@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "locanara/vitest/@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "locanara/vitest/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "locanara/vitest/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "locanara/vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "locanara/vitest/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + "npm-run-all/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "npm-run-all/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], @@ -2037,6 +2190,56 @@ "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "@inquirer/core/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -2047,6 +2250,10 @@ "cliui/wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "cross-fetch/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "cross-fetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "graphql-config/@graphql-tools/url-loader/@graphql-tools/executor-graphql-ws/@graphql-tools/executor-common": ["@graphql-tools/executor-common@0.0.6", "", { "dependencies": { "@envelop/core": "^5.3.0", "@graphql-tools/utils": "^10.9.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-JAH/R1zf77CSkpYATIJw+eOJwsbWocdDjY+avY7G+P5HCXxwQjAjWVkJI1QJBQYjPQDVxwf1fmTZlIN3VOadow=="], "graphql-config/@graphql-tools/url-loader/@graphql-tools/executor-http/@graphql-hive/signal": ["@graphql-hive/signal@1.0.0", "", {}, "sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag=="], diff --git a/libraries/expo-ondevice-ai/README.md b/libraries/expo-ondevice-ai/README.md index b2be57d..bbecd79 100644 --- a/libraries/expo-ondevice-ai/README.md +++ b/libraries/expo-ondevice-ai/README.md @@ -19,6 +19,7 @@ npx expo install expo-ondevice-ai - Expo SDK 52+ - iOS 26+ (Apple Intelligence) - Android 14+ (Gemini Nano) +- Web: Chrome 138+ (Chrome Built-in AI / Gemini Nano) ## Usage diff --git a/libraries/expo-ondevice-ai/android/build.gradle b/libraries/expo-ondevice-ai/android/build.gradle index 94daee3..0cdd4c1 100644 --- a/libraries/expo-ondevice-ai/android/build.gradle +++ b/libraries/expo-ondevice-ai/android/build.gradle @@ -6,11 +6,19 @@ version = '0.1.0' // Read Locanara version from locanara-versions.json (Single Source of Truth) def getLocanaraVersion() { - def versionsFile = new File(rootProject.projectDir, "../../../locanara-versions.json") - def versionJson = versionsFile.text - // Extract android version using regex: "android": "1.0.2" - def matcher = (versionJson =~ /"android"\s*:\s*"([^"]+)"/) - return matcher ? matcher[0][1] : "1.0.2" + def candidates = [ + new File(project.projectDir, "../../../locanara-versions.json"), + new File(rootProject.projectDir, "../../../locanara-versions.json"), + new File(rootProject.projectDir, "../../../../locanara-versions.json"), + ] + for (candidate in candidates) { + if (candidate.exists()) { + def versionJson = candidate.text + def matcher = (versionJson =~ /"android"\s*:\s*"([^"]+)"/) + if (matcher) return matcher[0][1] + } + } + return "1.0.2" } def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") diff --git a/libraries/expo-ondevice-ai/android/src/main/java/expo/modules/ondeviceai/ExpoOndeviceAiHelper.kt b/libraries/expo-ondevice-ai/android/src/main/java/expo/modules/ondeviceai/ExpoOndeviceAiHelper.kt index 95f3c35..69e6745 100644 --- a/libraries/expo-ondevice-ai/android/src/main/java/expo/modules/ondeviceai/ExpoOndeviceAiHelper.kt +++ b/libraries/expo-ondevice-ai/android/src/main/java/expo/modules/ondeviceai/ExpoOndeviceAiHelper.kt @@ -19,12 +19,11 @@ object ExpoOndeviceAiHelper { } } - fun inputType(options: Map?): String { - return when (options?.get("inputType") as? String) { + fun inputType(options: Map?): String = + when (options?.get("inputType") as? String) { "CONVERSATION" -> "conversation" else -> "text" } - } // endregion diff --git a/libraries/expo-ondevice-ai/example/app.config.ts b/libraries/expo-ondevice-ai/example/app.config.ts index 21efcc2..df03c9b 100644 --- a/libraries/expo-ondevice-ai/example/app.config.ts +++ b/libraries/expo-ondevice-ai/example/app.config.ts @@ -39,6 +39,7 @@ export default ({config}: ConfigContext): ExpoConfig => { '../app.plugin.js', { enableLocalDev: true, + enableLlamaCpp: !process.env.CI, localPath: { ios: LOCAL_LOCANARA_PATHS.ios, android: LOCAL_LOCANARA_PATHS.android, @@ -51,7 +52,7 @@ export default ({config}: ConfigContext): ExpoConfig => { 'expo-build-properties', { ios: { - deploymentTarget: '15.1', + deploymentTarget: '17.0', }, android: { minSdkVersion: 31, diff --git a/libraries/expo-ondevice-ai/example/app/(tabs)/framework.tsx b/libraries/expo-ondevice-ai/example/app/(tabs)/framework.tsx index 7936dcd..ee54794 100644 --- a/libraries/expo-ondevice-ai/example/app/(tabs)/framework.tsx +++ b/libraries/expo-ondevice-ai/example/app/(tabs)/framework.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - View, - FlatList, - StyleSheet, - TouchableOpacity, - Text, -} from 'react-native'; +import {View, FlatList, StyleSheet, TouchableOpacity, Text} from 'react-native'; import {useRouter} from 'expo-router'; import {Ionicons} from '@expo/vector-icons'; import {AIStatusBanner} from '../../components/shared/AIStatusBanner'; @@ -50,8 +44,7 @@ const FRAMEWORK_DEMOS: FrameworkDemo[] = [ id: 'guardrail', name: 'Guardrail', icon: 'shield-checkmark', - description: - 'Wrap chains with input length and content safety guardrails', + description: 'Wrap chains with input length and content safety guardrails', }, { id: 'session', @@ -64,8 +57,7 @@ const FRAMEWORK_DEMOS: FrameworkDemo[] = [ id: 'agent', name: 'Agent + Tools', icon: 'person-circle', - description: - 'ReAct-lite agent with tools and step-by-step reasoning trace', + description: 'ReAct-lite agent with tools and step-by-step reasoning trace', }, ]; @@ -94,9 +86,7 @@ export default function FrameworkScreen() { data={FRAMEWORK_DEMOS} keyExtractor={(item) => item.id} renderItem={({item}) => ( - handlePress(item)} - activeOpacity={0.7}> + handlePress(item)} activeOpacity={0.7}> @@ -131,9 +121,13 @@ const styles = StyleSheet.create({ backgroundColor: 'white', }, iconContainer: { - width: 40, height: 40, borderRadius: 8, + width: 40, + height: 40, + borderRadius: 8, backgroundColor: '#F2F2F7', - justifyContent: 'center', alignItems: 'center', marginRight: 12, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, }, rowContent: {flex: 1, marginRight: 8}, rowName: {fontSize: 17, fontWeight: '600', color: '#000', marginBottom: 2}, diff --git a/libraries/expo-ondevice-ai/example/bun.lock b/libraries/expo-ondevice-ai/example/bun.lock index 903a005..9828691 100644 --- a/libraries/expo-ondevice-ai/example/bun.lock +++ b/libraries/expo-ondevice-ai/example/bun.lock @@ -17,7 +17,7 @@ "expo-status-bar": "~3.0.8", "expo-system-ui": "~6.0.7", "react": "19.1.0", - "react-dom": "^19.2.4", + "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.1", @@ -286,10 +286,6 @@ "@expo/xcpretty": ["@expo/xcpretty@4.4.0", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", "js-yaml": "^4.1.0" }, "bin": { "excpretty": "build/cli.js" } }, "sha512-o2qDlTqJ606h4xR36H2zWTywmZ3v3842K6TU8Ik2n1mfW0S580VHlt3eItVYdLYz+klaPp7CXqanja8eASZjRw=="], - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="], - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], @@ -404,7 +400,7 @@ "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.81.5", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw=="], - "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.12.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-/GtOfVWRligHG0mvX39I1FGdUWeWl0GVF2okEziQSQj0bOTrLIt7y44C3r/aCLkEpTVltCPGM3swqGTH3UfRCw=="], + "@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.14.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-oG2VdoInuIyK0o9o90Yo47hTCS+sPyVE7k8eSB37vt3pq3uQxjh8V3xJpsQfOfNlRUXOPB/ejH93nSBlP7ZHmQ=="], "@react-navigation/core": ["@react-navigation/core@7.14.0", "", { "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g=="], @@ -412,7 +408,7 @@ "@react-navigation/native": ["@react-navigation/native@7.1.28", "", { "dependencies": { "@react-navigation/core": "^7.14.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ=="], - "@react-navigation/native-stack": ["@react-navigation/native-stack@7.12.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-XmNJsPshjkNsahgbxNgGWQUq4s1l6HqH/Fei4QsjBNn/0mTvVrRVZwJ1XrY9YhWYvyiYkAN6/OmarWQaQJ0otQ=="], + "@react-navigation/native-stack": ["@react-navigation/native-stack@7.13.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-5OOp1IKEd5woHl9hGBU0qCAfrQ4+7Tqej0HzDzGQeXzS8tg9gq84x1qUdRvFk5BXbhuAyvJliY9F1/I07d2X0A=="], "@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="], @@ -452,7 +448,7 @@ "@types/jsdom": ["@types/jsdom@20.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ=="], - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], @@ -482,15 +478,15 @@ "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn-globals": ["acorn-globals@7.0.1", "", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], @@ -546,7 +542,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], @@ -576,7 +572,7 @@ "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -688,7 +684,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], @@ -1074,9 +1070,9 @@ "metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="], - "metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], + "metro-runtime": ["metro-runtime@0.83.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-sWj9KN311yG22Zv0kVbAp9dorB9HtTThvQKsAn6PLxrVrz+1UBsLrQSxjE/s4PtzDi1HABC648jo4K9Euz/5jw=="], - "metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + "metro-source-map": ["metro-source-map@0.83.4", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.4", "nullthrows": "^1.1.1", "ob1": "0.83.4", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-pPbmQwS0zgU+/0u5KPkuvlsQP0V+WYQ9qNshqupIL720QRH0vS3QR25IVVtbunofEDJchI11Q4QtIbmUyhpOBw=="], "metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="], @@ -1100,7 +1096,7 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], @@ -1136,7 +1132,7 @@ "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], - "ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], + "ob1": ["ob1@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-9JiflaRKCkxKzH8uuZlax72cHzZ8iFLsNIORFOAKDgZUOfvfwYWOVS0ezGLzPp/yEhVktD+PTTImC0AAehSOBw=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1174,7 +1170,7 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -1226,7 +1222,7 @@ "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-error-boundary": ["react-error-boundary@3.1.4", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA=="], @@ -1250,7 +1246,7 @@ "react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="], - "react-native-worklets": ["react-native-worklets@0.7.2", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog=="], + "react-native-worklets": ["react-native-worklets@0.7.4", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], @@ -1402,7 +1398,7 @@ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="], + "tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="], "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], @@ -1444,7 +1440,7 @@ "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -1542,35 +1538,39 @@ "@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "@expo/cli/glob": ["glob@13.0.1", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], + "@expo/cli/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@expo/cli/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@expo/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@expo/cli/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "@expo/config/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - "@expo/config/glob": ["glob@13.0.1", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], + "@expo/config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@expo/config/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@expo/config/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@expo/config-plugins/glob": ["glob@13.0.1", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], + "@expo/config-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@expo/config-plugins/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@expo/config-plugins/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "@expo/fingerprint/glob": ["glob@13.0.1", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], + "@expo/fingerprint/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@expo/fingerprint/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@expo/fingerprint/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@expo/image-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@expo/image-utils/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="], - "@expo/metro-config/glob": ["glob@13.0.1", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], + "@expo/metro/metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], + + "@expo/metro/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + + "@expo/metro-config/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], - "@expo/prebuild-config/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@expo/prebuild-config/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@expo/xcpretty/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -1592,9 +1592,9 @@ "@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w=="], - "@react-native/community-cli-plugin/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@react-native/community-cli-plugin/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@types/react-test-renderer/@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], + "@types/react-test-renderer/@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -1614,7 +1614,7 @@ "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "expo-build-properties/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "expo-build-properties/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], @@ -1640,7 +1640,7 @@ "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], - "jest-snapshot/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "jest-snapshot/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1666,23 +1666,35 @@ "log-symbols/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], "metro/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + "metro/metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], + + "metro/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + "metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "metro-babel-transformer/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], "metro-cache/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "metro-config/metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="], + + "metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-clyWAXDgkDHPwvldl95pcLTrJIqUj9GbZayL8tfeUs69ilsIUBpVym2lRd/8l3/8PIHCInxL868NvD2Y7OqKXg=="], + + "metro-symbolicate/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + + "metro-transform-worker/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "npm-package-arg/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "npm-package-arg/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], @@ -1690,7 +1702,7 @@ "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "path-scurry/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -1700,9 +1712,7 @@ "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], - "react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - - "react-native/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "react-native/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "react-native-reanimated/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -1752,19 +1762,21 @@ "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "@expo/cli/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], + "@expo/cli/glob/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + + "@expo/config-plugins/glob/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - "@expo/config-plugins/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], + "@expo/config/glob/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - "@expo/config/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], + "@expo/fingerprint/glob/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - "@expo/fingerprint/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], + "@expo/metro-config/glob/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - "@expo/metro-config/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], + "@expo/metro/metro-source-map/ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], "@expo/xcpretty/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@jest/reporters/istanbul-lib-instrument/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@jest/reporters/istanbul-lib-instrument/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@jest/reporters/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], @@ -1794,8 +1806,14 @@ "metro-cache/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "metro-symbolicate/metro-source-map/ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], + + "metro-transform-worker/metro-source-map/ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], + "metro/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], + "metro/metro-source-map/ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="], + "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -1820,6 +1838,16 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "@expo/cli/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "@expo/config-plugins/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "@expo/config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "@expo/fingerprint/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "@expo/metro-config/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + "log-symbols/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "log-symbols/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -1830,6 +1858,16 @@ "@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "@expo/cli/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + + "@expo/config-plugins/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + + "@expo/config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + + "@expo/fingerprint/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + + "@expo/metro-config/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], diff --git a/libraries/expo-ondevice-ai/example/components/AppState.tsx b/libraries/expo-ondevice-ai/example/components/AppState.tsx index 0c5f7f5..f1ecb9a 100644 --- a/libraries/expo-ondevice-ai/example/components/AppState.tsx +++ b/libraries/expo-ondevice-ai/example/components/AppState.tsx @@ -241,11 +241,23 @@ export function AppStateProvider({children}: {children: ReactNode}) { setIsModelReady(cap.isModelReady ?? cap.isSupported); // Set device info + const platformLabel = + Platform.OS === 'ios' + ? 'iOS' + : Platform.OS === 'web' + ? 'Web' + : 'Android'; + const providerLabel = + Platform.OS === 'web' + ? 'Chrome Built-in AI' + : cap.platform === 'IOS' + ? 'Apple Intelligence' + : 'Gemini Nano'; setDeviceInfo({ - platform: Platform.OS === 'ios' ? 'iOS' : 'Android', + platform: platformLabel, osVersion: Platform.Version.toString(), supportsOnDeviceAI: cap.isSupported, - provider: cap.platform === 'IOS' ? 'Apple Intelligence' : 'Gemini Nano', + provider: providerLabel, }); // Set available features based on capability diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ChatDemo/index.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ChatDemo/index.tsx index 9dcb816..f2fb13b 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ChatDemo/index.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ChatDemo/index.tsx @@ -308,6 +308,7 @@ const styles = StyleSheet.create({ fontSize: 16, maxHeight: 100, color: '#000', + ...(Platform.OS === 'web' ? {outlineStyle: 'none' as any} : {}), }, sendButton: { padding: 4, diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ClassifyDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ClassifyDemo.tsx index 5b16e27..8377dec 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ClassifyDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ClassifyDemo.tsx @@ -48,9 +48,19 @@ export function ClassifyDemo() { const classifyResult = await classify(inputText, options); console.log('[DEBUG] classify response:', JSON.stringify(classifyResult)); setResult(classifyResult); - setDebugLog({api: 'classify', request: {text: inputText.substring(0, 100) + '...', options}, response: classifyResult, timing: Date.now() - start}); + setDebugLog({ + api: 'classify', + request: {text: inputText.substring(0, 100) + '...', options}, + response: classifyResult, + timing: Date.now() - start, + }); } catch (error: any) { - setDebugLog({api: 'classify', request: {text: inputText.substring(0, 100) + '...'}, response: {error: error.message}, timing: Date.now() - start}); + setDebugLog({ + api: 'classify', + request: {text: inputText.substring(0, 100) + '...'}, + response: {error: error.message}, + timing: Date.now() - start, + }); setErrorMessage(error.message || 'Failed to classify'); } finally { setIsLoading(false); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ExtractDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ExtractDemo.tsx index 6502e38..c3434f1 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ExtractDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ExtractDemo.tsx @@ -26,6 +26,7 @@ const ENTITY_COLORS: Record = { phone: '#34C759', date: '#AF52DE', location: '#FF3B30', + organization: '#5856D6', }; export function ExtractDemo() { @@ -43,15 +44,28 @@ export function ExtractDemo() { const start = Date.now(); try { - const options = {entityTypes: ['person', 'email', 'phone', 'date', 'location'], extractKeyValues: true}; + const options = { + entityTypes: ['person', 'email', 'phone', 'date', 'location'], + extractKeyValues: true, + }; console.log('[DEBUG] extract request:', JSON.stringify(options)); const extractResult = await extract(inputText, options); console.log('[DEBUG] extract response:', JSON.stringify(extractResult)); setResult(extractResult); - setDebugLog({api: 'extract', request: {text: inputText.substring(0, 100) + '...', options}, response: extractResult, timing: Date.now() - start}); + setDebugLog({ + api: 'extract', + request: {text: inputText.substring(0, 100) + '...', options}, + response: extractResult, + timing: Date.now() - start, + }); } catch (error: any) { setErrorMessage(error.message || 'Failed to extract entities'); - setDebugLog({api: 'extract', request: {text: inputText.substring(0, 100) + '...'}, response: {error: error.message}, timing: Date.now() - start}); + setDebugLog({ + api: 'extract', + request: {text: inputText.substring(0, 100) + '...'}, + response: {error: error.message}, + timing: Date.now() - start, + }); } finally { setIsLoading(false); } diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ProofreadDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ProofreadDemo.tsx index 11351fc..8a993d0 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ProofreadDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/ProofreadDemo.tsx @@ -49,11 +49,24 @@ export function ProofreadDemo() { const options = {inputType: selectedInputType}; console.log('[DEBUG] proofread request:', JSON.stringify(options)); const proofreadResult = await proofread(inputText, options); - console.log('[DEBUG] proofread response:', JSON.stringify(proofreadResult)); + console.log( + '[DEBUG] proofread response:', + JSON.stringify(proofreadResult), + ); setResult(proofreadResult); - setDebugLog({api: 'proofread', request: {text: inputText.substring(0, 100) + '...', options}, response: proofreadResult, timing: Date.now() - start}); + setDebugLog({ + api: 'proofread', + request: {text: inputText.substring(0, 100) + '...', options}, + response: proofreadResult, + timing: Date.now() - start, + }); } catch (error: any) { - setDebugLog({api: 'proofread', request: {text: inputText.substring(0, 100) + '...'}, response: {error: error.message}, timing: Date.now() - start}); + setDebugLog({ + api: 'proofread', + request: {text: inputText.substring(0, 100) + '...'}, + response: {error: error.message}, + timing: Date.now() - start, + }); setErrorMessage(error.message || 'Failed to proofread'); } finally { setIsLoading(false); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/RewriteDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/RewriteDemo.tsx index 0dfb989..25e0ed3 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/RewriteDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/RewriteDemo.tsx @@ -55,9 +55,19 @@ export function RewriteDemo() { const rewriteResult = await rewrite(inputText, options); console.log('[DEBUG] rewrite response:', JSON.stringify(rewriteResult)); setResult(rewriteResult); - setDebugLog({api: 'rewrite', request: {text: inputText.substring(0, 100) + '...', options}, response: rewriteResult, timing: Date.now() - start}); + setDebugLog({ + api: 'rewrite', + request: {text: inputText.substring(0, 100) + '...', options}, + response: rewriteResult, + timing: Date.now() - start, + }); } catch (error: any) { - setDebugLog({api: 'rewrite', request: {text: inputText.substring(0, 100) + '...'}, response: {error: error.message}, timing: Date.now() - start}); + setDebugLog({ + api: 'rewrite', + request: {text: inputText.substring(0, 100) + '...'}, + response: {error: error.message}, + timing: Date.now() - start, + }); setErrorMessage(error.message || 'Failed to rewrite'); } finally { setIsLoading(false); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/SummarizeDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/SummarizeDemo.tsx index ba3fe83..576f87a 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/SummarizeDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/SummarizeDemo.tsx @@ -41,14 +41,30 @@ export function SummarizeDemo() { const start = Date.now(); try { - const options = {inputType: selectedInputType, outputType: selectedOutputType}; + const options = { + inputType: selectedInputType, + outputType: selectedOutputType, + }; console.log('[DEBUG] summarize request:', JSON.stringify(options)); const summarizeResult = await summarize(inputText, options); - console.log('[DEBUG] summarize response:', JSON.stringify(summarizeResult)); + console.log( + '[DEBUG] summarize response:', + JSON.stringify(summarizeResult), + ); setResult(summarizeResult); - setDebugLog({api: 'summarize', request: {text: inputText.substring(0, 100) + '...', options}, response: summarizeResult, timing: Date.now() - start}); + setDebugLog({ + api: 'summarize', + request: {text: inputText.substring(0, 100) + '...', options}, + response: summarizeResult, + timing: Date.now() - start, + }); } catch (error: any) { - setDebugLog({api: 'summarize', request: {text: inputText.substring(0, 100) + '...'}, response: {error: error.message}, timing: Date.now() - start}); + setDebugLog({ + api: 'summarize', + request: {text: inputText.substring(0, 100) + '...'}, + response: {error: error.message}, + timing: Date.now() - start, + }); setErrorMessage(error.message || 'Failed to summarize'); } finally { setIsLoading(false); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/TranslateDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/TranslateDemo.tsx index 2ae308c..2946ee4 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/TranslateDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FeatureDetail/TranslateDemo.tsx @@ -48,11 +48,24 @@ export function TranslateDemo() { const options = {targetLanguage}; console.log('[DEBUG] translate request:', JSON.stringify(options)); const translateResult = await translate(inputText, options); - console.log('[DEBUG] translate response:', JSON.stringify(translateResult)); + console.log( + '[DEBUG] translate response:', + JSON.stringify(translateResult), + ); setResult(translateResult); - setDebugLog({api: 'translate', request: {text: inputText.substring(0, 100) + '...', options}, response: translateResult, timing: Date.now() - start}); + setDebugLog({ + api: 'translate', + request: {text: inputText.substring(0, 100) + '...', options}, + response: translateResult, + timing: Date.now() - start, + }); } catch (error: any) { - setDebugLog({api: 'translate', request: {text: inputText.substring(0, 100) + '...'}, response: {error: error.message}, timing: Date.now() - start}); + setDebugLog({ + api: 'translate', + request: {text: inputText.substring(0, 100) + '...'}, + response: {error: error.message}, + timing: Date.now() - start, + }); setErrorMessage(error.message || 'Failed to translate'); } finally { setIsLoading(false); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/AgentDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/AgentDemo.tsx index 4d4ab7c..00088de 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/AgentDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/AgentDemo.tsx @@ -1,20 +1,57 @@ import React, {useState} from 'react'; -import {View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator} from 'react-native'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + ActivityIndicator, +} from 'react-native'; import {chat, summarize} from 'expo-ondevice-ai'; import {Ionicons} from '@expo/vector-icons'; import {CodePatternCard} from './CodePatternCard'; import {StatBadge} from '../../shared/StatBadge'; -interface ReasoningStep { step: number; thought: string; action: string; observation: string; } +interface ReasoningStep { + step: number; + thought: string; + action: string; + observation: string; +} const LOCAL_DOCS = [ - {id: 'doc1', title: 'On-Device AI Overview', content: 'On-device AI processes data locally on the user\'s device without sending it to cloud servers. This ensures privacy and enables offline functionality. Apple Intelligence uses Foundation Models, while Android uses Gemini Nano via ML Kit.'}, - {id: 'doc2', title: 'Privacy & Security', content: 'On-device AI keeps all user data on the device. No data is transmitted to external servers. This makes it ideal for sensitive applications like health data, financial information, and personal communications.'}, - {id: 'doc3', title: 'Neural Processing Units', content: 'Modern devices include dedicated NPUs (Neural Processing Units) optimized for AI inference. Apple\'s Neural Engine can perform 35 trillion operations per second. Google\'s Tensor chips include a dedicated TPU for on-device ML.'}, - {id: 'doc4', title: 'Locanara Framework', content: 'Locanara is an open-source on-device AI framework inspired by LangChain. It provides composable chains, memory management, guardrails, and a pipeline DSL for building production AI features using platform-native models.'}, + { + id: 'doc1', + title: 'On-Device AI Overview', + content: + "On-device AI processes data locally on the user's device without sending it to cloud servers. This ensures privacy and enables offline functionality. Apple Intelligence uses Foundation Models, while Android uses Gemini Nano via ML Kit.", + }, + { + id: 'doc2', + title: 'Privacy & Security', + content: + 'On-device AI keeps all user data on the device. No data is transmitted to external servers. This makes it ideal for sensitive applications like health data, financial information, and personal communications.', + }, + { + id: 'doc3', + title: 'Neural Processing Units', + content: + "Modern devices include dedicated NPUs (Neural Processing Units) optimized for AI inference. Apple's Neural Engine can perform 35 trillion operations per second. Google's Tensor chips include a dedicated TPU for on-device ML.", + }, + { + id: 'doc4', + title: 'Locanara Framework', + content: + 'Locanara is an open-source on-device AI framework inspired by LangChain. It provides composable chains, memory management, guardrails, and a pipeline DSL for building production AI features using platform-native models.', + }, ]; -const SUGGESTIONS = ['What privacy benefits does on-device AI provide?', 'Summarize what NPUs can do', 'What is the Locanara framework?']; +const SUGGESTIONS = [ + 'What privacy benefits does on-device AI provide?', + 'Summarize what NPUs can do', + 'What is the Locanara framework?', +]; export function AgentDemo() { const [input, setInput] = useState(''); @@ -27,90 +64,284 @@ export function AgentDemo() { const handleRun = async () => { const query = input.trim(); if (!query || isProcessing) return; - setIsProcessing(true); setSteps([]); setFinalAnswer(''); setProcessingTime(null); + setIsProcessing(true); + setSteps([]); + setFinalAnswer(''); + setProcessingTime(null); const start = Date.now(); try { const keywords = query.toLowerCase().split(/\s+/); - const relevant = LOCAL_DOCS.filter(d => keywords.some(kw => kw.length > 3 && (d.content.toLowerCase().includes(kw) || d.title.toLowerCase().includes(kw)))); - const s1: ReasoningStep = {step: 1, thought: `I need to find information about "${query}" in local documents.`, action: 'LocalSearchTool', observation: relevant.length > 0 ? `Found ${relevant.length} relevant document(s): ${relevant.map(d => d.title).join(', ')}` : 'No matching documents found.'}; + const relevant = LOCAL_DOCS.filter((d) => + keywords.some( + (kw) => + kw.length > 3 && + (d.content.toLowerCase().includes(kw) || + d.title.toLowerCase().includes(kw)), + ), + ); + const s1: ReasoningStep = { + step: 1, + thought: `I need to find information about "${query}" in local documents.`, + action: 'LocalSearchTool', + observation: + relevant.length > 0 + ? `Found ${relevant.length} relevant document(s): ${relevant.map((d) => d.title).join(', ')}` + : 'No matching documents found.', + }; setSteps([s1]); - const context = relevant.map(d => d.content).join('\n\n'); - const s2: ReasoningStep = {step: 2, thought: relevant.length > 0 ? 'I have relevant context. Let me answer using this information.' : 'No documents found. Let me try general knowledge.', action: 'ChatChain', observation: ''}; - setSteps(prev => [...prev, s2]); + const context = relevant.map((d) => d.content).join('\n\n'); + const s2: ReasoningStep = { + step: 2, + thought: + relevant.length > 0 + ? 'I have relevant context. Let me answer using this information.' + : 'No documents found. Let me try general knowledge.', + action: 'ChatChain', + observation: '', + }; + setSteps((prev) => [...prev, s2]); - console.log('[DEBUG] agent chat request:', JSON.stringify({query: query.substring(0, 100), contextLength: context.length})); - const result = await chat(query, {systemPrompt: `You are a helpful assistant. Answer based on this context:\n\n${context}\n\nKeep your answer concise (2-3 sentences).`}); + console.log( + '[DEBUG] agent chat request:', + JSON.stringify({ + query: query.substring(0, 100), + contextLength: context.length, + }), + ); + const result = await chat(query, { + systemPrompt: `You are a helpful assistant. Answer based on this context:\n\n${context}\n\nKeep your answer concise (2-3 sentences).`, + }); console.log('[DEBUG] agent chat response:', JSON.stringify(result)); - setSteps(prev => { const u = [...prev]; u[u.length - 1] = {...u[u.length - 1], observation: result.message}; return u; }); + setSteps((prev) => { + const u = [...prev]; + u[u.length - 1] = {...u[u.length - 1], observation: result.message}; + return u; + }); if (result.message.length > 200) { - const s3: ReasoningStep = {step: 3, thought: 'The response is long. Let me summarize.', action: 'SummarizeChain', observation: ''}; - setSteps(prev => [...prev, s3]); + const s3: ReasoningStep = { + step: 3, + thought: 'The response is long. Let me summarize.', + action: 'SummarizeChain', + observation: '', + }; + setSteps((prev) => [...prev, s3]); const sr = await summarize(result.message); - setSteps(prev => { const u = [...prev]; u[u.length - 1] = {...u[u.length - 1], observation: sr.summary}; return u; }); + setSteps((prev) => { + const u = [...prev]; + u[u.length - 1] = {...u[u.length - 1], observation: sr.summary}; + return u; + }); setFinalAnswer(sr.summary); - } else { setFinalAnswer(result.message); } + } else { + setFinalAnswer(result.message); + } setProcessingTime(Date.now() - start); - } catch (e: any) { setFinalAnswer(`Error: ${e.message}`); } - finally { setIsProcessing(false); } + } catch (e: any) { + setFinalAnswer(`Error: ${e.message}`); + } finally { + setIsProcessing(false); + } }; return ( - - setShowDocs(!showDocs)}> + + setShowDocs(!showDocs)} + > - Local Documents ({LOCAL_DOCS.length}) - + + Local Documents ({LOCAL_DOCS.length}) + + - {showDocs && {LOCAL_DOCS.map(d => {d.title}{d.content})}} + {showDocs && ( + + {LOCAL_DOCS.map((d) => ( + + {d.title} + + {d.content} + + + ))} + + )} Suggested Queries - {SUGGESTIONS.map((s, i) => setInput(s)}>{s})} - - - {isProcessing ? : Run Agent} + + {SUGGESTIONS.map((s, i) => ( + setInput(s)} + > + {s} + + ))} + + + + {isProcessing ? ( + + ) : ( + Run Agent + )} {steps.length > 0 && ( Reasoning Trace - {processingTime !== null && } + {processingTime !== null && ( + + + + + )} {steps.map((s, i) => ( 0 && styles.stepSep]}> Step {s.step} - Thought: {s.thought} - Action: {s.action} - {s.observation ? Observation: {s.observation} : null} + + + Thought: + {s.thought} + + + + Action: + {s.action} + + {s.observation ? ( + + + Observation: + {s.observation} + + ) : null} ))} )} - {finalAnswer ? Final Answer{finalAnswer} : null} + {finalAnswer ? ( + + Final Answer + {finalAnswer} + + ) : null} ); } const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#F2F2F7'}, content: {padding: 16, paddingBottom: 40}, - sectionTitle: {fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, marginTop: 4}, - docsToggle: {flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: 'white', padding: 12, borderRadius: 10, marginBottom: 8}, + container: {flex: 1, backgroundColor: '#F2F2F7'}, + content: {padding: 16, paddingBottom: 40}, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + marginTop: 4, + }, + docsToggle: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: 'white', + padding: 12, + borderRadius: 10, + marginBottom: 8, + }, docsText: {flex: 1, fontSize: 14, fontWeight: '600', color: '#333'}, - docsCard: {backgroundColor: 'white', borderRadius: 10, padding: 12, marginBottom: 12}, - docItem: {paddingVertical: 6}, docTitle: {fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 2}, docContent: {fontSize: 13, color: '#666', lineHeight: 18}, + docsCard: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + marginBottom: 12, + }, + docItem: {paddingVertical: 6}, + docTitle: {fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 2}, + docContent: {fontSize: 13, color: '#666', lineHeight: 18}, suggestions: {gap: 8, marginBottom: 12}, - chip: {backgroundColor: 'white', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, borderWidth: 1, borderColor: '#E5E5EA'}, + chip: { + backgroundColor: 'white', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 8, + borderWidth: 1, + borderColor: '#E5E5EA', + }, chipText: {fontSize: 14, color: '#007AFF'}, - input: {backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, marginBottom: 12, color: '#000'}, - runBtn: {backgroundColor: '#007AFF', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 16}, - runBtnDis: {opacity: 0.6}, runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, - traceCard: {backgroundColor: 'white', borderRadius: 10, padding: 16, marginBottom: 12}, + input: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + marginBottom: 12, + color: '#000', + }, + runBtn: { + backgroundColor: '#007AFF', + paddingVertical: 14, + borderRadius: 10, + alignItems: 'center', + marginBottom: 16, + }, + runBtnDis: {opacity: 0.6}, + runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, + traceCard: { + backgroundColor: 'white', + borderRadius: 10, + padding: 16, + marginBottom: 12, + }, traceTitle: {fontSize: 15, fontWeight: '600', color: '#333', marginBottom: 8}, badgeRow: {flexDirection: 'row', gap: 8, marginBottom: 12}, stepBlock: {paddingVertical: 4}, - stepSep: {borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#E5E5EA', marginTop: 8, paddingTop: 12}, + stepSep: { + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#E5E5EA', + marginTop: 8, + paddingTop: 12, + }, stepHeader: {fontSize: 14, fontWeight: '700', color: '#333', marginBottom: 6}, - traceRow: {flexDirection: 'row', alignItems: 'flex-start', gap: 6, paddingVertical: 2}, - traceLabel: {fontSize: 13, fontWeight: '600', color: '#666'}, traceText: {flex: 1, fontSize: 13, color: '#333', lineHeight: 18}, - answerCard: {backgroundColor: '#E8F5E9', borderRadius: 10, padding: 16, borderLeftWidth: 4, borderLeftColor: '#34C759'}, - answerTitle: {fontSize: 15, fontWeight: '600', color: '#2E7D32', marginBottom: 8}, answerText: {fontSize: 15, color: '#333', lineHeight: 22}, + traceRow: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: 6, + paddingVertical: 2, + }, + traceLabel: {fontSize: 13, fontWeight: '600', color: '#666'}, + traceText: {flex: 1, fontSize: 13, color: '#333', lineHeight: 18}, + answerCard: { + backgroundColor: '#E8F5E9', + borderRadius: 10, + padding: 16, + borderLeftWidth: 4, + borderLeftColor: '#34C759', + }, + answerTitle: { + fontSize: 15, + fontWeight: '600', + color: '#2E7D32', + marginBottom: 8, + }, + answerText: {fontSize: 15, color: '#333', lineHeight: 22}, }); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ChainDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ChainDemo.tsx index bb62714..05d1b5d 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ChainDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ChainDemo.tsx @@ -1,14 +1,25 @@ import React, {useState} from 'react'; -import {View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator} from 'react-native'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + ActivityIndicator, +} from 'react-native'; import {summarize, classify} from 'expo-ondevice-ai'; import {CodePatternCard} from './CodePatternCard'; import {StatBadge} from '../../shared/StatBadge'; type ChainType = 'sequential' | 'parallel' | 'conditional'; const CHAIN_TYPES: {key: ChainType; label: string}[] = [ - {key: 'sequential', label: 'Sequential'}, {key: 'parallel', label: 'Parallel'}, {key: 'conditional', label: 'Conditional'}, + {key: 'sequential', label: 'Sequential'}, + {key: 'parallel', label: 'Parallel'}, + {key: 'conditional', label: 'Conditional'}, ]; -const SAMPLE = 'Artificial intelligence is rapidly transforming healthcare, enabling early disease detection and personalized treatment plans. Machine learning models can now analyze medical images with accuracy comparable to specialists. This technological advancement promises to make quality healthcare more accessible and affordable worldwide.'; +const SAMPLE = + 'Artificial intelligence is rapidly transforming healthcare, enabling early disease detection and personalized treatment plans. Machine learning models can now analyze medical images with accuracy comparable to specialists. This technological advancement promises to make quality healthcare more accessible and affordable worldwide.'; export function ChainDemo() { const [input, setInput] = useState(SAMPLE); @@ -19,32 +30,52 @@ export function ChainDemo() { const handleRun = async () => { if (!input.trim() || isProcessing) return; - setIsProcessing(true); setResults([]); setProcessingTime(null); + setIsProcessing(true); + setResults([]); + setProcessingTime(null); const start = Date.now(); try { - console.log('[DEBUG] chain request:', JSON.stringify({chainType, inputLength: input.length})); + console.log( + '[DEBUG] chain request:', + JSON.stringify({chainType, inputLength: input.length}), + ); if (chainType === 'sequential') { const s1 = await summarize(input); console.log('[DEBUG] chain step1 (summarize):', JSON.stringify(s1)); - setResults(prev => [...prev, `Step 1 (Summarize): ${s1.summary}`]); + setResults((prev) => [...prev, `Step 1 (Summarize): ${s1.summary}`]); const s2 = await classify(s1.summary); console.log('[DEBUG] chain step2 (classify):', JSON.stringify(s2)); - setResults(prev => [...prev, `Step 2 (Classify): ${s2.topClassification.label} (${(s2.topClassification.score*100).toFixed(0)}%)`]); + setResults((prev) => [ + ...prev, + `Step 2 (Classify): ${s2.topClassification.label} (${(s2.topClassification.score * 100).toFixed(0)}%)`, + ]); } else if (chainType === 'parallel') { const [s, c] = await Promise.all([summarize(input), classify(input)]); - setResults([`Summarize: ${s.summary}`, `Classify: ${c.topClassification.label} (${(c.topClassification.score*100).toFixed(0)}%)`]); + setResults([ + `Summarize: ${s.summary}`, + `Classify: ${c.topClassification.label} (${(c.topClassification.score * 100).toFixed(0)}%)`, + ]); } else { if (input.length > 200) { const r = await summarize(input); - setResults([`Condition: Text is long (${input.length} chars) → Summarize`, `Result: ${r.summary}`]); + setResults([ + `Condition: Text is long (${input.length} chars) → Summarize`, + `Result: ${r.summary}`, + ]); } else { const r = await classify(input); - setResults([`Condition: Text is short (${input.length} chars) → Classify`, `Result: ${r.topClassification.label} (${(r.topClassification.score*100).toFixed(0)}%)`]); + setResults([ + `Condition: Text is short (${input.length} chars) → Classify`, + `Result: ${r.topClassification.label} (${(r.topClassification.score * 100).toFixed(0)}%)`, + ]); } } setProcessingTime(Date.now() - start); - } catch (e: any) { setResults([`Error: ${e.message}`]); } - finally { setIsProcessing(false); } + } catch (e: any) { + setResults([`Error: ${e.message}`]); + } finally { + setIsProcessing(false); + } }; const codes: Record = { @@ -58,21 +89,54 @@ export function ChainDemo() { Chain Type - {CHAIN_TYPES.map(ct => ( - setChainType(ct.key)}> - {ct.label} + {CHAIN_TYPES.map((ct) => ( + setChainType(ct.key)} + > + + {ct.label} + ))} - - - {isProcessing ? : Run Chain} + + + {isProcessing ? ( + + ) : ( + Run Chain + )} {results.length > 0 && ( - {processingTime !== null && } + {processingTime !== null && ( + + + + + )} {results.map((r, i) => ( - 0 && styles.stepSep]}>{r} + 0 && styles.stepSep]}> + {r} + ))} )} @@ -81,18 +145,55 @@ export function ChainDemo() { } const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#F2F2F7'}, content: {padding: 16, paddingBottom: 40}, - sectionTitle: {fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, marginTop: 4}, + container: {flex: 1, backgroundColor: '#F2F2F7'}, + content: {padding: 16, paddingBottom: 40}, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + marginTop: 4, + }, row: {flexDirection: 'row', gap: 8, marginBottom: 16}, - btn: {flex: 1, paddingVertical: 10, borderRadius: 8, backgroundColor: 'white', alignItems: 'center', borderWidth: 1, borderColor: '#E5E5EA'}, + btn: { + flex: 1, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: 'white', + alignItems: 'center', + borderWidth: 1, + borderColor: '#E5E5EA', + }, btnSel: {backgroundColor: '#007AFF', borderColor: '#007AFF'}, - btnText: {fontSize: 14, fontWeight: '600', color: '#333'}, btnTextSel: {color: 'white'}, - input: {backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, minHeight: 80, textAlignVertical: 'top', marginBottom: 12, color: '#000'}, - runBtn: {backgroundColor: '#007AFF', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 16}, - runBtnDis: {opacity: 0.6}, runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, + btnText: {fontSize: 14, fontWeight: '600', color: '#333'}, + btnTextSel: {color: 'white'}, + input: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + minHeight: 80, + textAlignVertical: 'top', + marginBottom: 12, + color: '#000', + }, + runBtn: { + backgroundColor: '#007AFF', + paddingVertical: 14, + borderRadius: 10, + alignItems: 'center', + marginBottom: 16, + }, + runBtnDis: {opacity: 0.6}, + runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, resultCard: {backgroundColor: 'white', borderRadius: 10, padding: 16}, badgeRow: {flexDirection: 'row', gap: 8, marginBottom: 12}, stepRow: {paddingVertical: 4}, - stepSep: {borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#E5E5EA', marginTop: 8, paddingTop: 12}, + stepSep: { + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#E5E5EA', + marginTop: 8, + paddingTop: 12, + }, resultText: {fontSize: 15, color: '#333', lineHeight: 22}, }); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/CodePatternCard.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/CodePatternCard.tsx index 16ec9ad..9f05036 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/CodePatternCard.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/CodePatternCard.tsx @@ -15,7 +15,8 @@ export function CodePatternCard({title, code}: Props) { setExpanded(!expanded)} - activeOpacity={0.7}> + activeOpacity={0.7} + > {title} (null); + const [blockedPatterns, setBlockedPatterns] = useState( + 'password, SSN, credit card', + ); + const [result, setResult] = useState<{ + type: 'success' | 'blocked'; + message: string; + } | null>(null); const [isProcessing, setIsProcessing] = useState(false); const [processingTime, setProcessingTime] = useState(null); const isOverLimit = input.length > maxLength; - const patterns = blockedPatterns.split(',').map(p => p.trim().toLowerCase()).filter(Boolean); - const hasBlocked = patterns.some(p => input.toLowerCase().includes(p)); + const patterns = blockedPatterns + .split(',') + .map((p) => p.trim().toLowerCase()) + .filter(Boolean); + const hasBlocked = patterns.some((p) => input.toLowerCase().includes(p)); const handleRun = async () => { if (!input.trim() || isProcessing) return; - setIsProcessing(true); setResult(null); setProcessingTime(null); + setIsProcessing(true); + setResult(null); + setProcessingTime(null); const start = Date.now(); if (isOverLimit) { - setResult({type: 'blocked', message: `Blocked by InputLengthGuardrail: Text is ${input.length} characters (max: ${maxLength})`}); - setProcessingTime(Date.now() - start); setIsProcessing(false); return; + setResult({ + type: 'blocked', + message: `Blocked by InputLengthGuardrail: Text is ${input.length} characters (max: ${maxLength})`, + }); + setProcessingTime(Date.now() - start); + setIsProcessing(false); + return; } if (hasBlocked) { - const found = patterns.find(p => input.toLowerCase().includes(p)); - setResult({type: 'blocked', message: `Blocked by ContentFilterGuardrail: Contains prohibited pattern "${found}"`}); - setProcessingTime(Date.now() - start); setIsProcessing(false); return; + const found = patterns.find((p) => input.toLowerCase().includes(p)); + setResult({ + type: 'blocked', + message: `Blocked by ContentFilterGuardrail: Contains prohibited pattern "${found}"`, + }); + setProcessingTime(Date.now() - start); + setIsProcessing(false); + return; } try { - console.log('[DEBUG] guardrail summarize request:', JSON.stringify({inputLength: input.length})); + console.log( + '[DEBUG] guardrail summarize request:', + JSON.stringify({inputLength: input.length}), + ); const r = await summarize(input); console.log('[DEBUG] guardrail summarize response:', JSON.stringify(r)); setResult({type: 'success', message: r.summary}); setProcessingTime(Date.now() - start); - } catch (e: any) { setResult({type: 'blocked', message: `Error: ${e.message}`}); } - finally { setIsProcessing(false); } + } catch (e: any) { + setResult({type: 'blocked', message: `Error: ${e.message}`}); + } finally { + setIsProcessing(false); + } }; return ( - + Max Character Limit - setMaxLength(Math.max(50, maxLength - 50))}> + setMaxLength(Math.max(50, maxLength - 50))} + > + + {maxLength} - setMaxLength(Math.min(2000, maxLength + 50))}> + setMaxLength(Math.min(2000, maxLength + 50))} + > + + - Blocked Patterns (comma-separated) - - - {input.length} / {maxLength} - - {isProcessing ? : Run with Guardrails} + + Blocked Patterns (comma-separated) + + + + + {input.length} / {maxLength} + + + {isProcessing ? ( + + ) : ( + Run with Guardrails + )} {result && ( - - {processingTime !== null && } + + {processingTime !== null && ( + + + + + )} - - {result.type === 'success' ? 'Guardrails Passed' : 'Blocked by Guardrail'} + + + {result.type === 'success' + ? 'Guardrails Passed' + : 'Blocked by Guardrail'} + {result.message} @@ -71,19 +169,77 @@ export function GuardrailDemo() { } const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#F2F2F7'}, content: {padding: 16, paddingBottom: 40}, - sectionTitle: {fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, marginTop: 4}, - sliderRow: {flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 16, backgroundColor: 'white', borderRadius: 10, padding: 12, marginBottom: 12}, - sliderValue: {fontSize: 20, fontWeight: '700', color: '#333', minWidth: 50, textAlign: 'center'}, - patternInput: {backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, marginBottom: 12, color: '#000'}, - input: {backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, minHeight: 80, textAlignVertical: 'top', color: '#000'}, + container: {flex: 1, backgroundColor: '#F2F2F7'}, + content: {padding: 16, paddingBottom: 40}, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + marginTop: 4, + }, + sliderRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 16, + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + marginBottom: 12, + }, + sliderValue: { + fontSize: 20, + fontWeight: '700', + color: '#333', + minWidth: 50, + textAlign: 'center', + }, + patternInput: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + marginBottom: 12, + color: '#000', + }, + input: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + minHeight: 80, + textAlignVertical: 'top', + color: '#000', + }, inputError: {borderWidth: 1, borderColor: '#FF3B30'}, - charCount: {fontSize: 12, color: '#999', textAlign: 'right', marginTop: 4, marginBottom: 12}, charCountErr: {color: '#FF3B30'}, - runBtn: {backgroundColor: '#007AFF', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 16}, - runBtnDis: {opacity: 0.6}, runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, - resultCard: {backgroundColor: 'white', borderRadius: 10, padding: 16}, resultBlocked: {backgroundColor: '#FFF5F5'}, + charCount: { + fontSize: 12, + color: '#999', + textAlign: 'right', + marginTop: 4, + marginBottom: 12, + }, + charCountErr: {color: '#FF3B30'}, + runBtn: { + backgroundColor: '#007AFF', + paddingVertical: 14, + borderRadius: 10, + alignItems: 'center', + marginBottom: 16, + }, + runBtnDis: {opacity: 0.6}, + runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, + resultCard: {backgroundColor: 'white', borderRadius: 10, padding: 16}, + resultBlocked: {backgroundColor: '#FFF5F5'}, badgeRow: {flexDirection: 'row', gap: 8, marginBottom: 12}, - resultHeader: {flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8}, - resultTitle: {fontSize: 15, fontWeight: '600', color: '#34C759'}, resultTitleBlocked: {color: '#FF3B30'}, + resultHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + marginBottom: 8, + }, + resultTitle: {fontSize: 15, fontWeight: '600', color: '#34C759'}, + resultTitleBlocked: {color: '#FF3B30'}, resultText: {fontSize: 15, color: '#333', lineHeight: 22}, }); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/MemoryDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/MemoryDemo.tsx index 545c0ce..ea227de 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/MemoryDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/MemoryDemo.tsx @@ -1,5 +1,13 @@ import React, {useState} from 'react'; -import {View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator} from 'react-native'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + ActivityIndicator, +} from 'react-native'; import {chat} from 'expo-ondevice-ai'; import type {ChatMessage} from 'expo-ondevice-ai'; import {CodePatternCard} from './CodePatternCard'; @@ -17,50 +25,167 @@ export function MemoryDemo() { const handleSend = async () => { if (!input.trim() || isProcessing) return; setIsProcessing(true); - const userMsg = input.trim(); setInput(''); - const newHist: ChatMessage[] = [...history, {role: 'user' as const, content: userMsg}]; + const userMsg = input.trim(); + setInput(''); + const newHist: ChatMessage[] = [ + ...history, + {role: 'user' as const, content: userMsg}, + ]; const ctx = memoryType === 'buffer' ? newHist.slice(-maxEntries) : newHist; try { - console.log('[DEBUG] memory chat request:', JSON.stringify({message: userMsg, memoryType, historyLength: ctx.length})); - const r = await chat(userMsg, {systemPrompt: 'You are a helpful assistant. Keep responses short.', history: ctx}); + console.log( + '[DEBUG] memory chat request:', + JSON.stringify({ + message: userMsg, + memoryType, + historyLength: ctx.length, + }), + ); + const r = await chat(userMsg, { + systemPrompt: 'You are a helpful assistant. Keep responses short.', + history: ctx, + }); console.log('[DEBUG] memory chat response:', JSON.stringify(r)); - setHistory([...newHist, {role: 'assistant' as const, content: r.message}]); - } catch (e: any) { setHistory([...newHist, {role: 'assistant' as const, content: `Error: ${e.message}`}]); } - finally { setIsProcessing(false); } + setHistory([ + ...newHist, + {role: 'assistant' as const, content: r.message}, + ]); + } catch (e: any) { + setHistory([ + ...newHist, + {role: 'assistant' as const, content: `Error: ${e.message}`}, + ]); + } finally { + setIsProcessing(false); + } }; - const handleClear = () => { setHistory([]); setInput(''); }; - const display = memoryType === 'buffer' ? history.slice(-maxEntries) : history; - const tokens = history.reduce((s, m) => s + Math.ceil(m.content.length / 4), 0); + const handleClear = () => { + setHistory([]); + setInput(''); + }; + const display = + memoryType === 'buffer' ? history.slice(-maxEntries) : history; + const tokens = history.reduce( + (s, m) => s + Math.ceil(m.content.length / 4), + 0, + ); return ( - + Memory Type - { setMemoryType('buffer'); handleClear(); }}> - Buffer - Last {maxEntries} entries + { + setMemoryType('buffer'); + handleClear(); + }} + > + + Buffer + + + Last {maxEntries} entries + - { setMemoryType('summary'); handleClear(); }}> - Summary - Compressed history + { + setMemoryType('summary'); + handleClear(); + }} + > + + Summary + + + Compressed history + - + + + + + - Memory Entries{history.length > 0 && Clear} - {display.length === 0 ? No entries yet. Start a conversation. : display.map((e, i) => ( - - {e.role === 'user' ? 'U' : 'A'} - {e.content} - - ))} + + Memory Entries + {history.length > 0 && ( + + Clear + + )} + + {display.length === 0 ? ( + + No entries yet. Start a conversation. + + ) : ( + display.map((e, i) => ( + + + + {e.role === 'user' ? 'U' : 'A'} + + + + {e.content} + + + )) + )} - - - {isProcessing ? : Send} + + + {isProcessing ? ( + + ) : ( + Send + )} @@ -68,26 +193,84 @@ export function MemoryDemo() { } const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#F2F2F7'}, content: {padding: 16, paddingBottom: 40}, - sectionTitle: {fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, marginTop: 4}, + container: {flex: 1, backgroundColor: '#F2F2F7'}, + content: {padding: 16, paddingBottom: 40}, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + marginTop: 4, + }, row: {flexDirection: 'row', gap: 8, marginBottom: 12}, - typeBtn: {flex: 1, padding: 12, borderRadius: 10, backgroundColor: 'white', alignItems: 'center', borderWidth: 1, borderColor: '#E5E5EA'}, + typeBtn: { + flex: 1, + padding: 12, + borderRadius: 10, + backgroundColor: 'white', + alignItems: 'center', + borderWidth: 1, + borderColor: '#E5E5EA', + }, typeSel: {backgroundColor: '#007AFF', borderColor: '#007AFF'}, - typeText: {fontSize: 15, fontWeight: '600', color: '#333'}, typeTextSel: {color: 'white'}, - typeDesc: {fontSize: 12, color: '#999', marginTop: 2}, typeDescSel: {color: 'rgba(255,255,255,0.8)'}, + typeText: {fontSize: 15, fontWeight: '600', color: '#333'}, + typeTextSel: {color: 'white'}, + typeDesc: {fontSize: 12, color: '#999', marginTop: 2}, + typeDescSel: {color: 'rgba(255,255,255,0.8)'}, badgeRow: {flexDirection: 'row', gap: 8, marginBottom: 12, flexWrap: 'wrap'}, - card: {backgroundColor: 'white', borderRadius: 10, padding: 16, marginBottom: 12}, - cardHeader: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12}, + card: { + backgroundColor: 'white', + borderRadius: 10, + padding: 16, + marginBottom: 12, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, cardTitle: {fontSize: 15, fontWeight: '600', color: '#333'}, clearText: {fontSize: 14, color: '#FF3B30', fontWeight: '500'}, - emptyText: {fontSize: 14, color: '#999', textAlign: 'center', paddingVertical: 12}, - entryRow: {flexDirection: 'row', alignItems: 'flex-start', paddingVertical: 6, gap: 8}, - badge: {width: 24, height: 24, borderRadius: 12, justifyContent: 'center', alignItems: 'center'}, - badgeUser: {backgroundColor: '#007AFF'}, badgeAI: {backgroundColor: '#34C759'}, + emptyText: { + fontSize: 14, + color: '#999', + textAlign: 'center', + paddingVertical: 12, + }, + entryRow: { + flexDirection: 'row', + alignItems: 'flex-start', + paddingVertical: 6, + gap: 8, + }, + badge: { + width: 24, + height: 24, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + }, + badgeUser: {backgroundColor: '#007AFF'}, + badgeAI: {backgroundColor: '#34C759'}, badgeText: {fontSize: 12, fontWeight: '700', color: 'white'}, entryText: {flex: 1, fontSize: 14, color: '#333', lineHeight: 20}, inputRow: {flexDirection: 'row', gap: 8}, - input: {flex: 1, backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, color: '#000'}, - sendBtn: {backgroundColor: '#007AFF', paddingHorizontal: 20, borderRadius: 10, justifyContent: 'center', alignItems: 'center'}, - sendDis: {opacity: 0.6}, sendText: {color: 'white', fontSize: 15, fontWeight: '600'}, + input: { + flex: 1, + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + color: '#000', + }, + sendBtn: { + backgroundColor: '#007AFF', + paddingHorizontal: 20, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + sendDis: {opacity: 0.6}, + sendText: {color: 'white', fontSize: 15, fontWeight: '600'}, }); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ModelDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ModelDemo.tsx index 76571b8..3e0a647 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ModelDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/ModelDemo.tsx @@ -1,7 +1,12 @@ import React, {useState} from 'react'; import { - View, Text, TextInput, TouchableOpacity, ScrollView, - StyleSheet, ActivityIndicator, + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + ActivityIndicator, } from 'react-native'; import {chat, chatStream} from 'expo-ondevice-ai'; import {CodePatternCard} from './CodePatternCard'; @@ -9,13 +14,28 @@ import {StatBadge} from '../../shared/StatBadge'; type Preset = 'structured' | 'creative' | 'conversational'; const PRESETS: {key: Preset; label: string; prompt: string}[] = [ - {key: 'structured', label: 'Structured', prompt: 'You are a precise assistant. Answer concisely with structured data.'}, - {key: 'creative', label: 'Creative', prompt: 'You are a creative writer. Be imaginative and expressive.'}, - {key: 'conversational', label: 'Conversational', prompt: 'You are a friendly, casual assistant. Be warm and natural.'}, + { + key: 'structured', + label: 'Structured', + prompt: + 'You are a precise assistant. Answer concisely with structured data.', + }, + { + key: 'creative', + label: 'Creative', + prompt: 'You are a creative writer. Be imaginative and expressive.', + }, + { + key: 'conversational', + label: 'Conversational', + prompt: 'You are a friendly, casual assistant. Be warm and natural.', + }, ]; export function ModelDemo() { - const [input, setInput] = useState('Explain what on-device AI means in 2 sentences.'); + const [input, setInput] = useState( + 'Explain what on-device AI means in 2 sentences.', + ); const [selectedPreset, setSelectedPreset] = useState('structured'); const [useStreaming, setUseStreaming] = useState(false); const [output, setOutput] = useState(''); @@ -24,13 +44,25 @@ export function ModelDemo() { const handleRun = async () => { if (!input.trim() || isProcessing) return; - setIsProcessing(true); setOutput(''); setProcessingTime(null); + setIsProcessing(true); + setOutput(''); + setProcessingTime(null); const preset = PRESETS.find((p) => p.key === selectedPreset)!; const start = Date.now(); try { - console.log('[DEBUG] model request:', JSON.stringify({input: input.substring(0, 100), preset: preset.key, streaming: useStreaming})); + console.log( + '[DEBUG] model request:', + JSON.stringify({ + input: input.substring(0, 100), + preset: preset.key, + streaming: useStreaming, + }), + ); if (useStreaming) { - await chatStream(input, {systemPrompt: preset.prompt, onChunk: (c) => setOutput(c.accumulated)}); + await chatStream(input, { + systemPrompt: preset.prompt, + onChunk: (c) => setOutput(c.accumulated), + }); console.log('[DEBUG] model response (stream): completed'); } else { const result = await chat(input, {systemPrompt: preset.prompt}); @@ -38,34 +70,80 @@ export function ModelDemo() { setOutput(result.message); } setProcessingTime(Date.now() - start); - } catch (e: any) { setOutput(`Error: ${e.message}`); } - finally { setIsProcessing(false); } + } catch (e: any) { + setOutput(`Error: ${e.message}`); + } finally { + setIsProcessing(false); + } }; return ( - + Preset Configuration {PRESETS.map((p) => ( - setSelectedPreset(p.key)}> - {p.label} + setSelectedPreset(p.key)} + > + + {p.label} + ))} - setUseStreaming(!useStreaming)}> + setUseStreaming(!useStreaming)} + > Streaming - + - - - {isProcessing ? : Generate} + + + {isProcessing ? ( + + ) : ( + Generate + )} {(output || processingTime !== null) && ( - {processingTime !== null && } + {processingTime !== null && ( + + + + + + )} {output ? {output} : null} )} @@ -74,20 +152,73 @@ export function ModelDemo() { } const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#F2F2F7'}, content: {padding: 16, paddingBottom: 40}, - sectionTitle: {fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, marginTop: 4}, + container: {flex: 1, backgroundColor: '#F2F2F7'}, + content: {padding: 16, paddingBottom: 40}, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + marginTop: 4, + }, row: {flexDirection: 'row', gap: 8, marginBottom: 16}, - btn: {flex: 1, paddingVertical: 10, borderRadius: 8, backgroundColor: 'white', alignItems: 'center', borderWidth: 1, borderColor: '#E5E5EA'}, + btn: { + flex: 1, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: 'white', + alignItems: 'center', + borderWidth: 1, + borderColor: '#E5E5EA', + }, btnSel: {backgroundColor: '#007AFF', borderColor: '#007AFF'}, - btnText: {fontSize: 14, fontWeight: '600', color: '#333'}, btnTextSel: {color: 'white'}, - toggleRow: {flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: 'white', padding: 12, borderRadius: 10, marginBottom: 12}, + btnText: {fontSize: 14, fontWeight: '600', color: '#333'}, + btnTextSel: {color: 'white'}, + toggleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: 'white', + padding: 12, + borderRadius: 10, + marginBottom: 12, + }, toggleLabel: {fontSize: 15, color: '#333'}, - toggle: {width: 50, height: 30, borderRadius: 15, backgroundColor: '#E5E5EA', justifyContent: 'center', padding: 2}, + toggle: { + width: 50, + height: 30, + borderRadius: 15, + backgroundColor: '#E5E5EA', + justifyContent: 'center', + padding: 2, + }, toggleOn: {backgroundColor: '#34C759'}, - toggleThumb: {width: 26, height: 26, borderRadius: 13, backgroundColor: 'white'}, toggleThumbOn: {alignSelf: 'flex-end'}, - input: {backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, minHeight: 80, textAlignVertical: 'top', marginBottom: 12, color: '#000'}, - runBtn: {backgroundColor: '#007AFF', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 16}, - runBtnDisabled: {opacity: 0.6}, runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, + toggleThumb: { + width: 26, + height: 26, + borderRadius: 13, + backgroundColor: 'white', + }, + toggleThumbOn: {alignSelf: 'flex-end'}, + input: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + minHeight: 80, + textAlignVertical: 'top', + marginBottom: 12, + color: '#000', + }, + runBtn: { + backgroundColor: '#007AFF', + paddingVertical: 14, + borderRadius: 10, + alignItems: 'center', + marginBottom: 16, + }, + runBtnDisabled: {opacity: 0.6}, + runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, resultCard: {backgroundColor: 'white', borderRadius: 10, padding: 16}, badgeRow: {flexDirection: 'row', gap: 8, marginBottom: 12, flexWrap: 'wrap'}, resultText: {fontSize: 15, color: '#333', lineHeight: 22}, diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/PipelineDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/PipelineDemo.tsx index 19f44f9..fac41a6 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/PipelineDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/PipelineDemo.tsx @@ -1,13 +1,28 @@ import React, {useState} from 'react'; -import {View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator} from 'react-native'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + ActivityIndicator, +} from 'react-native'; import {proofread, translate} from 'expo-ondevice-ai'; import {CodePatternCard} from './CodePatternCard'; import {StatBadge} from '../../shared/StatBadge'; -const LANGUAGES = [{code: 'ko', label: 'Korean'}, {code: 'ja', label: 'Japanese'}, {code: 'es', label: 'Spanish'}, {code: 'fr', label: 'French'}]; +const LANGUAGES = [ + {code: 'ko', label: 'Korean'}, + {code: 'ja', label: 'Japanese'}, + {code: 'es', label: 'Spanish'}, + {code: 'fr', label: 'French'}, +]; export function PipelineDemo() { - const [input, setInput] = useState('Ths is a mesage with typos and grammer erors.'); + const [input, setInput] = useState( + 'Ths is a mesage with typos and grammer erors.', + ); const [targetLang, setTargetLang] = useState('ko'); const [steps, setSteps] = useState<{label: string; result: string}[]>([]); const [isProcessing, setIsProcessing] = useState(false); @@ -15,41 +30,98 @@ export function PipelineDemo() { const handleRun = async () => { if (!input.trim() || isProcessing) return; - setIsProcessing(true); setSteps([]); setProcessingTime(null); + setIsProcessing(true); + setSteps([]); + setProcessingTime(null); const start = Date.now(); try { - console.log('[DEBUG] pipeline step1 (proofread) request:', JSON.stringify({inputLength: input.length})); + console.log( + '[DEBUG] pipeline step1 (proofread) request:', + JSON.stringify({inputLength: input.length}), + ); const p = await proofread(input); - console.log('[DEBUG] pipeline step1 (proofread) response:', JSON.stringify(p)); - setSteps(prev => [...prev, {label: 'Step 1: Proofread', result: p.correctedText}]); - const langLabel = LANGUAGES.find(l => l.code === targetLang)?.label; - console.log('[DEBUG] pipeline step2 (translate) request:', JSON.stringify({targetLang})); + console.log( + '[DEBUG] pipeline step1 (proofread) response:', + JSON.stringify(p), + ); + setSteps((prev) => [ + ...prev, + {label: 'Step 1: Proofread', result: p.correctedText}, + ]); + const langLabel = LANGUAGES.find((l) => l.code === targetLang)?.label; + console.log( + '[DEBUG] pipeline step2 (translate) request:', + JSON.stringify({targetLang}), + ); const t = await translate(p.correctedText, {targetLanguage: targetLang}); - console.log('[DEBUG] pipeline step2 (translate) response:', JSON.stringify(t)); - setSteps(prev => [...prev, {label: `Step 2: Translate → ${langLabel}`, result: t.translatedText}]); + console.log( + '[DEBUG] pipeline step2 (translate) response:', + JSON.stringify(t), + ); + setSteps((prev) => [ + ...prev, + {label: `Step 2: Translate → ${langLabel}`, result: t.translatedText}, + ]); setProcessingTime(Date.now() - start); - } catch (e: any) { setSteps(prev => [...prev, {label: 'Error', result: e.message}]); } - finally { setIsProcessing(false); } + } catch (e: any) { + setSteps((prev) => [...prev, {label: 'Error', result: e.message}]); + } finally { + setIsProcessing(false); + } }; return ( - + Target Language - {LANGUAGES.map(l => ( - setTargetLang(l.code)}> - {l.label} + {LANGUAGES.map((l) => ( + setTargetLang(l.code)} + > + + {l.label} + ))} - - - {isProcessing ? : Run Pipeline} + + + {isProcessing ? ( + + ) : ( + Run Pipeline + )} {steps.length > 0 && ( - {processingTime !== null && } + {processingTime !== null && ( + + + + + )} {steps.map((s, i) => ( 0 && styles.stepSep]}> {s.label} @@ -63,19 +135,61 @@ export function PipelineDemo() { } const styles = StyleSheet.create({ - container: {flex: 1, backgroundColor: '#F2F2F7'}, content: {padding: 16, paddingBottom: 40}, - sectionTitle: {fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, marginTop: 4}, + container: {flex: 1, backgroundColor: '#F2F2F7'}, + content: {padding: 16, paddingBottom: 40}, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#666', + marginBottom: 8, + marginTop: 4, + }, row: {flexDirection: 'row', gap: 8, marginBottom: 16}, - btn: {flex: 1, paddingVertical: 10, borderRadius: 8, backgroundColor: 'white', alignItems: 'center', borderWidth: 1, borderColor: '#E5E5EA'}, + btn: { + flex: 1, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: 'white', + alignItems: 'center', + borderWidth: 1, + borderColor: '#E5E5EA', + }, btnSel: {backgroundColor: '#007AFF', borderColor: '#007AFF'}, - btnText: {fontSize: 13, fontWeight: '600', color: '#333'}, btnTextSel: {color: 'white'}, - input: {backgroundColor: 'white', borderRadius: 10, padding: 12, fontSize: 15, minHeight: 80, textAlignVertical: 'top', marginBottom: 12, color: '#000'}, - runBtn: {backgroundColor: '#007AFF', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 16}, - runBtnDis: {opacity: 0.6}, runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, + btnText: {fontSize: 13, fontWeight: '600', color: '#333'}, + btnTextSel: {color: 'white'}, + input: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + fontSize: 15, + minHeight: 80, + textAlignVertical: 'top', + marginBottom: 12, + color: '#000', + }, + runBtn: { + backgroundColor: '#007AFF', + paddingVertical: 14, + borderRadius: 10, + alignItems: 'center', + marginBottom: 16, + }, + runBtnDis: {opacity: 0.6}, + runBtnText: {color: 'white', fontSize: 17, fontWeight: '600'}, resultCard: {backgroundColor: 'white', borderRadius: 10, padding: 16}, badgeRow: {flexDirection: 'row', gap: 8, marginBottom: 12}, stepBlock: {paddingVertical: 4}, - stepSep: {borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#E5E5EA', marginTop: 8, paddingTop: 12}, - stepLabel: {fontSize: 13, fontWeight: '600', color: '#007AFF', marginBottom: 4}, + stepSep: { + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#E5E5EA', + marginTop: 8, + paddingTop: 12, + }, + stepLabel: { + fontSize: 13, + fontWeight: '600', + color: '#007AFF', + marginBottom: 4, + }, stepResult: {fontSize: 15, color: '#333', lineHeight: 22}, }); diff --git a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/SessionDemo.tsx b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/SessionDemo.tsx index 9f58402..6cadd37 100644 --- a/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/SessionDemo.tsx +++ b/libraries/expo-ondevice-ai/example/components/pages/FrameworkDetail/SessionDemo.tsx @@ -1,12 +1,23 @@ import React, {useState, useRef} from 'react'; -import {View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator} from 'react-native'; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + StyleSheet, + ActivityIndicator, +} from 'react-native'; import {chatStream} from 'expo-ondevice-ai'; import type {ChatMessage} from 'expo-ondevice-ai'; import {Ionicons} from '@expo/vector-icons'; import {CodePatternCard} from './CodePatternCard'; import {StatBadge} from '../../shared/StatBadge'; -interface DisplayMessage { role: 'user' | 'assistant'; content: string; } +interface DisplayMessage { + role: 'user' | 'assistant'; + content: string; +} export function SessionDemo() { const [input, setInput] = useState(''); @@ -15,62 +26,154 @@ export function SessionDemo() { const [showMemory, setShowMemory] = useState(false); const scrollRef = useRef(null); const maxMem = 6; - const history: ChatMessage[] = messages.map(m => ({role: m.role, content: m.content})); + const history: ChatMessage[] = messages.map((m) => ({ + role: m.role, + content: m.content, + })); const ctx = history.slice(-maxMem); const tokens = ctx.reduce((s, m) => s + Math.ceil(m.content.length / 4), 0); const handleSend = async () => { if (!input.trim() || isProcessing) return; - const msg = input.trim(); setInput(''); setIsProcessing(true); + const msg = input.trim(); + setInput(''); + setIsProcessing(true); // Build history synchronously before state updates to avoid stale closure const currentHistory: ChatMessage[] = [ - ...messages.map(m => ({role: m.role, content: m.content})), + ...messages.map((m) => ({role: m.role, content: m.content})), {role: 'user' as const, content: msg}, ].slice(-maxMem); - setMessages(prev => [...prev, {role: 'user', content: msg}]); + setMessages((prev) => [...prev, {role: 'user', content: msg}]); try { let acc = ''; - setMessages(prev => [...prev, {role: 'assistant', content: '...'}]); - console.log('[DEBUG] session chatStream request:', JSON.stringify({message: msg, historyLength: currentHistory.length})); + setMessages((prev) => [...prev, {role: 'assistant', content: '...'}]); + console.log( + '[DEBUG] session chatStream request:', + JSON.stringify({message: msg, historyLength: currentHistory.length}), + ); await chatStream(msg, { systemPrompt: 'You are a helpful assistant. Keep responses concise.', history: currentHistory, - onChunk: (c) => { acc = c.accumulated; setMessages(prev => { const u = [...prev]; u[u.length - 1] = {role: 'assistant', content: acc}; return u; }); }, + onChunk: (c) => { + acc = c.accumulated; + setMessages((prev) => { + const u = [...prev]; + u[u.length - 1] = {role: 'assistant', content: acc}; + return u; + }); + }, + }); + } catch (e: any) { + setMessages((prev) => { + const u = [...prev]; + u[u.length - 1] = {role: 'assistant', content: `Error: ${e.message}`}; + return u; }); - } catch (e: any) { setMessages(prev => { const u = [...prev]; u[u.length - 1] = {role: 'assistant', content: `Error: ${e.message}`}; return u; }); } - finally { setIsProcessing(false); setTimeout(() => scrollRef.current?.scrollToEnd({animated: true}), 100); } + } finally { + setIsProcessing(false); + setTimeout(() => scrollRef.current?.scrollToEnd({animated: true}), 100); + } }; return ( - scrollRef.current?.scrollToEnd({animated: true})}> - - setShowMemory(!showMemory)}> + + scrollRef.current?.scrollToEnd({animated: true}) + } + > + + setShowMemory(!showMemory)} + > Memory Inspector - - + + + + + {showMemory && ctx.length > 0 && ( {ctx.map((e, i) => ( - {e.role === 'user' ? 'U' : 'A'} - {e.content} + + + {e.role === 'user' ? 'U' : 'A'} + + + + {e.content} + ))} )} {messages.map((m, i) => ( - - {m.content} + + + {m.content} + ))} - {messages.length > 0 && { setMessages([]); setInput(''); }} style={styles.resetBtn}>} - - + {messages.length > 0 && ( + { + setMessages([]); + setInput(''); + }} + style={styles.resetBtn} + > + + + )} + + @@ -80,23 +183,91 @@ export function SessionDemo() { const styles = StyleSheet.create({ container: {flex: 1, backgroundColor: '#F2F2F7'}, - scroll: {flex: 1}, scrollContent: {padding: 16, paddingBottom: 8}, - memToggle: {flexDirection: 'row', alignItems: 'center', gap: 8, backgroundColor: 'white', padding: 12, borderRadius: 10, marginBottom: 8}, - memToggleText: {fontSize: 14, fontWeight: '600', color: '#333', marginRight: 'auto'}, + scroll: {flex: 1}, + scrollContent: {padding: 16, paddingBottom: 8}, + memToggle: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: 'white', + padding: 12, + borderRadius: 10, + marginBottom: 8, + }, + memToggleText: { + fontSize: 14, + fontWeight: '600', + color: '#333', + marginRight: 'auto', + }, badgeRow: {flexDirection: 'row', gap: 4}, - memEntries: {backgroundColor: 'white', borderRadius: 10, padding: 12, marginBottom: 12}, - memEntry: {flexDirection: 'row', alignItems: 'flex-start', gap: 8, paddingVertical: 4}, - memBadge: {width: 22, height: 22, borderRadius: 11, justifyContent: 'center', alignItems: 'center'}, - memBadgeU: {backgroundColor: '#007AFF'}, memBadgeA: {backgroundColor: '#34C759'}, + memEntries: { + backgroundColor: 'white', + borderRadius: 10, + padding: 12, + marginBottom: 12, + }, + memEntry: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: 8, + paddingVertical: 4, + }, + memBadge: { + width: 22, + height: 22, + borderRadius: 11, + justifyContent: 'center', + alignItems: 'center', + }, + memBadgeU: {backgroundColor: '#007AFF'}, + memBadgeA: {backgroundColor: '#34C759'}, memBadgeText: {fontSize: 11, fontWeight: '700', color: 'white'}, memText: {flex: 1, fontSize: 13, color: '#666', lineHeight: 18}, - bubble: {maxWidth: '80%', paddingHorizontal: 14, paddingVertical: 10, borderRadius: 18, marginBottom: 8}, - bubbleUser: {alignSelf: 'flex-end', backgroundColor: '#007AFF', borderBottomRightRadius: 4}, - bubbleAI: {alignSelf: 'flex-start', backgroundColor: 'white', borderBottomLeftRadius: 4}, - bubbleText: {fontSize: 15, color: '#333', lineHeight: 22}, bubbleTextUser: {color: 'white'}, - inputBar: {flexDirection: 'row', padding: 12, gap: 8, backgroundColor: 'white', borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#E5E5EA'}, + bubble: { + maxWidth: '80%', + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 18, + marginBottom: 8, + }, + bubbleUser: { + alignSelf: 'flex-end', + backgroundColor: '#007AFF', + borderBottomRightRadius: 4, + }, + bubbleAI: { + alignSelf: 'flex-start', + backgroundColor: 'white', + borderBottomLeftRadius: 4, + }, + bubbleText: {fontSize: 15, color: '#333', lineHeight: 22}, + bubbleTextUser: {color: 'white'}, + inputBar: { + flexDirection: 'row', + padding: 12, + gap: 8, + backgroundColor: 'white', + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#E5E5EA', + }, resetBtn: {justifyContent: 'center', paddingHorizontal: 4}, - input: {flex: 1, backgroundColor: '#F2F2F7', borderRadius: 20, paddingHorizontal: 16, paddingVertical: 10, fontSize: 15, color: '#000'}, - sendBtn: {width: 40, height: 40, borderRadius: 20, backgroundColor: '#007AFF', justifyContent: 'center', alignItems: 'center'}, + input: { + flex: 1, + backgroundColor: '#F2F2F7', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 10, + fontSize: 15, + color: '#000', + }, + sendBtn: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#007AFF', + justifyContent: 'center', + alignItems: 'center', + }, sendDis: {opacity: 0.4}, }); diff --git a/libraries/expo-ondevice-ai/example/components/shared/AIStatusBanner.tsx b/libraries/expo-ondevice-ai/example/components/shared/AIStatusBanner.tsx index 463737c..cc1b74d 100644 --- a/libraries/expo-ondevice-ai/example/components/shared/AIStatusBanner.tsx +++ b/libraries/expo-ondevice-ai/example/components/shared/AIStatusBanner.tsx @@ -50,7 +50,13 @@ export function AIStatusBanner() { - Checking Apple Intelligence... + + {Platform.OS === 'web' + ? 'Checking Chrome Built-in AI...' + : Platform.OS === 'ios' + ? 'Checking Apple Intelligence...' + : 'Checking Gemini Nano...'} + Please wait while checking device capabilities @@ -63,7 +69,11 @@ export function AIStatusBanner() { if (isModelReady) { const engineLabel = ENGINE_LABELS[modelState.currentEngine] ?? - (Platform.OS === 'ios' ? 'Apple Intelligence' : 'Gemini Nano'); + (Platform.OS === 'web' + ? 'Chrome Built-in AI' + : Platform.OS === 'ios' + ? 'Apple Intelligence' + : 'Gemini Nano'); return ( <> @@ -87,6 +97,22 @@ export function AIStatusBanner() { ); } + // Web: Chrome Built-in AI not available + if (Platform.OS === 'web') { + return ( + + + + Chrome Built-in AI Not Available + + Requires Chrome 138+ with Gemini Nano enabled. Check + chrome://flags/#optimization-guide-on-device-model + + + + ); + } + // Device supports Apple Intelligence but model not ready if (capability?.supportsAppleIntelligence) { return ( @@ -105,15 +131,16 @@ export function AIStatusBanner() { ); } - // Device does not support Apple Intelligence + // Device does not support on-device AI return ( Device Not Supported - This device does not support Apple Intelligence. Requires iPhone 15 - Pro or newer with iOS 18.1+ + {Platform.OS === 'ios' + ? 'This device does not support Apple Intelligence. Requires iPhone 15 Pro or newer with iOS 18.1+' + : 'This device does not support Gemini Nano. Requires Android 14+'} diff --git a/libraries/expo-ondevice-ai/example/components/shared/DebugLogPanel.tsx b/libraries/expo-ondevice-ai/example/components/shared/DebugLogPanel.tsx index 55d213f..a27c2c3 100644 --- a/libraries/expo-ondevice-ai/example/components/shared/DebugLogPanel.tsx +++ b/libraries/expo-ondevice-ai/example/components/shared/DebugLogPanel.tsx @@ -1,5 +1,11 @@ import React, {useState} from 'react'; -import {View, Text, TouchableOpacity, StyleSheet, ScrollView} from 'react-native'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, +} from 'react-native'; import {Ionicons} from '@expo/vector-icons'; export interface DebugLog { @@ -15,11 +21,19 @@ export function DebugLogPanel({log}: {log: DebugLog | null}) { return ( - setExpanded(!expanded)} activeOpacity={0.7}> + setExpanded(!expanded)} + activeOpacity={0.7} + > Debug Log {log.timing}ms - + {expanded && ( @@ -27,11 +41,15 @@ export function DebugLogPanel({log}: {log: DebugLog | null}) { {log.api} Request - {JSON.stringify(log.request, null, 2)} + + {JSON.stringify(log.request, null, 2)} + Response - {JSON.stringify(log.response, null, 2)} + + {JSON.stringify(log.response, null, 2)} + )} @@ -40,13 +58,36 @@ export function DebugLogPanel({log}: {log: DebugLog | null}) { } const styles = StyleSheet.create({ - container: {backgroundColor: '#1C1C1E', borderRadius: 10, marginTop: 12, overflow: 'hidden'}, + container: { + backgroundColor: '#1C1C1E', + borderRadius: 10, + marginTop: 12, + overflow: 'hidden', + }, header: {flexDirection: 'row', alignItems: 'center', padding: 12, gap: 8}, title: {flex: 1, fontSize: 14, fontWeight: '600', color: '#8E8E93'}, timing: {fontSize: 13, fontWeight: '500', color: '#30D158', marginRight: 4}, body: {paddingHorizontal: 12, paddingBottom: 12}, - label: {fontSize: 11, fontWeight: '700', color: '#636366', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 8, marginBottom: 4}, - api: {fontSize: 14, fontWeight: '600', color: '#0A84FF', fontFamily: 'monospace'}, + label: { + fontSize: 11, + fontWeight: '700', + color: '#636366', + textTransform: 'uppercase', + letterSpacing: 0.5, + marginTop: 8, + marginBottom: 4, + }, + api: { + fontSize: 14, + fontWeight: '600', + color: '#0A84FF', + fontFamily: 'monospace', + }, codeScroll: {maxHeight: 200}, - code: {fontSize: 12, color: '#E5E5EA', fontFamily: 'monospace', lineHeight: 18}, + code: { + fontSize: 12, + color: '#E5E5EA', + fontFamily: 'monospace', + lineHeight: 18, + }, }); diff --git a/libraries/expo-ondevice-ai/expo-module.config.json b/libraries/expo-ondevice-ai/expo-module.config.json index 25adb8d..77c6fc9 100644 --- a/libraries/expo-ondevice-ai/expo-module.config.json +++ b/libraries/expo-ondevice-ai/expo-module.config.json @@ -1,16 +1,9 @@ { - "platforms": [ - "ios", - "android" - ], + "platforms": ["ios", "android", "web"], "ios": { - "modules": [ - "ExpoOndeviceAiModule" - ] + "modules": ["ExpoOndeviceAiModule"] }, "android": { - "modules": [ - "expo.modules.ondeviceai.ExpoOndeviceAiModule" - ] + "modules": ["expo.modules.ondeviceai.ExpoOndeviceAiModule"] } } diff --git a/libraries/expo-ondevice-ai/ios/ExpoOndeviceAi.podspec b/libraries/expo-ondevice-ai/ios/ExpoOndeviceAi.podspec index 7156b7a..e8fbc0e 100644 --- a/libraries/expo-ondevice-ai/ios/ExpoOndeviceAi.podspec +++ b/libraries/expo-ondevice-ai/ios/ExpoOndeviceAi.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.license = package['license'] s.author = package['author'] s.homepage = package['homepage'] - s.platforms = { :ios => '15.0' } + s.platforms = { :ios => '17.0' } s.swift_version = '5.9' s.source = { git: 'https://github.com/hyodotdev/locanara' } s.static_framework = true diff --git a/libraries/expo-ondevice-ai/plugin/src/withOndeviceAi.ts b/libraries/expo-ondevice-ai/plugin/src/withOndeviceAi.ts index 69ac7fe..f140d4b 100644 --- a/libraries/expo-ondevice-ai/plugin/src/withOndeviceAi.ts +++ b/libraries/expo-ondevice-ai/plugin/src/withOndeviceAi.ts @@ -122,7 +122,9 @@ function addSPMPackageToMainProject(project: any): void { function addEmbedLlamaFrameworkPhase(project: any): void { const appTarget = project.getFirstTarget(); if (!appTarget?.firstTarget) { - console.warn('[expo-ondevice-ai] Could not find app target for embed phase'); + console.warn( + '[expo-ondevice-ai] Could not find app target for embed phase', + ); return; } @@ -572,9 +574,7 @@ const withOndeviceAi: ConfigPlugin = ( // Android: Build local SDK AAR and install to mavenLocal. if (androidPath) { const resolvedAndroidPath = path.resolve(androidPath); - logOnce( - `[expo-ondevice-ai] Local Android SDK: ${resolvedAndroidPath}`, - ); + logOnce(`[expo-ondevice-ai] Local Android SDK: ${resolvedAndroidPath}`); config = withDangerousMod(config, [ 'android', diff --git a/libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts b/libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts new file mode 100644 index 0000000..8a73809 --- /dev/null +++ b/libraries/expo-ondevice-ai/src/ExpoOndeviceAiModule.web.ts @@ -0,0 +1,739 @@ +/** + * Web implementation of ExpoOndeviceAi module + * Uses Chrome Built-in AI (Gemini Nano) APIs + */ + +import type { + DeviceCapability, + SummarizeOptions, + SummarizeResult, + ClassifyOptions, + ClassifyResult, + ExtractOptions, + ExtractResult, + ChatOptions, + ChatResult, + TranslateOptions, + TranslateResult, + RewriteOptions, + RewriteResult, + ProofreadOptions, + ProofreadResult, + InitializeResult, + DownloadableModelInfo, + InferenceEngine, +} from './types'; + +// ============================================================================ +// Chrome Built-in AI Type Definitions +// ============================================================================ + +interface ChromeSummarizerConstructor { + availability(): Promise; + create(options?: { + type?: 'key-points' | 'tldr' | 'teaser' | 'headline'; + length?: 'short' | 'medium' | 'long'; + format?: 'markdown' | 'plain-text'; + }): Promise; +} + +interface ChromeSummarizer { + summarize(text: string): Promise; + destroy(): void; +} + +interface ChromeTranslatorConstructor { + create(options: { + sourceLanguage: string; + targetLanguage: string; + }): Promise; +} + +interface ChromeTranslator { + translate(text: string): Promise; + destroy(): void; +} + +interface ChromeRewriterConstructor { + availability(): Promise; + create(options?: { + tone?: 'more-formal' | 'as-is' | 'more-casual'; + length?: 'shorter' | 'as-is' | 'longer'; + }): Promise; +} + +interface ChromeRewriter { + rewrite(text: string): Promise; + destroy(): void; +} + +interface ChromeWriterConstructor { + availability(): Promise; + create(options?: Record): Promise; +} + +interface ChromeWriter { + write(prompt: string): Promise; + destroy(): void; +} + +interface ChromeLanguageModelConstructor { + availability(): Promise; + create(options?: { + initialPrompts?: Array<{role: string; content: string}>; + }): Promise; +} + +interface ChromeLanguageModelSession { + prompt(message: string): Promise; + promptStreaming(message: string): AsyncIterable; + destroy(): void; +} + +// ============================================================================ +// Chrome AI API Accessors +// ============================================================================ + +function getSummarizerAPI(): ChromeSummarizerConstructor | undefined { + const s = (globalThis as Record).Summarizer; + if (s && (typeof s === 'object' || typeof s === 'function')) + return s as unknown as ChromeSummarizerConstructor; + return undefined; +} + +function getTranslatorAPI(): ChromeTranslatorConstructor | undefined { + const t = (globalThis as Record).Translator; + if (t && (typeof t === 'object' || typeof t === 'function')) + return t as unknown as ChromeTranslatorConstructor; + return undefined; +} + +function getRewriterAPI(): ChromeRewriterConstructor | undefined { + const r = (globalThis as Record).Rewriter; + if (r && (typeof r === 'object' || typeof r === 'function')) + return r as unknown as ChromeRewriterConstructor; + return undefined; +} + +function getWriterAPI(): ChromeWriterConstructor | undefined { + const w = (globalThis as Record).Writer; + if (w && (typeof w === 'object' || typeof w === 'function')) + return w as unknown as ChromeWriterConstructor; + return undefined; +} + +function getLanguageModelAPI(): ChromeLanguageModelConstructor | undefined { + // Try globalThis.LanguageModel first (newer API) + const lm = (globalThis as Record).LanguageModel; + if (lm && (typeof lm === 'object' || typeof lm === 'function')) + return lm as unknown as ChromeLanguageModelConstructor; + // Try globalThis.ai.languageModel (older API) + const ai = (globalThis as Record).ai as + | Record + | undefined; + if (ai && typeof ai === 'object' && ai.languageModel) + return ai.languageModel as unknown as ChromeLanguageModelConstructor; + return undefined; +} + +// ============================================================================ +// Cached Chrome AI Instances +// ============================================================================ + +const MAX_CACHED_TRANSLATORS = 10; + +let cachedSummarizer: ChromeSummarizer | null = null; +let cachedSummarizerKey = ''; +let cachedLanguageModel: ChromeLanguageModelSession | null = null; +let cachedSystemPrompt: string | undefined = undefined; +const cachedTranslators = new Map(); +let cachedRewriter: ChromeRewriter | null = null; +let cachedWriter: ChromeWriter | null = null; + +// Simple event emitter for web (mimics Expo native module EventEmitter) +const eventListeners = new Map void>>(); + +interface ChatStreamChunkData { + delta: string; + accumulated: string; + isFinal: boolean; +} + +function emitEvent(eventName: string, data: ChatStreamChunkData) { + const listeners = eventListeners.get(eventName); + if (listeners) { + for (const listener of listeners) { + listener(data); + } + } +} + +// ============================================================================ +// Availability Helpers +// ============================================================================ + +function hasAPI(api: string): boolean { + const obj = (globalThis as Record)[api]; + return !!obj && (typeof obj === 'object' || typeof obj === 'function'); +} + +async function checkAvailability(api: string): Promise { + try { + const obj = (globalThis as Record)[api] as + | {availability?: () => Promise} + | undefined; + if (!obj) return false; + if (typeof obj.availability === 'function') { + const status = await Promise.race([ + obj.availability(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('timeout')), 3000), + ), + ]); + return ( + status === 'available' || + status === 'readily' || + status === 'downloadable' || + status === 'after-download' + ); + } + return typeof obj === 'object' || typeof obj === 'function'; + } catch { + return hasAPI(api); + } +} + +// ============================================================================ +// Module Implementation +// ============================================================================ + +const ExpoOndeviceAiModule = { + async initialize(): Promise { + return {success: true}; + }, + + async getDeviceCapability(): Promise { + const [hasSummarizer, hasRewriter, hasWriter] = await Promise.all([ + checkAvailability('Summarizer'), + checkAvailability('Rewriter'), + checkAvailability('Writer'), + ]); + const hasTranslator = hasAPI('Translator'); + + const lm = getLanguageModelAPI(); + let hasLanguageModel = !!lm; + if (lm && typeof lm.availability === 'function') { + try { + const s = await Promise.race([ + lm.availability(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('timeout')), 3000), + ), + ]); + hasLanguageModel = + s === 'readily' || + s === 'available' || + s === 'downloadable' || + s === 'after-download'; + } catch { + hasLanguageModel = !!lm; + } + } + + return { + isSupported: hasSummarizer || hasLanguageModel || hasTranslator, + isModelReady: hasSummarizer || hasLanguageModel, + platform: 'WEB' as const, + features: { + summarize: hasSummarizer, + classify: hasLanguageModel, + extract: hasLanguageModel, + chat: hasLanguageModel, + translate: hasTranslator, + rewrite: hasRewriter, + proofread: hasLanguageModel || hasWriter, + }, + }; + }, + + async summarize( + text: string, + options?: SummarizeOptions, + ): Promise { + const Summarizer = getSummarizerAPI(); + if (!Summarizer) + throw new Error('Summarizer API not available in this browser'); + + const optionsKey = 'key-points:long'; + if (!cachedSummarizer || cachedSummarizerKey !== optionsKey) { + cachedSummarizer?.destroy(); + cachedSummarizer = await Summarizer.create({ + type: 'key-points', + length: 'long', + format: 'markdown', + }); + cachedSummarizerKey = optionsKey; + } + + const raw = await cachedSummarizer.summarize(text); + + const bulletCount = + options?.outputType === 'ONE_BULLET' + ? 1 + : options?.outputType === 'TWO_BULLETS' + ? 2 + : 3; + const bullets = raw + .split('\n') + .map((l: string) => l.trim()) + .filter((l: string) => l.startsWith('*') || l.startsWith('-')); + const summary = + bullets.length > 0 ? bullets.slice(0, bulletCount).join('\n') : raw; + + return { + summary, + originalLength: text.length, + summaryLength: summary.length, + }; + }, + + async classify( + text: string, + options?: ClassifyOptions, + ): Promise { + const lm = getLanguageModelAPI(); + if (!lm) throw new Error('LanguageModel API not available in this browser'); + + const categories = options?.categories ?? [ + 'positive', + 'negative', + 'neutral', + ]; + const session = await lm.create({}); + const prompt = `Classify the following text into one of these categories: ${categories.join(', ')}.\n\nText: ${text}\n\nRespond with ONLY the category name.`; + const response = await session.prompt(prompt); + session.destroy(); + + const category = response.trim(); + const isValid = categories.some( + (c: string) => c.toLowerCase() === category.toLowerCase(), + ); + + return { + classifications: [ + {label: isValid ? category : categories[0], score: isValid ? 0.9 : 0.5}, + ], + topClassification: { + label: isValid ? category : categories[0], + score: isValid ? 0.9 : 0.5, + }, + }; + }, + + async extract( + text: string, + _options?: ExtractOptions, + ): Promise { + const lm = getLanguageModelAPI(); + if (!lm) throw new Error('LanguageModel API not available in this browser'); + + const session = await lm.create({}); + const prompt = `Extract entities from this text. Return JSON with these exact keys: "person", "email", "phone", "date", "location", "organization". Each key maps to an array of strings. Only include keys that have values.\n\nText: ${text}\n\nRespond with valid JSON only, no markdown.`; + const response = await session.prompt(prompt); + session.destroy(); + + const typeNormalize: Record = { + person: 'person', + persons: 'person', + people: 'person', + name: 'person', + names: 'person', + email: 'email', + emails: 'email', + phone: 'phone', + phones: 'phone', + phone_number: 'phone', + phone_numbers: 'phone', + date: 'date', + dates: 'date', + location: 'location', + locations: 'location', + place: 'location', + places: 'location', + organization: 'organization', + organizations: 'organization', + org: 'organization', + orgs: 'organization', + contact: 'email', + }; + const confidenceMap: Record = { + person: 0.95, + email: 0.98, + phone: 0.97, + date: 0.96, + location: 0.92, + organization: 0.9, + }; + + try { + const jsonStr = response + .replace(/^```(?:json)?\s*\n?/m, '') + .replace(/\n?```\s*$/m, '') + .trim(); + const parsed = JSON.parse(jsonStr); + + const entities: {type: string; value: string; confidence: number}[] = []; + const walk = (obj: unknown, parentKey?: string) => { + if (Array.isArray(obj)) { + obj.forEach((item) => { + if (typeof item === 'string') { + const normalized = + typeNormalize[(parentKey ?? '').toLowerCase()] ?? + parentKey ?? + 'unknown'; + entities.push({ + type: normalized, + value: item, + confidence: confidenceMap[normalized] ?? 0.85, + }); + } else { + walk(item, parentKey); + } + }); + } else if (typeof obj === 'object' && obj !== null) { + Object.entries(obj as Record).forEach(([key, value]) => + walk(value, key), + ); + } else { + const normalized = + typeNormalize[(parentKey ?? '').toLowerCase()] ?? + parentKey ?? + 'unknown'; + entities.push({ + type: normalized, + value: String(obj), + confidence: confidenceMap[normalized] ?? 0.85, + }); + } + }; + walk(parsed); + return {entities}; + } catch { + return {entities: [{type: 'raw', value: response, confidence: 0.5}]}; + } + }, + + async chat(message: string, options?: ChatOptions): Promise { + const lm = getLanguageModelAPI(); + if (!lm) throw new Error('LanguageModel API not available in this browser'); + + const newSystemPrompt = options?.systemPrompt; + if (!cachedLanguageModel || newSystemPrompt !== cachedSystemPrompt) { + cachedLanguageModel?.destroy(); + const initialPrompts: {role: string; content: string}[] = []; + if (newSystemPrompt) { + initialPrompts.push({role: 'system', content: newSystemPrompt}); + } + cachedLanguageModel = await lm.create({ + initialPrompts: initialPrompts.length > 0 ? initialPrompts : undefined, + }); + cachedSystemPrompt = newSystemPrompt; + } + + const response = await cachedLanguageModel.prompt(message); + return { + message: response, + canContinue: true, + }; + }, + + addListener(eventName: string, listener: (data: ChatStreamChunkData) => void) { + if (!eventListeners.has(eventName)) + eventListeners.set(eventName, new Set()); + eventListeners.get(eventName)!.add(listener); + return {remove: () => eventListeners.get(eventName)?.delete(listener)}; + }, + + removeListeners(_count: number) { + // No-op, cleanup handled by subscription.remove() + }, + + async chatStream( + message: string, + options?: ChatOptions, + ): Promise { + const lm = getLanguageModelAPI(); + if (!lm) throw new Error('LanguageModel API not available in this browser'); + + const newSystemPrompt = options?.systemPrompt; + if (!cachedLanguageModel || newSystemPrompt !== cachedSystemPrompt) { + cachedLanguageModel?.destroy(); + const initialPrompts: {role: string; content: string}[] = []; + if (newSystemPrompt) { + initialPrompts.push({role: 'system', content: newSystemPrompt}); + } + cachedLanguageModel = await lm.create({ + initialPrompts: initialPrompts.length > 0 ? initialPrompts : undefined, + }); + cachedSystemPrompt = newSystemPrompt; + } + + if (typeof cachedLanguageModel.promptStreaming === 'function') { + const stream = cachedLanguageModel.promptStreaming(message); + let accumulated = ''; + + for await (const chunk of stream) { + const text = typeof chunk === 'string' ? chunk : String(chunk); + // Chrome may return cumulative or delta text depending on version + if (text.length >= accumulated.length && text.startsWith(accumulated)) { + const delta = text.slice(accumulated.length); + accumulated = text; + emitEvent('onChatStreamChunk', {delta, accumulated, isFinal: false}); + } else { + accumulated += text; + emitEvent('onChatStreamChunk', { + delta: text, + accumulated, + isFinal: false, + }); + } + } + + emitEvent('onChatStreamChunk', {delta: '', accumulated, isFinal: true}); + return {message: accumulated, canContinue: true}; + } + + // Fallback to non-streaming + const response = await cachedLanguageModel.prompt(message); + emitEvent('onChatStreamChunk', { + delta: response, + accumulated: response, + isFinal: true, + }); + return {message: response, canContinue: true}; + }, + + async translate( + text: string, + options: TranslateOptions, + ): Promise { + const Translator = getTranslatorAPI(); + if (!Translator) + throw new Error('Translator API not available in this browser'); + + const key = `${options.sourceLanguage ?? 'en'}-${options.targetLanguage}`; + if (!cachedTranslators.has(key)) { + // Evict oldest entry if cache is full + if (cachedTranslators.size >= MAX_CACHED_TRANSLATORS) { + const oldestKey = cachedTranslators.keys().next().value!; + cachedTranslators.get(oldestKey)?.destroy(); + cachedTranslators.delete(oldestKey); + } + const translator = await Translator.create({ + sourceLanguage: options.sourceLanguage ?? 'en', + targetLanguage: options.targetLanguage, + }); + cachedTranslators.set(key, translator); + } + + const translator = cachedTranslators.get(key)!; + const translatedText = await translator.translate(text); + + return { + translatedText, + sourceLanguage: options.sourceLanguage ?? 'en', + targetLanguage: options.targetLanguage, + }; + }, + + async rewrite(text: string, options: RewriteOptions): Promise { + const Rewriter = getRewriterAPI(); + if (!Rewriter) + throw new Error('Rewriter API not available in this browser'); + + const toneMap: Record = { + FRIENDLY: 'more-casual', + PROFESSIONAL: 'more-formal', + ELABORATE: 'as-is', + SHORTEN: 'as-is', + EMOJIFY: 'more-casual', + REPHRASE: 'as-is', + }; + const lengthMap: Record = { + ELABORATE: 'longer', + SHORTEN: 'shorter', + }; + + cachedRewriter?.destroy(); + cachedRewriter = await Rewriter.create({ + tone: toneMap[options.outputType] ?? 'as-is', + length: lengthMap[options.outputType] ?? 'as-is', + }); + + const rewrittenText = await cachedRewriter.rewrite(text); + return { + rewrittenText, + style: options.outputType, + }; + }, + + async proofread( + text: string, + _options?: ProofreadOptions, + ): Promise { + // Prefer LanguageModel for structured proofreading (returns corrections list) + const lm = getLanguageModelAPI(); + if (lm) { + const session = await lm.create({}); + const prompt = `You are a proofreader. Fix ONLY spelling, grammar, and punctuation errors. Do NOT change meaning, tense, or style. Return JSON with this exact format: +{"correctedText":"the full corrected text","corrections":[{"original":"misspeled","corrected":"misspelled","type":"spelling"}]} + +Type must be one of: "spelling", "grammar", "punctuation". +If there are no errors, return: {"correctedText":"","corrections":[]} +Respond with valid JSON only, no markdown, no explanation. + +Text to proofread: +${text}`; + const response = await session.prompt(prompt); + session.destroy(); + + try { + const jsonStr = response + .replace(/^```(?:json)?\s*\n?/m, '') + .replace(/\n?```\s*$/m, '') + .trim(); + const parsed = JSON.parse(jsonStr) as { + correctedText?: string; + corrections?: Array<{ + original?: string; + corrected?: string; + type?: string; + }>; + }; + const correctedText = parsed.correctedText ?? text; + const corrections = Array.isArray(parsed.corrections) + ? parsed.corrections.map((c) => ({ + original: c.original ?? '', + corrected: c.corrected ?? '', + type: c.type ?? 'grammar', + confidence: 0.9, + })) + : []; + return { + correctedText, + corrections, + hasCorrections: corrections.length > 0, + }; + } catch { + // JSON parse failed — fall through to Writer API + } + } + + // Fallback to Writer API with word-diff + const Writer = getWriterAPI(); + if (!Writer) + throw new Error( + 'Writer or LanguageModel API not available in this browser', + ); + + if (!cachedWriter) { + cachedWriter = await Writer.create({}); + } + + const correctedText = await cachedWriter.write( + `Proofread and correct this text. Fix ONLY spelling, grammar, and punctuation. Do NOT change meaning, tense, or word choice. Return only the corrected text:\n\n${text}`, + ); + + const corrections: { + original: string; + corrected: string; + type: string; + confidence: number; + }[] = []; + const origWords = text.split(/\s+/); + const corrWords = correctedText.split(/\s+/); + if (origWords.length === corrWords.length) { + for (let i = 0; i < origWords.length; i++) { + if (origWords[i] !== corrWords[i]) { + corrections.push({ + original: origWords[i], + corrected: corrWords[i], + type: 'spelling', + confidence: 0.85, + }); + } + } + } + + return { + correctedText, + corrections, + hasCorrections: correctedText !== text, + }; + }, + + // Model Management - Chrome manages models automatically + async getAvailableModels(): Promise { + return []; + }, + + async getDownloadedModels(): Promise { + return []; + }, + + async getLoadedModel(): Promise { + return null; + }, + + async getCurrentEngine(): Promise { + return 'none'; + }, + + async downloadModel(_modelId: string): Promise { + return false; + }, + + async loadModel(_modelId: string): Promise {}, + + async deleteModel(_modelId: string): Promise {}, + + async getPromptApiStatus(): Promise { + const lm = getLanguageModelAPI(); + if (!lm) return 'not_available'; + try { + return await lm.availability(); + } catch { + return 'not_available'; + } + }, + + async downloadPromptApiModel(): Promise { + return false; + }, + + /** Destroy all cached Chrome AI instances and free resources */ + destroy() { + cachedSummarizer?.destroy(); + cachedSummarizer = null; + cachedSummarizerKey = ''; + + cachedLanguageModel?.destroy(); + cachedLanguageModel = null; + cachedSystemPrompt = undefined; + + for (const translator of cachedTranslators.values()) { + translator.destroy(); + } + cachedTranslators.clear(); + + cachedRewriter?.destroy(); + cachedRewriter = null; + + cachedWriter?.destroy(); + cachedWriter = null; + + eventListeners.clear(); + }, +}; + +export default ExpoOndeviceAiModule; diff --git a/libraries/expo-ondevice-ai/src/index.ts b/libraries/expo-ondevice-ai/src/index.ts index ff92cb5..bd3d661 100644 --- a/libraries/expo-ondevice-ai/src/index.ts +++ b/libraries/expo-ondevice-ai/src/index.ts @@ -23,10 +23,10 @@ import type { ModelDownloadProgress, InferenceEngine, } from './types'; +import {ExpoOndeviceAiLog as Log} from './log'; export * from './types'; export {ExpoOndeviceAiLog} from './log'; -import {ExpoOndeviceAiLog as Log} from './log'; /** * Initialize the Locanara SDK diff --git a/libraries/expo-ondevice-ai/src/types.ts b/libraries/expo-ondevice-ai/src/types.ts index e2ad6be..400016f 100644 --- a/libraries/expo-ondevice-ai/src/types.ts +++ b/libraries/expo-ondevice-ai/src/types.ts @@ -18,7 +18,7 @@ export type RewriteOutputType = | 'PROFESSIONAL' | 'REPHRASE'; export type ProofreadInputType = 'KEYBOARD' | 'VOICE'; -export type Platform = 'IOS' | 'ANDROID'; +export type Platform = 'IOS' | 'ANDROID' | 'WEB'; /** * Device capability information for on-device AI diff --git a/libraries/react-native-ondevice-ai/NitroOndeviceAi.podspec b/libraries/react-native-ondevice-ai/NitroOndeviceAi.podspec index 458fe2b..26d7da2 100644 --- a/libraries/react-native-ondevice-ai/NitroOndeviceAi.podspec +++ b/libraries/react-native-ondevice-ai/NitroOndeviceAi.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => '15.0', :macos => '14.0' } + s.platforms = { :ios => '17.0', :macos => '14.0' } s.source = { :git => "https://github.com/hyodotdev/locanara.git", :tag => "#{s.version}" } s.source_files = [ diff --git a/libraries/react-native-ondevice-ai/example/ios/Podfile b/libraries/react-native-ondevice-ai/example/ios/Podfile index a6e3d0f..816d4a8 100644 --- a/libraries/react-native-ondevice-ai/example/ios/Podfile +++ b/libraries/react-native-ondevice-ai/example/ios/Podfile @@ -15,6 +15,9 @@ if linkage != nil end target 'OndeviceAiExample' do + # Use local Locanara SDK (published 1.0.1 is outdated) + pod 'Locanara', :path => '../../../../packages/apple' + config = use_native_modules! use_react_native!( diff --git a/libraries/react-native-ondevice-ai/ios/HybridOndeviceAi.swift b/libraries/react-native-ondevice-ai/ios/HybridOndeviceAi.swift index 404ce3c..24c078a 100644 --- a/libraries/react-native-ondevice-ai/ios/HybridOndeviceAi.swift +++ b/libraries/react-native-ondevice-ai/ios/HybridOndeviceAi.swift @@ -48,10 +48,11 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { // MARK: - AI Features - func summarize(text: String, options: NitroSummarizeOptions?) throws -> Promise { + func summarize(text: String, options: Variant_NullType_NitroSummarizeOptions?) throws -> Promise { + let opts: NitroSummarizeOptions? = if case .second(let v)? = options { v } else { nil } return Promise.async { - let bulletCount = OndeviceAiHelper.bulletCount(from: options) - let inputType = OndeviceAiHelper.inputType(from: options) + let bulletCount = OndeviceAiHelper.bulletCount(from: opts) + let inputType = OndeviceAiHelper.inputType(from: opts) let result = try await SummarizeChain(bulletCount: bulletCount, inputType: inputType).run(text) return NitroSummarizeResult( summary: result.summary, @@ -62,9 +63,10 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { } } - func classify(text: String, options: NitroClassifyOptions?) throws -> Promise { + func classify(text: String, options: Variant_NullType_NitroClassifyOptions?) throws -> Promise { + let opts: NitroClassifyOptions? = if case .second(let v)? = options { v } else { nil } return Promise.async { - let (categories, maxResults) = OndeviceAiHelper.classifyOptions(from: options) + let (categories, maxResults) = OndeviceAiHelper.classifyOptions(from: opts) let result = try await ClassifyChain(categories: categories, maxResults: maxResults).run(text) let classifications = result.classifications.map { c in NitroClassification( @@ -81,9 +83,10 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { } } - func extract(text: String, options: NitroExtractOptions?) throws -> Promise { + func extract(text: String, options: Variant_NullType_NitroExtractOptions?) throws -> Promise { + let opts: NitroExtractOptions? = if case .second(let v)? = options { v } else { nil } return Promise.async { - let entityTypes = OndeviceAiHelper.entityTypes(from: options) + let entityTypes = OndeviceAiHelper.entityTypes(from: opts) let result = try await ExtractChain(entityTypes: entityTypes).run(text) let entities = result.entities.map { e in NitroExtractEntity( @@ -98,9 +101,10 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { } } - func chat(message: String, options: NitroChatOptions?) throws -> Promise { + func chat(message: String, options: Variant_NullType_NitroChatOptions?) throws -> Promise { + let opts: NitroChatOptions? = if case .second(let v)? = options { v } else { nil } return Promise.async { - let (systemPrompt, memory) = OndeviceAiHelper.chatOptions(from: options) + let (systemPrompt, memory) = OndeviceAiHelper.chatOptions(from: opts) let result = try await ChatChain(memory: memory, systemPrompt: systemPrompt).run(message) return NitroChatResult( message: result.message, @@ -158,9 +162,10 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { // MARK: - Chat Streaming - func chatStream(message: String, options: NitroChatOptions?) throws -> Promise { + func chatStream(message: String, options: Variant_NullType_NitroChatOptions?) throws -> Promise { + let opts: NitroChatOptions? = if case .second(let v)? = options { v } else { nil } return Promise.async { - let (systemPrompt, memory) = OndeviceAiHelper.chatOptions(from: options) + let (systemPrompt, memory) = OndeviceAiHelper.chatOptions(from: opts) let chain = ChatChain(memory: memory, systemPrompt: systemPrompt) var accumulated = "" @@ -196,11 +201,11 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { } } - func addChatStreamListener(listener: @escaping (NitroChatStreamChunk) -> Void) { + func addChatStreamListener(listener: @escaping (_ chunk: NitroChatStreamChunk) -> Void) throws { listenerQueue.sync { chatStreamListeners.append(listener) } } - func removeChatStreamListener(listener: @escaping (NitroChatStreamChunk) -> Void) { + func removeChatStreamListener(listener: @escaping (_ chunk: NitroChatStreamChunk) -> Void) throws { listenerQueue.sync { chatStreamListeners.removeAll { $0 as AnyObject === listener as AnyObject } } } @@ -240,10 +245,10 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { return Promise.async { let engine = self.client.getCurrentEngine() switch engine { - case .foundationModels: return .foundation_models - case .llamaCpp: return .llama_cpp + case .foundationModels: return .foundationModels + case .llamaCpp: return .llamaCpp case .mlx: return .mlx - case .coreMl: return .core_ml + case .coreML: return .coreMl default: return .none } } @@ -269,11 +274,11 @@ class HybridOndeviceAi: HybridOndeviceAiSpec { } } - func addModelDownloadProgressListener(listener: @escaping (NitroModelDownloadProgress) -> Void) { + func addModelDownloadProgressListener(listener: @escaping (_ progress: NitroModelDownloadProgress) -> Void) throws { listenerQueue.sync { modelDownloadProgressListeners.append(listener) } } - func removeModelDownloadProgressListener(listener: @escaping (NitroModelDownloadProgress) -> Void) { + func removeModelDownloadProgressListener(listener: @escaping (_ progress: NitroModelDownloadProgress) -> Void) throws { listenerQueue.sync { modelDownloadProgressListeners.removeAll { $0 as AnyObject === listener as AnyObject } } } diff --git a/libraries/react-native-ondevice-ai/ios/OndeviceAiHelper.swift b/libraries/react-native-ondevice-ai/ios/OndeviceAiHelper.swift index ac41409..56b2260 100644 --- a/libraries/react-native-ondevice-ai/ios/OndeviceAiHelper.swift +++ b/libraries/react-native-ondevice-ai/ios/OndeviceAiHelper.swift @@ -10,8 +10,8 @@ enum OndeviceAiHelper { static func bulletCount(from options: NitroSummarizeOptions?) -> Int { guard let outputType = options?.outputType else { return 1 } switch outputType { - case .TWO_BULLETS: return 2 - case .THREE_BULLETS: return 3 + case .twoBullets: return 2 + case .threeBullets: return 3 default: return 1 } } @@ -19,7 +19,7 @@ enum OndeviceAiHelper { static func inputType(from options: NitroSummarizeOptions?) -> String { guard let inputType = options?.inputType else { return "text" } switch inputType { - case .CONVERSATION: return "conversation" + case .conversation: return "conversation" default: return "text" } } @@ -27,24 +27,42 @@ enum OndeviceAiHelper { // MARK: - Classify static func classifyOptions(from options: NitroClassifyOptions?) -> (categories: [String], maxResults: Int) { - let categories = options?.categories ?? ["positive", "negative", "neutral"] - let maxResults = options?.maxResults.flatMap { Int(exactly: $0) } ?? 3 + let categories: [String] + if case .second(let arr) = options?.categories { + categories = arr + } else { + categories = ["positive", "negative", "neutral"] + } + let maxResults: Int + if case .second(let v) = options?.maxResults { + maxResults = Int(v) + } else { + maxResults = 3 + } return (categories, maxResults) } // MARK: - Extract static func entityTypes(from options: NitroExtractOptions?) -> [String] { - options?.entityTypes ?? ["person", "location", "date", "organization"] + if case .second(let arr) = options?.entityTypes { + return arr + } + return ["person", "location", "date", "organization"] } // MARK: - Chat static func chatOptions(from options: NitroChatOptions?) -> (systemPrompt: String, memory: (any Memory)?) { - let systemPrompt = options?.systemPrompt ?? "You are a friendly, helpful assistant." + let systemPrompt: String + if case .second(let s) = options?.systemPrompt { + systemPrompt = s + } else { + systemPrompt = "You are a friendly, helpful assistant." + } var memory: (any Memory)? = nil - if let history = options?.history, !history.isEmpty { + if case .second(let history) = options?.history, !history.isEmpty { memory = PrefilledMemory(history: history) } @@ -61,12 +79,12 @@ enum OndeviceAiHelper { static func rewriteStyle(from options: NitroRewriteOptions) -> RewriteOutputType { switch options.outputType { - case .ELABORATE: return .elaborate - case .EMOJIFY: return .emojify - case .SHORTEN: return .shorten - case .FRIENDLY: return .friendly - case .PROFESSIONAL: return .professional - case .REPHRASE: return .rephrase + case .elaborate: return .elaborate + case .emojify: return .emojify + case .shorten: return .shorten + case .friendly: return .friendly + case .professional: return .professional + case .rephrase: return .rephrase } } } @@ -80,7 +98,7 @@ final class PrefilledMemory: Memory, @unchecked Sendable { init(history: [NitroChatMessage]) { self.entries = history.map { msg in - MemoryEntry(role: msg.role.rawValue, content: msg.content) + MemoryEntry(role: msg.role.stringValue, content: msg.content) } } diff --git a/libraries/react-native-ondevice-ai/src/types.ts b/libraries/react-native-ondevice-ai/src/types.ts index a49f28f..22ce604 100644 --- a/libraries/react-native-ondevice-ai/src/types.ts +++ b/libraries/react-native-ondevice-ai/src/types.ts @@ -17,7 +17,7 @@ export type RewriteOutputType = | 'PROFESSIONAL' | 'REPHRASE'; export type ProofreadInputType = 'KEYBOARD' | 'VOICE'; -export type Platform = 'IOS' | 'ANDROID'; +export type Platform = 'IOS' | 'ANDROID' | 'WEB'; export type InferenceEngine = | 'foundation_models' diff --git a/packages/apple/Locanara.podspec b/packages/apple/Locanara.podspec index b8142df..4412796 100644 --- a/packages/apple/Locanara.podspec +++ b/packages/apple/Locanara.podspec @@ -23,5 +23,6 @@ Pod::Spec.new do |s| s.source_files = 'Sources/**/*.swift' s.frameworks = 'Foundation' - s.weak_frameworks = 'FoundationModels' + # FoundationModels is resolved via canImport() in Swift source; + # weak_frameworks causes linker errors on Xcode < 26. end diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 0000000..d41fa22 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,263 @@ +# @locanara/web + +Locanara SDK for Chrome Built-in AI (Gemini Nano) + +## Requirements + +- **Chrome 138+** with Built-in AI enabled +- Chrome Canary or Dev channel recommended for testing + +## Setup + +### 1. Enable Chrome Built-in AI + +1. Open Chrome (Canary or Dev channel) +2. Go to `chrome://flags` +3. Enable the following flags: + - `#optimization-guide-on-device-model` → **Enabled BypassPerfRequirement** + - `#prompt-api-for-gemini-nano` → **Enabled** + - `#summarization-api-for-gemini-nano` → **Enabled** + - `#translation-api` → **Enabled** + - `#rewriter-api-for-gemini-nano` → **Enabled** + - `#writer-api-for-gemini-nano` → **Enabled** + - `#language-detection-api` → **Enabled** +4. Restart Chrome + +### 2. Download the Model + +After enabling the flags, the Gemini Nano model needs to be downloaded: + +1. Go to `chrome://components` +2. Find **Optimization Guide On Device Model** +3. Click **Check for update** to download the model +4. Wait for the download to complete (may take several minutes) + +### 3. Verify Installation + +Open the browser console and run: + +```javascript +// Check if Prompt API is available +await window.LanguageModel?.availability(); // Should return 'readily' + +// Check if Summarizer is available +await window.Summarizer?.availability(); // Should return 'available' +``` + +## Installation + +```bash +npm install locanara +# or +bun add locanara +``` + +## Quick Start + +```typescript +import { Locanara } from "locanara"; + +// Get singleton instance +const locanara = Locanara.getInstance(); + +// Check device capabilities +const capability = await locanara.getDeviceCapability(); +console.log(capability.availableFeatures); + +// Summarize text +const summary = await locanara.summarize("Long article text here..."); +console.log(summary.summary); + +// Translate text +const translation = await locanara.translate("Hello!", { + sourceLanguage: "en", + targetLanguage: "ko", +}); +console.log(translation.translatedText); + +// Chat with AI +const response = await locanara.chat("What is machine learning?"); +console.log(response.response); +``` + +## Features + +### Summarize + +```typescript +import { SummarizeType, SummarizeLength } from "locanara"; + +const result = await locanara.summarize(text, { + type: SummarizeType.KEY_POINTS, // KEY_POINTS | TLDR | TEASER | HEADLINE + length: SummarizeLength.MEDIUM, // SHORT | MEDIUM | LONG +}); +``` + +### Translate + +```typescript +const result = await locanara.translate(text, { + sourceLanguage: "en", + targetLanguage: "ko", +}); +``` + +Supported languages: `en`, `es`, `fr`, `de`, `ja`, `ko`, `zh`, and more. + +### Chat + +```typescript +const result = await locanara.chat(message, { + systemPrompt: "You are a helpful assistant.", + temperature: 0.7, + topK: 3, +}); + +// Reset chat session +await locanara.resetChat(); +``` + +### Rewrite + +```typescript +import { RewriteTone, RewriteLength } from "locanara"; + +const result = await locanara.rewrite(text, { + tone: RewriteTone.MORE_FORMAL, // AS_IS | MORE_FORMAL | MORE_CASUAL + length: RewriteLength.AS_IS, // SHORTER | AS_IS | LONGER +}); +``` + +### Classify + +```typescript +const result = await locanara.classify(text, { + categories: ["Technology", "Sports", "Politics"], +}); +console.log(result.category); // 'Technology' +console.log(result.confidence); // 0.95 +``` + +### Detect Language + +```typescript +const results = await locanara.detectLanguage("Bonjour!"); +console.log(results[0].detectedLanguage); // 'fr' +console.log(results[0].confidence); // 0.95 +``` + +## Streaming + +Most features support streaming: + +```typescript +// Streaming summarize +for await (const chunk of locanara.summarizeStreaming(text)) { + console.log(chunk); +} + +// Streaming chat +for await (const chunk of locanara.chatStreaming(message)) { + process.stdout.write(chunk); +} + +// Streaming translate +for await (const chunk of locanara.translateStreaming(text, options)) { + console.log(chunk); +} +``` + +## Download Progress + +Monitor model download progress: + +```typescript +const locanara = Locanara.getInstance({ + onDownloadProgress: (progress) => { + console.log( + `Downloaded: ${((progress.loaded / progress.total) * 100).toFixed(1)}%`, + ); + }, +}); +``` + +## Error Handling + +```typescript +import { LocanaraError, LocanaraErrorCode } from "locanara"; + +try { + const result = await locanara.summarize(text); +} catch (error) { + if (error instanceof LocanaraError) { + switch (error.code) { + case LocanaraErrorCode.NOT_SUPPORTED: + console.log("Feature not supported on this device"); + break; + case LocanaraErrorCode.EXECUTION_FAILED: + console.log("Execution failed:", error.message); + break; + } + } +} +``` + +## Development + +### Run Example + +```bash +cd packages/web +bun install +bun run dev +``` + +Open http://localhost:5173 in Chrome with Built-in AI enabled. + +### Run Tests + +```bash +bun run test +``` + +### Build + +```bash +bun run build +``` + +## API Reference + +### Locanara + +| Method | Description | +| ------------------------------------ | ------------------------ | +| `getInstance(options?)` | Get singleton instance | +| `getDeviceCapability()` | Check available features | +| `summarize(text, options?)` | Summarize text | +| `summarizeStreaming(text, options?)` | Summarize with streaming | +| `translate(text, options)` | Translate text | +| `translateStreaming(text, options)` | Translate with streaming | +| `chat(message, options?)` | Chat with AI | +| `chatStreaming(message, options?)` | Chat with streaming | +| `rewrite(text, options?)` | Rewrite text | +| `rewriteStreaming(text, options?)` | Rewrite with streaming | +| `classify(text, options)` | Classify text | +| `extract(text, options)` | Extract information | +| `proofread(text)` | Proofread text | +| `detectLanguage(text)` | Detect language | +| `resetChat()` | Reset chat session | +| `destroy()` | Cleanup resources | + +## Browser Support + +| Browser | Support | +| -------------- | ------------- | +| Chrome 138+ | Full support | +| Chrome Canary | Full support | +| Chrome Dev | Full support | +| Other browsers | Not supported | + +## License + +MIT diff --git a/packages/web/biome.json b/packages/web/biome.json new file mode 100644 index 0000000..f0eb40b --- /dev/null +++ b/packages/web/biome.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noForEach": "off" + }, + "style": { + "noNonNullAssertion": "off", + "useImportType": "error" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "quoteStyle": "single" + } + }, + "files": { + "ignore": ["dist/**", "node_modules/**", "*.d.ts"] + } +} diff --git a/packages/web/example/icon.png b/packages/web/example/icon.png new file mode 100644 index 0000000..b17c30c Binary files /dev/null and b/packages/web/example/icon.png differ diff --git a/packages/web/example/index.html b/packages/web/example/index.html new file mode 100644 index 0000000..dba6310 --- /dev/null +++ b/packages/web/example/index.html @@ -0,0 +1,1221 @@ + + + + + + + Locanara Web SDK Example + + + + + + + +
+ +
+ +
+
+
+ + + +
+
+
Checking AI Status...
+
Please wait
+
+
+ Setup +
+
+
+ + +
Available Features
+
+ +
+
+ + +
+
Browser
+
+
+ Browser + - +
+
+ Platform + - +
+
+ +
AI Capabilities
+
+
+ Chrome Built-in AI + - +
+
+ LanguageModel API + - +
+
+ Summarizer API + - +
+
+ Translator API + - +
+
+ Rewriter API + - +
+
+ +
SDK
+
+
+ Locanara Version + - +
+
+ SDK State + - +
+
+
+ + +
+ +
Setup Guide
+
+
+ How to Enable Chrome Built-in AI + + + +
+
+
+
+
+
1. Use Chrome 138+ (Latest Stable recommended)
+
Download latest from chrome.com
+
+
+
2. Enable Flags (click URL to copy)
+
+
    +
  • chrome://flags/#optimization-guide-on-device-model -> Enabled
  • +
  • chrome://flags/#prompt-api-for-gemini-nano -> Enabled
  • +
  • chrome://flags/#enable-experimental-web-platform-features -> Enabled
  • +
+
+
+
+
3. Restart Chrome
+
Click "Relaunch" button or close and reopen Chrome completely.
+
+
+
4. Verify Model Status
+
Go to chrome://on-device-internals -> Model Status tab
+
+
+
5. Download Model (if needed)
+
If model is not available, click the button below:
+ +
+
+
+ Requirements: Windows 10+, macOS 13+, Linux. 22GB+ free disk space. GPU with 4GB+ VRAM or CPU with 16GB+ RAM. +
+
+
+
+
+ +
Chrome Built-in AI
+
+
+ Chrome AI Settings + Copy URL +
+
+ Model Status + Copy URL +
+
+ +
About
+ +
+ + + + +
+
+
+
Input Text
+ +
+
+
Input Type
+
+ + +
+
+
+
Output Type
+
+ + + +
+
+ +
+ +
Result will appear here...
+ +
+
+
+ + +
+
+
+
Input Text
+ +
+
+
Categories
+ +
+ +
+
Result will appear here...
+
+
+
+ + +
+
+
+
Input Text
+ +
+ +
+
Result will appear here...
+
+
+
+ + +
+
+
+
Message
+ +
+
+ + +
+
+
Response will appear here...
+
+
+
+ + +
+
+
+
Input Text
+ +
+
+
Languages
+
+ + -> + +
+
+ +
+
Result will appear here...
+
+
+
+ + +
+
+
+
Input Text
+ +
+
+
Tone
+
+ + + +
+
+
+
Length
+
+ + + +
+
+ +
+
Result will appear here...
+
+
+
+ + +
+
+
+
Input Text
+ +
+ +
+
Result will appear here...
+
+
+
+ + +
+
+
+
Select Image
+ + +
+ +
+
Result will appear here...
+
+
+
+
+ + +
+ + + +
+ + + + diff --git a/packages/web/example/main.ts b/packages/web/example/main.ts new file mode 100644 index 0000000..79f7ab8 --- /dev/null +++ b/packages/web/example/main.ts @@ -0,0 +1,1007 @@ +/** + * Locanara Web SDK Example + * Tab-based navigation matching Mac/iOS structure + */ + +import { + FeatureAvailability, + FeatureType, + Locanara, + RewriteLength, + RewriteTone, + SummarizeLength, + SummarizeType, +} from '../src' + +// ============================================================================ +// Navigation State +// ============================================================================ + +type ViewState = 'tabs' | 'detail' +let currentView: ViewState = 'tabs' +let currentTab = 'features' +let currentDetailPage: string | null = null + +// Feature availability state +const featureAvailability: Record = {} + +// ============================================================================ +// Feature Definitions (matching Mac order) +// ============================================================================ + +interface FeatureDefinition { + id: string + name: string + description: string + icon: string +} + +const features: FeatureDefinition[] = [ + { + id: 'summarize', + name: 'Summarize', + description: 'Condense long text into concise summaries', + icon: '', + }, + { + id: 'classify', + name: 'Classify', + description: 'Categorize content into predefined labels', + icon: '', + }, + { + id: 'extract', + name: 'Extract', + description: 'Extract entities and key information from text', + icon: '', + }, + { + id: 'chat', + name: 'Chat', + description: 'Have conversational interactions with AI', + icon: '', + }, + { + id: 'translate', + name: 'Translate', + description: 'Translate text between languages', + icon: '', + }, + { + id: 'rewrite', + name: 'Rewrite', + description: 'Rewrite text in different styles or tones', + icon: '', + }, + { + id: 'proofread', + name: 'Proofread', + description: 'Check and correct grammar and spelling', + icon: '', + }, + { + id: 'describeimage', + name: 'Describe Image', + description: 'Generate descriptions for images', + icon: '', + }, +] + +// ============================================================================ +// Initialize Locanara +// ============================================================================ + +const locanara = Locanara.getInstance({ + onDownloadProgress: (progress) => { + console.log( + `Download progress: ${progress.total > 0 ? ((progress.loaded / progress.total) * 100).toFixed(1) : '0'}%`, + ) + }, +}) + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function $(id: string): HTMLElement { + const element = document.getElementById(id) + if (!element) { + throw new Error(`Element with ID '${id}' not found.`) + } + return element +} + +function escapeHtml(text: string): string { + const div = document.createElement('div') + div.textContent = text + return div.innerHTML +} + +function markdownToHtml(text: string): string { + const lines = text.split('\n') + let html = '' + let inList = false + + for (const line of lines) { + const bulletMatch = line.match(/^\s*[\*\-]\s+(.+)/) + if (bulletMatch) { + if (!inList) { + html += '
    ' + inList = true + } + let content = escapeHtml(bulletMatch[1]) + content = content.replace(/\*\*(.+?)\*\*/g, '$1') + content = content.replace(/\*(.+?)\*/g, '$1') + html += `
  • ${content}
  • ` + } else { + if (inList) { + html += '
' + inList = false + } + if (line.trim()) { + let content = escapeHtml(line) + content = content.replace(/\*\*(.+?)\*\*/g, '$1') + content = content.replace(/\*(.+?)\*/g, '$1') + html += `

${content}

` + } + } + } + if (inList) html += '' + return html +} + +function setResult(id: string, text: string, isError = false): void { + const el = $(id) + el.classList.remove('empty', 'error', 'formatted') + if (isError) { + el.classList.add('error') + el.textContent = text + } else { + el.classList.add('formatted') + el.innerHTML = markdownToHtml(text) + } +} + +function setLoading(btnId: string, loading: boolean): void { + const btn = $(btnId) as HTMLButtonElement + btn.disabled = loading + + if (loading) { + if (!btn.dataset.text) { + btn.dataset.text = btn.textContent || '' + } + btn.textContent = 'Processing...' + } else { + btn.textContent = btn.dataset.text || btn.textContent + } +} + +function getDropdownValue(dataId: string): string { + const dropdown = document.querySelector(`.dropdown[data-id="${dataId}"]`) as HTMLElement + return dropdown?.dataset.value || '' +} + +function getSegmentedValue(dataId: string): string { + const control = document.querySelector(`.segmented-control[data-id="${dataId}"]`) as HTMLElement + return control?.dataset.value || '' +} + +function initSegmentedControls(): void { + document.querySelectorAll('.segmented-control').forEach((control) => { + const segments = control.querySelectorAll('.segment') + segments.forEach((segment) => { + segment.addEventListener('click', (e) => { + e.stopPropagation() + const value = (segment as HTMLElement).dataset.value || '' + ;(control as HTMLElement).dataset.value = value + segments.forEach((s) => s.classList.remove('active')) + segment.classList.add('active') + }) + }) + }) +} + +// ============================================================================ +// Navigation Functions +// ============================================================================ + +function switchTab(tabId: string): void { + currentTab = tabId + + // Update tab buttons + document.querySelectorAll('.tab-item').forEach((tab) => { + tab.classList.toggle('active', tab.getAttribute('data-tab') === tabId) + }) + + // Update tab content + document.querySelectorAll('.tab-content').forEach((content) => { + content.classList.toggle('active', content.id === `tab-${tabId}`) + }) + + // Hide all detail pages + document.querySelectorAll('.detail-page').forEach((page) => { + page.classList.remove('active') + }) + + // Show tab bar, hide header + $('tab-bar').classList.remove('hidden') + $('header').classList.remove('visible') + $('content').style.padding = '1rem' + $('content').style.paddingBottom = '80px' + + currentView = 'tabs' + currentDetailPage = null +} + +function navigateToDetail(featureId: string): void { + const feature = features.find((f) => f.id === featureId) + if (!feature) return + + // Check if feature is available + const availability = featureAvailability[featureId] + if (availability === FeatureAvailability.UNAVAILABLE) return + + currentView = 'detail' + currentDetailPage = featureId + + // Hide all tab content + document.querySelectorAll('.tab-content').forEach((content) => { + content.classList.remove('active') + }) + + // Show detail page + const detailPage = $(`page-${featureId}`) + if (detailPage) { + detailPage.classList.add('active') + } + + // Update header + $('header-title').textContent = feature.name + $('header').classList.add('visible') + + // Hide tab bar + $('tab-bar').classList.add('hidden') + + // Adjust content padding + $('content').style.padding = '0' + $('content').style.paddingBottom = '0' +} + +function navigateBack(): void { + switchTab(currentTab) +} + +// ============================================================================ +// Feature List Rendering +// ============================================================================ + +function renderFeaturesList(): void { + const container = $('features-list') + + container.innerHTML = features + .map((feature) => { + const availability = featureAvailability[feature.id] || FeatureAvailability.UNAVAILABLE + const isAvailable = availability !== FeatureAvailability.UNAVAILABLE + const disabledClass = isAvailable ? '' : 'disabled' + + return ` +
+
${feature.icon}
+
+
${feature.name}
+
${feature.description}
+
+ ${ + isAvailable + ? ` +
+ + + +
+ ` + : ` +
+ + + +
+ ` + } +
+ ` + }) + .join('') + + // Add click handlers + container.querySelectorAll('.list-item').forEach((item) => { + item.addEventListener('click', () => { + const featureId = item.getAttribute('data-feature') + if (featureId && !item.classList.contains('disabled')) { + navigateToDetail(featureId) + } + }) + }) +} + +// ============================================================================ +// AI Status Banner +// ============================================================================ + +function updateAIBanner(status: 'checking' | 'available' | 'downloadable' | 'unavailable'): void { + const icon = $('ai-banner-icon') + const title = $('ai-banner-title') + const subtitle = $('ai-banner-subtitle') + const action = $('ai-banner-action') + + icon.classList.remove('success', 'warning', 'error') + action.classList.remove('visible') + + switch (status) { + case 'checking': + icon.innerHTML = + '' + title.textContent = 'Checking AI Status...' + subtitle.textContent = 'Please wait' + break + case 'available': + icon.classList.add('success') + icon.innerHTML = + '' + title.textContent = 'Chrome Built-in AI Active' + subtitle.textContent = 'On-device AI is ready to use' + break + case 'downloadable': + icon.classList.add('warning') + icon.innerHTML = + '' + title.textContent = 'Model Download Required' + subtitle.textContent = 'Tap Setup to configure' + action.classList.add('visible') + break + case 'unavailable': + icon.classList.add('error') + icon.innerHTML = + '' + title.textContent = 'Chrome Built-in AI Not Available' + subtitle.textContent = 'Tap Setup to configure' + action.classList.add('visible') + break + } +} + +function goToSettingsSetup(): void { + switchTab('settings') + // Open the setup guide automatically + setTimeout(() => { + const content = $('setup-content') + const arrow = $('setup-arrow') + if (!content.classList.contains('open')) { + content.classList.add('open') + arrow.classList.add('open') + } + }, 100) +} + +// ============================================================================ +// Device Info +// ============================================================================ + +function updateDeviceInfo(): void { + // Browser info + const userAgent = navigator.userAgent + let browser = 'Unknown' + if (userAgent.includes('Chrome')) { + const chromeMatch = userAgent.match(/Chrome\/(\d+)/) + if (chromeMatch?.[1]) { + browser = `Chrome ${chromeMatch[1]}` + } + } else if (userAgent.includes('Firefox')) browser = 'Firefox' + else if (userAgent.includes('Safari')) browser = 'Safari' + else if (userAgent.includes('Edge')) browser = 'Edge' + + $('device-browser').textContent = browser + $('device-platform').textContent = navigator.platform + + // SDK info + $('device-sdk-version').textContent = '1.0.0' + $('settings-version').textContent = '1.0.0' +} + +declare const LanguageModel: unknown +declare const Summarizer: unknown +declare const Translator: unknown +declare const Rewriter: unknown + +function updateAPIStatus(): void { + const lmStatus = $('device-lm-api') + const summarizerStatus = $('device-summarizer') + const translatorStatus = $('device-translator') + const rewriterStatus = $('device-rewriter') + + const setAPIStatus = (el: HTMLElement, available: boolean) => { + el.textContent = available ? 'Available' : 'Not Found' + el.classList.toggle('success', available) + el.classList.toggle('error', !available) + } + + setAPIStatus(lmStatus, typeof LanguageModel !== 'undefined') + setAPIStatus(summarizerStatus, typeof Summarizer !== 'undefined') + setAPIStatus(translatorStatus, typeof Translator !== 'undefined') + setAPIStatus(rewriterStatus, typeof Rewriter !== 'undefined') +} + +// ============================================================================ +// Capabilities Check +// ============================================================================ + +async function initCapabilities(): Promise { + updateAIBanner('checking') + + try { + const capability = await locanara.getDeviceCapability() + + // Store availability for each feature + let hasAnyAvailable = false + let hasDownloadable = false + + for (const f of capability.availableFeatures) { + const featureId = f.feature.toLowerCase().replace('_', '') + featureAvailability[featureId] = f.availability + + if (f.availability === FeatureAvailability.AVAILABLE) { + hasAnyAvailable = true + } else if (f.availability === FeatureAvailability.DOWNLOADABLE) { + hasDownloadable = true + } + } + + // Map DESCRIBE_IMAGE to describeimage + const describeImageFeature = capability.availableFeatures.find( + (f) => f.feature === FeatureType.DESCRIBE_IMAGE, + ) + if (describeImageFeature) { + featureAvailability.describeimage = describeImageFeature.availability + } + + // Update banner + if (hasAnyAvailable) { + updateAIBanner('available') + $('device-ai-status').textContent = 'Available' + $('device-ai-status').classList.add('success') + $('device-sdk-state').textContent = 'Initialized' + } else if (hasDownloadable) { + updateAIBanner('downloadable') + $('device-ai-status').textContent = 'Download Required' + $('device-ai-status').classList.add('warning') + $('device-sdk-state').textContent = 'Ready' + } else { + updateAIBanner('unavailable') + $('device-ai-status').textContent = 'Not Available' + $('device-ai-status').classList.add('error') + $('device-sdk-state').textContent = 'Limited' + } + + // Update feature list + renderFeaturesList() + } catch (error) { + console.error('Failed to get capabilities:', error) + updateAIBanner('unavailable') + $('device-ai-status').textContent = 'Error' + $('device-ai-status').classList.add('error') + $('device-sdk-state').textContent = 'Error' + renderFeaturesList() + } +} + +// ============================================================================ +// Custom Dropdowns +// ============================================================================ + +function initDropdowns(): void { + const dropdowns = document.querySelectorAll('.dropdown') + + dropdowns.forEach((dropdown) => { + const toggle = dropdown.querySelector('.dropdown-toggle') + const items = dropdown.querySelectorAll('.dropdown-item') + + toggle?.addEventListener('click', (e) => { + e.stopPropagation() + dropdowns.forEach((d) => { + if (d !== dropdown) d.classList.remove('open') + }) + dropdown.classList.toggle('open') + }) + + items.forEach((item) => { + item.addEventListener('click', (e) => { + e.stopPropagation() + const value = (item as HTMLElement).dataset.value || '' + const text = item.textContent || '' + ;(dropdown as HTMLElement).dataset.value = value + const span = toggle?.querySelector('span') + if (span) span.textContent = text + items.forEach((i) => i.classList.remove('selected')) + item.classList.add('selected') + dropdown.classList.remove('open') + }) + }) + }) + + document.addEventListener('click', () => { + dropdowns.forEach((d) => d.classList.remove('open')) + }) +} + +// ============================================================================ +// Feature Handlers +// ============================================================================ + +// Summarize — maps iOS-style controls (Input Type + Output Type) to Chrome Summarizer API +const bulletCount: Record = { + ONE_BULLET: 1, + TWO_BULLETS: 2, + THREE_BULLETS: 3, +} + +function trimToBullets(text: string, count: number): string { + const lines = text.split('\n') + const bullets: string[] = [] + for (const line of lines) { + if (/^\s*[\*\-]\s+/.test(line)) { + bullets.push(line) + if (bullets.length >= count) break + } + } + return bullets.length > 0 ? bullets.join('\n') : text +} + +$('summarize-btn').addEventListener('click', async () => { + const input = ($('summarize-input') as HTMLTextAreaElement).value + const outputType = getSegmentedValue('summarize-output-type') + const requestedBullets = bulletCount[outputType] ?? 1 + + if (!input.trim()) { + setResult('summarize-result', 'Please enter some text to summarize.', true) + return + } + + setLoading('summarize-btn', true) + $('summarize-stats').style.display = 'none' + $('summarize-result-title').style.display = 'none' + + try { + const result = await locanara.summarize(input, { + type: SummarizeType.KEY_POINTS, + length: SummarizeLength.LONG, + }) + const trimmed = trimToBullets(result.summary, requestedBullets) + setResult('summarize-result', trimmed) + $('summarize-result-title').style.display = 'block' + $('summarize-stat-original').textContent = `${result.originalLength} chars` + $('summarize-stat-summary').textContent = `${trimmed.length} chars` + $('summarize-stats').style.display = 'flex' + } catch (error) { + setResult('summarize-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('summarize-btn', false) + } +}) + +// Classify +$('classify-btn').addEventListener('click', async () => { + const input = ($('classify-input') as HTMLTextAreaElement).value + const categoriesInput = ($('classify-categories') as HTMLInputElement).value + const categories = categoriesInput + .split(',') + .map((c) => c.trim()) + .filter(Boolean) + + if (!input.trim()) { + setResult('classify-result', 'Please enter some text to classify.', true) + return + } + + if (categories.length < 2) { + setResult('classify-result', 'Please enter at least 2 categories.', true) + return + } + + setLoading('classify-btn', true) + + try { + const result = await locanara.classify(input, { categories }) + setResult( + 'classify-result', + `Category: ${result.category}\nConfidence: ${(result.confidence * 100).toFixed(1)}%`, + ) + } catch (error) { + setResult('classify-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('classify-btn', false) + } +}) + +// Extract +$('extract-btn').addEventListener('click', async () => { + const input = ($('extract-input') as HTMLTextAreaElement).value + + if (!input.trim()) { + setResult('extract-result', 'Please enter some text to extract from.', true) + return + } + + setLoading('extract-btn', true) + + try { + const result = await locanara.extract(input) + setResult('extract-result', JSON.stringify(result.entities, null, 2)) + } catch (error) { + setResult('extract-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('extract-btn', false) + } +}) + +// Chat +$('chat-btn').addEventListener('click', async () => { + const input = ($('chat-input') as HTMLTextAreaElement).value + + if (!input.trim()) { + setResult('chat-result', 'Please enter a message.', true) + return + } + + setLoading('chat-btn', true) + const resultEl = $('chat-result') + resultEl.classList.remove('empty', 'error') + resultEl.textContent = 'Initializing AI model...' + + try { + let response = '' + for await (const chunk of locanara.chatStreaming(input)) { + response += chunk + resultEl.textContent = response + } + if (!response) { + resultEl.textContent = '(No response)' + } + } catch (error) { + setResult('chat-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('chat-btn', false) + } +}) + +$('chat-reset-btn').addEventListener('click', async () => { + await locanara.resetChat() + const resultEl = $('chat-result') + resultEl.textContent = 'Chat session reset.' + resultEl.classList.add('empty') +}) + +// Translate +$('translate-btn').addEventListener('click', async () => { + const input = ($('translate-input') as HTMLTextAreaElement).value + const sourceLanguage = getDropdownValue('translate-source') + const targetLanguage = getDropdownValue('translate-target') + + if (!input.trim()) { + setResult('translate-result', 'Please enter some text to translate.', true) + return + } + + if (sourceLanguage === targetLanguage) { + setResult('translate-result', 'Source and target languages must be different.', true) + return + } + + setLoading('translate-btn', true) + + try { + const result = await locanara.translate(input, { sourceLanguage, targetLanguage }) + setResult('translate-result', result.translatedText) + } catch (error) { + setResult('translate-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('translate-btn', false) + } +}) + +// Rewrite +$('rewrite-btn').addEventListener('click', async () => { + const input = ($('rewrite-input') as HTMLTextAreaElement).value + const tone = getSegmentedValue('rewrite-tone') as keyof typeof RewriteTone + const length = getSegmentedValue('rewrite-length') as keyof typeof RewriteLength + + if (!input.trim()) { + setResult('rewrite-result', 'Please enter some text to rewrite.', true) + return + } + + setLoading('rewrite-btn', true) + + try { + const result = await locanara.rewrite(input, { + tone: RewriteTone[tone], + length: RewriteLength[length], + }) + setResult('rewrite-result', result.rewrittenText) + } catch (error) { + setResult('rewrite-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('rewrite-btn', false) + } +}) + +// Proofread +$('proofread-btn').addEventListener('click', async () => { + const input = ($('proofread-input') as HTMLTextAreaElement).value + + if (!input.trim()) { + setResult('proofread-result', 'Please enter some text to proofread.', true) + return + } + + setLoading('proofread-btn', true) + + try { + const result = await locanara.proofread(input) + setResult('proofread-result', result.correctedText) + } catch (error) { + setResult('proofread-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('proofread-btn', false) + } +}) + +// Describe Image +let selectedImageBlob: Blob | null = null + +$('image-input').addEventListener('change', (e) => { + const input = e.target as HTMLInputElement + const file = input.files?.[0] + + if (file) { + selectedImageBlob = file + const reader = new FileReader() + reader.onload = (e) => { + const preview = $('image-preview') as HTMLElement + const img = $('preview-img') as HTMLImageElement + img.src = e.target?.result as string + preview.style.display = 'block' + } + reader.readAsDataURL(file) + } +}) + +$('describeimage-btn').addEventListener('click', async () => { + if (!selectedImageBlob) { + setResult('describeimage-result', 'Please select an image first.', true) + return + } + + setLoading('describeimage-btn', true) + + try { + const result = await locanara.describeImage(selectedImageBlob) + setResult('describeimage-result', result.description) + } catch (error) { + setResult('describeimage-result', `Error: ${(error as Error).message}`, true) + } finally { + setLoading('describeimage-btn', false) + } +}) + +// ============================================================================ +// Settings Functions +// ============================================================================ + +function toggleSetupGuide(): void { + const content = $('setup-content') + const arrow = $('setup-arrow') + const isOpen = content.classList.contains('open') + + content.classList.toggle('open', !isOpen) + arrow.classList.toggle('open', !isOpen) +} + +function copyToClipboard(text: string, element: HTMLElement): void { + navigator.clipboard.writeText(text).then(() => { + const originalBg = element.style.background + element.style.background = '#34C759' + element.style.color = '#fff' + const originalText = element.textContent + element.textContent = 'Copied!' + setTimeout(() => { + element.style.background = originalBg + element.style.color = '#007AFF' + element.textContent = originalText || '' + }, 1000) + }) +} + +interface WindowWithAI extends Window { + ai?: { + languageModel?: unknown + } +} + +declare const LanguageModelForDownload: + | { + availability: () => Promise + create: (options: unknown) => Promise<{ destroy: () => void }> + } + | undefined + +function getLanguageModelAPI(): { + api: typeof LanguageModelForDownload | undefined + source: string +} { + const win = window as WindowWithAI + + if (typeof LanguageModel !== 'undefined') { + return { api: LanguageModel as typeof LanguageModelForDownload, source: 'window.LanguageModel' } + } + if (win.ai?.languageModel) { + return { + api: win.ai.languageModel as typeof LanguageModelForDownload, + source: 'window.ai.languageModel', + } + } + + return { api: undefined, source: '' } +} + +async function checkModelStatus(): Promise { + const btn = document.getElementById('download-model-btn') as HTMLButtonElement | null + const status = document.getElementById('download-model-status') + + if (!btn || !status) return + + const { api: lmAPI, source: apiSource } = getLanguageModelAPI() + + if (!lmAPI) { + status.innerHTML = + 'LanguageModel API not found. Enable the flags and restart Chrome.' + return + } + + status.innerHTML = 'Checking model status...' + + try { + const availability = await lmAPI.availability() + + if (availability === 'unavailable' || availability === 'no') { + status.innerHTML = + 'Model unavailable. Check hardware requirements.' + btn.textContent = 'Unavailable' + btn.disabled = true + btn.style.opacity = '0.5' + return + } + + if (availability === 'available' || availability === 'readily') { + status.innerHTML = `Model is ready! (${apiSource})` + btn.textContent = 'Already Available' + btn.disabled = true + btn.style.background = '#34C759' + return + } + + // Model needs to be downloaded + status.innerHTML = + 'Model not downloaded yet. Click button to download.' + btn.textContent = 'Download Gemini Nano Model' + btn.disabled = false + btn.style.background = '#007AFF' + btn.style.color = '#fff' + } catch (error) { + status.innerHTML = `${apiSource} found. Click to check/download.` + } +} + +async function triggerModelDownload(): Promise { + const btn = document.getElementById('download-model-btn') as HTMLButtonElement | null + const status = document.getElementById('download-model-status') + + if (!btn || !status) return + + const { api: lmAPI, source: apiSource } = getLanguageModelAPI() + + if (!lmAPI) { + status.innerHTML = + 'LanguageModel API not found. Enable the flags and restart Chrome.' + return + } + + btn.disabled = true + btn.style.opacity = '0.5' + btn.textContent = 'Checking...' + + try { + const availability = await lmAPI.availability() + + if (availability === 'unavailable' || availability === 'no') { + status.innerHTML = + 'Model unavailable. Check hardware requirements.' + btn.textContent = 'Unavailable' + return + } + + if (availability === 'available' || availability === 'readily') { + status.innerHTML = `Model is ready! (${apiSource})` + btn.textContent = 'Already Available' + btn.style.background = '#34C759' + btn.style.opacity = '1' + return + } + + btn.textContent = 'Downloading...' + status.innerHTML = 'Downloading model (~1-2GB)...' + + const session = await lmAPI.create({ + monitor: (m: EventTarget) => { + m.addEventListener('downloadprogress', (( + e: CustomEvent<{ loaded: number; total: number }>, + ) => { + const percent = ((e.detail.loaded / e.detail.total) * 100).toFixed(1) + status.innerHTML = `Downloading: ${percent}%` + }) as EventListener) + }, + }) + + session.destroy() + status.innerHTML = 'Download complete! Refresh the page.' + btn.textContent = 'Download Complete' + btn.style.background = '#34C759' + btn.style.opacity = '1' + } catch (error) { + status.innerHTML = `Error: ${(error as Error).message}` + btn.textContent = 'Retry Download' + btn.style.background = '#007AFF' + btn.style.opacity = '1' + btn.disabled = false + } +} +// Expose functions to window +;(window as unknown as Record).copyToClipboard = copyToClipboard +;(window as unknown as Record).toggleSetupGuide = toggleSetupGuide +;(window as unknown as Record).triggerModelDownload = triggerModelDownload +;(window as unknown as Record).goToSettingsSetup = goToSettingsSetup + +// ============================================================================ +// Initialization +// ============================================================================ + +function init(): void { + // Tab navigation + document.querySelectorAll('.tab-item').forEach((tab) => { + tab.addEventListener('click', () => { + const tabId = tab.getAttribute('data-tab') + if (tabId) switchTab(tabId) + }) + }) + + // Back button + $('back-btn').addEventListener('click', navigateBack) + + // Initialize controls + initDropdowns() + initSegmentedControls() + + // Load device info + updateDeviceInfo() + updateAPIStatus() + + // Check capabilities + initCapabilities() + + // Check model status automatically + checkModelStatus() + + // Initial tab + switchTab('features') +} + +init() diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000..ea3d203 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,56 @@ +{ + "name": "locanara", + "version": "1.0.0", + "type": "module", + "description": "Locanara SDK for Chrome Built-in AI (Gemini Nano)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyodotdev/locanara.git", + "directory": "packages/web" + }, + "scripts": { + "dev": "vite example", + "build": "node scripts/build.mjs", + "build:dev": "tsc", + "test": "vitest run", + "lint": "biome check src tests example", + "lint:fix": "biome check --write src tests example" + }, + "keywords": [ + "locanara", + "chrome", + "gemini-nano", + "built-in-ai", + "on-device-ai", + "summarizer", + "translator", + "language-model" + ], + "author": "hyodotdev", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "esbuild": "^0.24.0", + "jsdom": "^25.0.1", + "terser": "^5.37.0", + "typescript": "^5.9.2", + "vite": "^6.0.7", + "vitest": "^3.0.0" + } +} diff --git a/packages/web/scripts/build.mjs b/packages/web/scripts/build.mjs new file mode 100644 index 0000000..4ce02fe --- /dev/null +++ b/packages/web/scripts/build.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; +import * as esbuild from "esbuild"; +import * as fs from "fs"; +import { minify } from "terser"; + +console.log("Building Locanara Web SDK..."); + +// Step 1: Compile TypeScript (for type declarations) +console.log("Compiling TypeScript..."); +execSync("npx tsc", { stdio: "inherit" }); + +// Step 2: Bundle with esbuild +console.log("Bundling..."); +await esbuild.build({ + entryPoints: ["src/index.ts"], + bundle: true, + format: "esm", + outfile: "dist/index.js", + platform: "browser", + target: "es2020", + keepNames: true, +}); + +// Step 3: Minify with Terser +console.log("Minifying..."); +const code = fs.readFileSync("dist/index.js", "utf8"); +const result = await minify(code, { + compress: true, + mangle: { + properties: { + regex: /^_/, // Only mangle private properties + }, + keep_classnames: true, + keep_fnames: true, + }, + format: { + comments: false, + }, +}); + +fs.writeFileSync("dist/index.js", result.code); + +console.log("Build complete!"); diff --git a/packages/web/src/Errors.ts b/packages/web/src/Errors.ts new file mode 100644 index 0000000..eb5c164 --- /dev/null +++ b/packages/web/src/Errors.ts @@ -0,0 +1,70 @@ +/** + * Locanara Web SDK Errors + */ + +export enum LocanaraErrorCode { + NOT_SUPPORTED = 'NOT_SUPPORTED', + NOT_AVAILABLE = 'NOT_AVAILABLE', + DOWNLOAD_REQUIRED = 'DOWNLOAD_REQUIRED', + INITIALIZATION_FAILED = 'INITIALIZATION_FAILED', + EXECUTION_FAILED = 'EXECUTION_FAILED', + INVALID_INPUT = 'INVALID_INPUT', + ABORTED = 'ABORTED', +} + +export class LocanaraError extends Error { + code: LocanaraErrorCode + details?: unknown + + constructor(code: LocanaraErrorCode, message: string, details?: unknown) { + super(message) + this.name = 'LocanaraError' + this.code = code + this.details = details + } + + static notSupported(feature: string): LocanaraError { + return new LocanaraError( + LocanaraErrorCode.NOT_SUPPORTED, + `${feature} is not supported in this browser`, + ) + } + + static notAvailable(feature: string): LocanaraError { + return new LocanaraError( + LocanaraErrorCode.NOT_AVAILABLE, + `${feature} is not available on this device`, + ) + } + + static downloadRequired(feature: string): LocanaraError { + return new LocanaraError( + LocanaraErrorCode.DOWNLOAD_REQUIRED, + `${feature} requires model download. Call with autoDownload: true option.`, + ) + } + + static initializationFailed(feature: string, details?: unknown): LocanaraError { + return new LocanaraError( + LocanaraErrorCode.INITIALIZATION_FAILED, + `Failed to initialize ${feature}`, + details, + ) + } + + static executionFailed(feature: string, details?: unknown): LocanaraError { + return new LocanaraError( + LocanaraErrorCode.EXECUTION_FAILED, + `Failed to execute ${feature}`, + details, + ) + } + + static invalidInput(message: string): LocanaraError { + return new LocanaraError(LocanaraErrorCode.INVALID_INPUT, message) + } + + static aborted(): LocanaraError { + return new LocanaraError(LocanaraErrorCode.ABORTED, 'Operation was aborted') + } +} diff --git a/packages/web/src/Locanara.ts b/packages/web/src/Locanara.ts new file mode 100644 index 0000000..61b5a90 --- /dev/null +++ b/packages/web/src/Locanara.ts @@ -0,0 +1,1122 @@ +/** + * Locanara Web SDK + * Unified interface for Chrome Built-in AI (Gemini Nano) + */ + +import { LocanaraError } from './Errors' +import { + type ChatOptions, + type ChatResult, + type ClassifyOptions, + type ClassifyResult, + type DescribeImageOptions, + type DescribeImageResult, + type DetectLanguageResult, + type DeviceCapability, + type ExtractOptions, + type ExtractResult, + FeatureAvailability, + FeatureType, + Platform, + type ProofreadOptions, + type ProofreadResult, + RewriteLength, + type RewriteOptions, + type RewriteResult, + RewriteTone, + SummarizeFormat, + SummarizeLength, + type SummarizeOptions, + type SummarizeResult, + SummarizeType, + type TranslateOptions, + type TranslateResult, + type WriteOptions, + type WriteResult, + WriterLength, + WriterTone, +} from './Types' + +export type DownloadProgressCallback = (progress: { + loaded: number + total: number +}) => void + +export interface LocanaraOptions { + onDownloadProgress?: DownloadProgressCallback +} + +/** + * Locanara - Unified On-Device AI SDK for Web (Chrome Built-in AI) + */ +export class Locanara { + private static _instance: Locanara | null = null + private _options: LocanaraOptions + + // Cached instances + private _summarizer: ChromeSummarizer | null = null + private _summarizerOptionsKey: string | null = null + private _translators: Map = new Map() + private _rewriter: ChromeRewriter | null = null + private _rewriterOptionsKey: string | null = null + private _writer: ChromeWriter | null = null + private _writerOptionsKey: string | null = null + private _proofreadWriter: ChromeWriter | null = null + private _languageModel: ChromeLanguageModelSession | null = null + private _languageModelOptionsKey: string | null = null + private _languageDetector: ChromeLanguageDetector | null = null + + private constructor(options: LocanaraOptions = {}) { + this._options = options + } + + /** + * Get the singleton instance of Locanara + */ + static getInstance(options?: LocanaraOptions): Locanara { + if (!Locanara._instance) { + Locanara._instance = new Locanara(options) + } + return Locanara._instance + } + + /** + * Reset the singleton instance (useful for testing) + */ + static resetInstance(): void { + if (Locanara._instance) { + Locanara._instance.destroy() + Locanara._instance = null + } + } + + // ============================================================================ + // Device Capability + // ============================================================================ + + /** + * Get device AI capabilities + */ + async getDeviceCapability(): Promise { + const features: { + feature: FeatureType + availability: FeatureAvailability + }[] = [] + + // Check Summarizer + features.push({ + feature: FeatureType.SUMMARIZE, + availability: await this.checkAvailability('Summarizer'), + }) + + // Check Translator (basic check without language pair) + features.push({ + feature: FeatureType.TRANSLATE, + availability: this.checkTranslatorAvailability(), + }) + + // Check LanguageModel (for Chat, Classify, Extract) + const chatAvailability = await this.checkLanguageModelAvailability() + features.push({ + feature: FeatureType.CHAT, + availability: chatAvailability, + }) + features.push({ + feature: FeatureType.CLASSIFY, + availability: chatAvailability, + }) + features.push({ + feature: FeatureType.EXTRACT, + availability: chatAvailability, + }) + features.push({ + feature: FeatureType.DESCRIBE_IMAGE, + availability: chatAvailability, + }) + + // Check Rewriter + features.push({ + feature: FeatureType.REWRITE, + availability: await this.checkAvailability('Rewriter'), + }) + + // Check Writer (used for Proofread) + features.push({ + feature: FeatureType.PROOFREAD, + availability: await this.checkAvailability('Writer'), + }) + + return { + platform: Platform.WEB, + supportsOnDeviceAI: features.some((f) => f.availability === FeatureAvailability.AVAILABLE), + availableFeatures: features, + } + } + + private async checkAvailability( + api: 'Summarizer' | 'Rewriter' | 'Writer', + ): Promise { + try { + const apiClass = (window as unknown as Record)[api] as + | { availability?: () => Promise } + | undefined + if (!apiClass || typeof apiClass.availability !== 'function') { + return FeatureAvailability.UNAVAILABLE + } + + const status = await Promise.race([ + apiClass.availability(), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)), + ]) + + switch (status) { + case 'available': + case 'readily': + return FeatureAvailability.AVAILABLE + case 'downloadable': + case 'after-download': + return FeatureAvailability.DOWNLOADABLE + default: + return FeatureAvailability.UNAVAILABLE + } + } catch { + return FeatureAvailability.UNAVAILABLE + } + } + + private getLanguageModelAPI(): + | { + availability?: () => Promise + create?: (options: unknown) => Promise + } + | undefined { + // Try window.LanguageModel first (newer API) + // LanguageModel is a class/constructor, so typeof is "function" + const lm = (window as unknown as Record).LanguageModel + if (lm && (typeof lm === 'object' || typeof lm === 'function')) { + return lm as { + availability?: () => Promise + create?: (options: unknown) => Promise + } + } + + // Try window.ai.languageModel (older API) + const ai = (window as unknown as Record).ai as + | Record + | undefined + if (ai && typeof ai === 'object' && ai.languageModel) { + return ai.languageModel as { + availability?: () => Promise + create?: (options: unknown) => Promise + } + } + + return undefined + } + + private async checkLanguageModelAvailability(): Promise { + try { + const lm = this.getLanguageModelAPI() + if (!lm || typeof lm.availability !== 'function') { + return FeatureAvailability.UNAVAILABLE + } + + const status = await Promise.race([ + lm.availability(), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)), + ]) + + switch (status) { + case 'readily': + case 'available': + return FeatureAvailability.AVAILABLE + case 'after-download': + case 'downloading': + case 'downloadable': + return FeatureAvailability.DOWNLOADABLE + default: + return FeatureAvailability.UNAVAILABLE + } + } catch { + return FeatureAvailability.UNAVAILABLE + } + } + + private checkTranslatorAvailability(): FeatureAvailability { + const translator = (window as unknown as Record).Translator + if (translator && typeof translator === 'object') { + return FeatureAvailability.AVAILABLE + } + return FeatureAvailability.UNAVAILABLE + } + + // ============================================================================ + // Summarize + // ============================================================================ + + /** + * Summarize text + */ + async summarize(text: string, options: SummarizeOptions = {}): Promise { + if (!window.Summarizer) { + throw LocanaraError.notSupported('Summarizer') + } + + try { + const optionsKey = JSON.stringify({ + type: options.type, + length: options.length, + format: options.format, + expectedInputLanguages: options.expectedInputLanguages, + outputLanguage: options.outputLanguage, + }) + + if (!this._summarizer || this._summarizerOptionsKey !== optionsKey) { + this._summarizer?.destroy() + this._summarizer = await window.Summarizer.create({ + type: this.mapSummarizeType(options.type), + length: this.mapSummarizeLength(options.length), + format: this.mapSummarizeFormat(options.format), + sharedContext: options.context, + expectedInputLanguages: options.expectedInputLanguages ?? ['en'], + outputLanguage: options.outputLanguage ?? 'en', + monitor: this.createMonitor(), + }) + this._summarizerOptionsKey = optionsKey + } + + const summary = await this._summarizer.summarize(text, { + context: options.context, + }) + + return { + summary, + originalLength: text.length, + summaryLength: summary.length, + } + } catch (error) { + throw LocanaraError.executionFailed('summarize', error) + } + } + + /** + * Summarize text with streaming + */ + async *summarizeStreaming(text: string, options: SummarizeOptions = {}): AsyncGenerator { + if (!window.Summarizer) { + throw LocanaraError.notSupported('Summarizer') + } + + try { + const optionsKey = JSON.stringify({ + type: options.type, + length: options.length, + format: options.format, + expectedInputLanguages: options.expectedInputLanguages, + outputLanguage: options.outputLanguage, + }) + + if (!this._summarizer || this._summarizerOptionsKey !== optionsKey) { + this._summarizer?.destroy() + this._summarizer = await window.Summarizer.create({ + type: this.mapSummarizeType(options.type), + length: this.mapSummarizeLength(options.length), + format: this.mapSummarizeFormat(options.format), + sharedContext: options.context, + expectedInputLanguages: options.expectedInputLanguages ?? ['en'], + outputLanguage: options.outputLanguage ?? 'en', + monitor: this.createMonitor(), + }) + this._summarizerOptionsKey = optionsKey + } + + const stream = this._summarizer.summarizeStreaming(text, { + context: options.context, + }) + + for await (const chunk of stream) { + yield chunk + } + } catch (error) { + throw LocanaraError.executionFailed('summarizeStreaming', error) + } + } + + private mapSummarizeType(type?: SummarizeType): 'key-points' | 'tldr' | 'teaser' | 'headline' { + switch (type) { + case SummarizeType.KEY_POINTS: + return 'key-points' + case SummarizeType.TLDR: + return 'tldr' + case SummarizeType.TEASER: + return 'teaser' + case SummarizeType.HEADLINE: + return 'headline' + default: + return 'key-points' + } + } + + private mapSummarizeLength(length?: SummarizeLength): 'short' | 'medium' | 'long' { + switch (length) { + case SummarizeLength.SHORT: + return 'short' + case SummarizeLength.MEDIUM: + return 'medium' + case SummarizeLength.LONG: + return 'long' + default: + return 'medium' + } + } + + private mapSummarizeFormat(format?: SummarizeFormat): 'markdown' | 'plain-text' { + switch (format) { + case SummarizeFormat.MARKDOWN: + return 'markdown' + case SummarizeFormat.PLAIN_TEXT: + return 'plain-text' + default: + return 'markdown' + } + } + + // ============================================================================ + // Translate + // ============================================================================ + + /** + * Translate text + */ + async translate(text: string, options: TranslateOptions): Promise { + if (!window.Translator) { + throw LocanaraError.notSupported('Translator') + } + + const key = `${options.sourceLanguage}-${options.targetLanguage}` + + try { + if (!this._translators.has(key)) { + const translator = await window.Translator.create({ + sourceLanguage: options.sourceLanguage, + targetLanguage: options.targetLanguage, + monitor: this.createMonitor(), + }) + this._translators.set(key, translator) + } + + const translator = this._translators.get(key)! + const translatedText = await translator.translate(text) + + return { + translatedText, + sourceLanguage: options.sourceLanguage, + targetLanguage: options.targetLanguage, + } + } catch (error) { + throw LocanaraError.executionFailed('translate', error) + } + } + + /** + * Translate text with streaming + */ + async *translateStreaming(text: string, options: TranslateOptions): AsyncGenerator { + if (!window.Translator) { + throw LocanaraError.notSupported('Translator') + } + + const key = `${options.sourceLanguage}-${options.targetLanguage}` + + try { + if (!this._translators.has(key)) { + const translator = await window.Translator.create({ + sourceLanguage: options.sourceLanguage, + targetLanguage: options.targetLanguage, + monitor: this.createMonitor(), + }) + this._translators.set(key, translator) + } + + const translator = this._translators.get(key)! + const stream = translator.translateStreaming(text) + + for await (const chunk of stream) { + yield chunk + } + } catch (error) { + throw LocanaraError.executionFailed('translateStreaming', error) + } + } + + // ============================================================================ + // Chat (using LanguageModel / Prompt API) + // ============================================================================ + + /** + * Send a chat message + */ + async chat(message: string, options: ChatOptions = {}): Promise { + const lmAPI = this.getLanguageModelAPI() + if (!lmAPI || typeof lmAPI.create !== 'function') { + throw LocanaraError.notSupported('LanguageModel') + } + + try { + const optionsKey = JSON.stringify({ + systemPrompt: options.systemPrompt, + temperature: options.temperature, + topK: options.topK, + initialPrompts: options.initialPrompts, + }) + + if (!this._languageModel || this._languageModelOptionsKey !== optionsKey) { + this._languageModel?.destroy() + + const initialPrompts: Array<{ role: string; content: string }> = [] + + if (options.systemPrompt) { + initialPrompts.push({ + role: 'system', + content: options.systemPrompt, + }) + } + + if (options.initialPrompts) { + initialPrompts.push( + ...options.initialPrompts.map((p) => ({ + role: p.role, + content: p.content, + })), + ) + } + + this._languageModel = (await lmAPI.create({ + temperature: options.temperature, + topK: options.topK, + initialPrompts: initialPrompts.length > 0 ? initialPrompts : undefined, + monitor: this.createMonitor(), + })) as ChromeLanguageModelSession + this._languageModelOptionsKey = optionsKey + } + + const response = await this._languageModel.prompt(message) + + return { response } + } catch (error) { + throw LocanaraError.executionFailed('chat', error) + } + } + + /** + * Send a chat message with streaming + */ + async *chatStreaming(message: string, options: ChatOptions = {}): AsyncGenerator { + const lmAPI = this.getLanguageModelAPI() + if (!lmAPI || typeof lmAPI.create !== 'function') { + throw LocanaraError.notSupported('LanguageModel') + } + + try { + const optionsKey = JSON.stringify({ + systemPrompt: options.systemPrompt, + temperature: options.temperature, + topK: options.topK, + }) + + if (!this._languageModel || this._languageModelOptionsKey !== optionsKey) { + this._languageModel?.destroy() + + const initialPrompts: Array<{ role: string; content: string }> = [] + + if (options.systemPrompt) { + initialPrompts.push({ + role: 'system', + content: options.systemPrompt, + }) + } + + this._languageModel = (await lmAPI.create({ + temperature: options.temperature, + topK: options.topK, + initialPrompts: initialPrompts.length > 0 ? initialPrompts : undefined, + monitor: this.createMonitor(), + })) as ChromeLanguageModelSession + this._languageModelOptionsKey = optionsKey + } + + const stream = this._languageModel.promptStreaming(message) + const reader = stream.getReader() + let accumulated = '' + + try { + while (true) { + const result = await reader.read() + if (result.done) { + break + } + if (result.value) { + const text = typeof result.value === 'string' ? result.value : String(result.value) + // Chrome may return cumulative or delta text depending on version + if (text.length >= accumulated.length && text.startsWith(accumulated)) { + // Cumulative: chunk contains all previous content + const delta = text.slice(accumulated.length) + accumulated = text + if (delta) yield delta + } else { + // Delta: just the new portion + accumulated += text + yield text + } + } + } + } finally { + reader.releaseLock() + } + } catch (error) { + throw LocanaraError.executionFailed('chatStreaming', error) + } + } + + /** + * Reset chat session (clear context) + */ + async resetChat(): Promise { + if (this._languageModel) { + this._languageModel.destroy() + this._languageModel = null + this._languageModelOptionsKey = null + } + } + + // ============================================================================ + // Rewrite + // ============================================================================ + + /** + * Rewrite text + */ + async rewrite(text: string, options: RewriteOptions = {}): Promise { + if (!window.Rewriter) { + throw LocanaraError.notSupported('Rewriter') + } + + try { + const optionsKey = JSON.stringify({ + tone: options.tone, + length: options.length, + format: options.format, + }) + + if (!this._rewriter || this._rewriterOptionsKey !== optionsKey) { + this._rewriter?.destroy() + this._rewriter = await window.Rewriter.create({ + tone: this.mapRewriteTone(options.tone), + length: this.mapRewriteLength(options.length), + format: options.format + ? (this.mapSummarizeFormat(options.format) as 'markdown' | 'plain-text' | 'as-is') + : 'as-is', + sharedContext: options.context, + monitor: this.createMonitor(), + }) + this._rewriterOptionsKey = optionsKey + } + + const rewrittenText = await this._rewriter.rewrite(text, { + context: options.context, + }) + + return { rewrittenText } + } catch (error) { + throw LocanaraError.executionFailed('rewrite', error) + } + } + + /** + * Rewrite text with streaming + */ + async *rewriteStreaming(text: string, options: RewriteOptions = {}): AsyncGenerator { + if (!window.Rewriter) { + throw LocanaraError.notSupported('Rewriter') + } + + try { + const optionsKey = JSON.stringify({ + tone: options.tone, + length: options.length, + format: options.format, + }) + + if (!this._rewriter || this._rewriterOptionsKey !== optionsKey) { + this._rewriter?.destroy() + this._rewriter = await window.Rewriter.create({ + tone: this.mapRewriteTone(options.tone), + length: this.mapRewriteLength(options.length), + monitor: this.createMonitor(), + }) + this._rewriterOptionsKey = optionsKey + } + + const stream = this._rewriter.rewriteStreaming(text, { + context: options.context, + }) + + for await (const chunk of stream) { + yield chunk + } + } catch (error) { + throw LocanaraError.executionFailed('rewriteStreaming', error) + } + } + + private mapRewriteTone(tone?: RewriteTone): 'more-formal' | 'as-is' | 'more-casual' { + switch (tone) { + case RewriteTone.MORE_FORMAL: + return 'more-formal' + case RewriteTone.AS_IS: + return 'as-is' + case RewriteTone.MORE_CASUAL: + return 'more-casual' + default: + return 'as-is' + } + } + + private mapRewriteLength(length?: RewriteLength): 'shorter' | 'as-is' | 'longer' { + switch (length) { + case RewriteLength.SHORTER: + return 'shorter' + case RewriteLength.AS_IS: + return 'as-is' + case RewriteLength.LONGER: + return 'longer' + default: + return 'as-is' + } + } + + // ============================================================================ + // Classify (using LanguageModel) + // ============================================================================ + + /** + * Classify text into categories + */ + async classify(text: string, options: ClassifyOptions): Promise { + const lmAPI = this.getLanguageModelAPI() + if (!lmAPI || typeof lmAPI.create !== 'function') { + throw LocanaraError.notSupported('LanguageModel') + } + + if (!options.categories || options.categories.length === 0) { + throw LocanaraError.invalidInput('Categories are required for classification') + } + + try { + const session = (await lmAPI.create({ + monitor: this.createMonitor(), + })) as ChromeLanguageModelSession + + const prompt = `Classify the following text into one of these categories: ${options.categories.join( + ', ', + )}. +${options.context ? `Context: ${options.context}` : ''} + +Text to classify: +${text} + +Respond with ONLY the category name, nothing else.` + + const response = await session.prompt(prompt) + session.destroy() + + const category = response.trim() + const isValidCategory = options.categories.some( + (c) => c.toLowerCase() === category.toLowerCase(), + ) + + return { + category: isValidCategory ? category : options.categories[0]!, + confidence: isValidCategory ? 0.9 : 0.5, + } + } catch (error) { + throw LocanaraError.executionFailed('classify', error) + } + } + + // ============================================================================ + // Extract (using LanguageModel) + // ============================================================================ + + /** + * Extract entities from text + */ + async extract(text: string, options: ExtractOptions = {}): Promise { + const lmAPI = this.getLanguageModelAPI() + if (!lmAPI || typeof lmAPI.create !== 'function') { + throw LocanaraError.notSupported('LanguageModel') + } + + try { + const session = (await lmAPI.create({ + monitor: this.createMonitor(), + })) as ChromeLanguageModelSession + + const schemaDescription = options.schema + ? `Extract the following fields: ${JSON.stringify(options.schema)}` + : 'Extract key entities like names, dates, locations, organizations, and other important information' + + const prompt = `${schemaDescription} +${options.context ? `Context: ${options.context}` : ''} + +Text: +${text} + +Respond with a valid JSON object containing the extracted entities.` + + const response = await session.prompt(prompt) + session.destroy() + + try { + const entities = JSON.parse(response) + return { entities } + } catch { + return { entities: { raw: response } } + } + } catch (error) { + throw LocanaraError.executionFailed('extract', error) + } + } + + // ============================================================================ + // Proofread (using Writer API) + // ============================================================================ + + /** + * Proofread text for grammar and spelling + */ + async proofread(text: string, options: ProofreadOptions = {}): Promise { + if (!window.Writer) { + throw LocanaraError.notSupported('Writer') + } + + try { + if (!this._proofreadWriter) { + this._proofreadWriter = await window.Writer.create({ + monitor: this.createMonitor(), + }) + } + + const prompt = `Proofread and correct the following text. Fix grammar, spelling, and punctuation errors while preserving the original meaning. +${options.context ? `Context: ${options.context}` : ''} + +Text: +${text}` + + const correctedText = await this._proofreadWriter.write(prompt) + + const hasCorrections = correctedText !== text + + return { + correctedText, + corrections: [], // Chrome API doesn't provide detailed corrections + hasCorrections, + } + } catch (error) { + throw LocanaraError.executionFailed('proofread', error) + } + } + + // ============================================================================ + // Describe Image (using LanguageModel with multimodal) + // ============================================================================ + + /** + * Describe an image + */ + async describeImage( + image: Blob | HTMLImageElement | HTMLCanvasElement | ImageData, + options: DescribeImageOptions = {}, + ): Promise { + const lmAPI = this.getLanguageModelAPI() + if (!lmAPI || typeof lmAPI.create !== 'function') { + throw LocanaraError.notSupported('LanguageModel') + } + + try { + const session = (await lmAPI.create({ + monitor: this.createMonitor(), + })) as ChromeLanguageModelSession + + let imageBlob: Blob + if (image instanceof Blob) { + imageBlob = image + } else if (image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) { + const canvas = image instanceof HTMLCanvasElement ? image : await this.imageToCanvas(image) + imageBlob = await new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => (blob ? resolve(blob) : reject(new Error('Failed to convert to blob'))), + 'image/png', + ) + }) + } else { + // ImageData + const canvas = document.createElement('canvas') + canvas.width = image.width + canvas.height = image.height + const ctx = canvas.getContext('2d')! + ctx.putImageData(image, 0, 0) + imageBlob = await new Promise((resolve, reject) => { + canvas.toBlob( + (blob) => (blob ? resolve(blob) : reject(new Error('Failed to convert to blob'))), + 'image/png', + ) + }) + } + + const prompt = options.context + ? `Describe this image. Context: ${options.context}` + : 'Describe this image in detail.' + + const response = await session.prompt([ + { role: 'user', content: imageBlob }, + { role: 'user', content: prompt }, + ]) + + session.destroy() + + return { description: response } + } catch (error) { + throw LocanaraError.executionFailed('describeImage', error) + } + } + + private async imageToCanvas(image: HTMLImageElement): Promise { + const canvas = document.createElement('canvas') + canvas.width = image.naturalWidth || image.width + canvas.height = image.naturalHeight || image.height + const ctx = canvas.getContext('2d')! + ctx.drawImage(image, 0, 0) + return canvas + } + + // ============================================================================ + // Language Detection + // ============================================================================ + + /** + * Detect language of text + */ + async detectLanguage(text: string): Promise { + if (!window.LanguageDetector) { + throw LocanaraError.notSupported('LanguageDetector') + } + + try { + if (!this._languageDetector) { + this._languageDetector = await window.LanguageDetector.create({ + monitor: this.createMonitor(), + }) + } + + return await this._languageDetector.detect(text) + } catch (error) { + throw LocanaraError.executionFailed('detectLanguage', error) + } + } + + // ============================================================================ + // Write (Chrome-specific) + // ============================================================================ + + /** + * Generate text based on a prompt + */ + async write(prompt: string, options: WriteOptions = {}): Promise { + if (!window.Writer) { + throw LocanaraError.notSupported('Writer') + } + + try { + const optionsKey = JSON.stringify({ + tone: options.tone, + length: options.length, + format: options.format, + }) + + if (!this._writer || this._writerOptionsKey !== optionsKey) { + this._writer?.destroy() + this._writer = await window.Writer.create({ + tone: this.mapWriterTone(options.tone), + length: this.mapWriterLength(options.length), + format: options.format ? this.mapSummarizeFormat(options.format) : 'markdown', + sharedContext: options.context, + monitor: this.createMonitor(), + }) + this._writerOptionsKey = optionsKey + } + + const text = await this._writer.write(prompt, { + context: options.context, + }) + + return { text } + } catch (error) { + throw LocanaraError.executionFailed('write', error) + } + } + + /** + * Generate text with streaming + */ + async *writeStreaming(prompt: string, options: WriteOptions = {}): AsyncGenerator { + if (!window.Writer) { + throw LocanaraError.notSupported('Writer') + } + + try { + const optionsKey = JSON.stringify({ + tone: options.tone, + length: options.length, + format: options.format, + }) + + if (!this._writer || this._writerOptionsKey !== optionsKey) { + this._writer?.destroy() + this._writer = await window.Writer.create({ + tone: this.mapWriterTone(options.tone), + length: this.mapWriterLength(options.length), + format: options.format ? this.mapSummarizeFormat(options.format) : 'markdown', + sharedContext: options.context, + monitor: this.createMonitor(), + }) + this._writerOptionsKey = optionsKey + } + + const stream = this._writer.writeStreaming(prompt, { + context: options.context, + }) + + for await (const chunk of stream) { + yield chunk + } + } catch (error) { + throw LocanaraError.executionFailed('writeStreaming', error) + } + } + + private mapWriterTone(tone?: WriterTone): 'formal' | 'neutral' | 'casual' { + switch (tone) { + case WriterTone.FORMAL: + return 'formal' + case WriterTone.NEUTRAL: + return 'neutral' + case WriterTone.CASUAL: + return 'casual' + default: + return 'neutral' + } + } + + private mapWriterLength(length?: WriterLength): 'short' | 'medium' | 'long' { + switch (length) { + case WriterLength.SHORT: + return 'short' + case WriterLength.MEDIUM: + return 'medium' + case WriterLength.LONG: + return 'long' + default: + return 'medium' + } + } + + // ============================================================================ + // Utilities + // ============================================================================ + + private createMonitor(): ((m: EventTarget) => void) | undefined { + if (!this._options.onDownloadProgress) return undefined + + return (monitor: EventTarget) => { + monitor.addEventListener('downloadprogress', (( + e: Event & { loaded?: number; total?: number }, + ) => { + const loaded = e.loaded ?? 0 + const total = e.total ?? 1 + this._options.onDownloadProgress?.({ loaded, total }) + }) as EventListener) + } + } + + // ============================================================================ + // Model Management + // ============================================================================ + + /** + * Preload models for better performance + * Chrome Built-in AI manages model caching automatically. + */ + async preloadModels(_features: FeatureType[]): Promise { + // Chrome Built-in AI manages model caching automatically + } + + /** + * Unload models to free memory + * Chrome Built-in AI manages model memory automatically. + */ + async unloadModels(_features: FeatureType[]): Promise { + // Chrome Built-in AI manages model memory automatically + } + + /** + * Cancel an ongoing execution + */ + cancelExecution(_executionId: string): void { + // Chrome Built-in AI operations complete quickly + } + + /** + * Download a specific model + * Chrome Built-in AI manages model downloads automatically. + */ + async downloadModel(_modelId: string): Promise { + // Chrome Built-in AI manages model downloads automatically + } + + // ============================================================================ + // Cleanup + // ============================================================================ + + /** + * Destroy all cached instances and free resources + */ + destroy(): void { + this._summarizer?.destroy() + this._summarizer = null + this._summarizerOptionsKey = null + + for (const translator of this._translators.values()) { + translator.destroy() + } + this._translators.clear() + + this._rewriter?.destroy() + this._rewriter = null + this._rewriterOptionsKey = null + + this._writer?.destroy() + this._writer = null + this._writerOptionsKey = null + + this._proofreadWriter?.destroy() + this._proofreadWriter = null + + this._languageModel?.destroy() + this._languageModel = null + + this._languageDetector = null + } +} + +export default Locanara diff --git a/packages/web/src/Types.ts b/packages/web/src/Types.ts new file mode 100644 index 0000000..e92d640 --- /dev/null +++ b/packages/web/src/Types.ts @@ -0,0 +1,448 @@ +/** + * Locanara Web SDK Types + * Types matching the GraphQL schema for Chrome Built-in AI + */ + +// ============================================================================ +// Enums (Common — from type.graphql) +// ============================================================================ + +export enum Platform { + IOS = 'IOS', + ANDROID = 'ANDROID', + WEB = 'WEB', +} + +export enum FeatureType { + SUMMARIZE = 'SUMMARIZE', + CLASSIFY = 'CLASSIFY', + EXTRACT = 'EXTRACT', + CHAT = 'CHAT', + TRANSLATE = 'TRANSLATE', + REWRITE = 'REWRITE', + PROOFREAD = 'PROOFREAD', + DESCRIBE_IMAGE = 'DESCRIBE_IMAGE', +} + +export enum FeatureAvailability { + AVAILABLE = 'AVAILABLE', + DOWNLOADABLE = 'DOWNLOADABLE', + UNAVAILABLE = 'UNAVAILABLE', +} + +/** ML Kit Summarization InputType (common — type.graphql) */ +export enum SummarizeInputType { + ARTICLE = 'ARTICLE', + CONVERSATION = 'CONVERSATION', +} + +/** ML Kit Summarization OutputType (common — type.graphql) */ +export enum SummarizeOutputType { + ONE_BULLET = 'ONE_BULLET', + TWO_BULLETS = 'TWO_BULLETS', + THREE_BULLETS = 'THREE_BULLETS', +} + +/** ML Kit Rewrite OutputType / Style (common — type.graphql) */ +export enum RewriteOutputType { + ELABORATE = 'ELABORATE', + EMOJIFY = 'EMOJIFY', + SHORTEN = 'SHORTEN', + FRIENDLY = 'FRIENDLY', + PROFESSIONAL = 'PROFESSIONAL', + REPHRASE = 'REPHRASE', +} + +/** ML Kit Proofreading InputType (common — type.graphql) */ +export enum ProofreadInputType { + KEYBOARD = 'KEYBOARD', + VOICE = 'VOICE', +} + +/** Device capability levels (common — type.graphql) */ +export enum CapabilityLevel { + NONE = 'NONE', + LIMITED = 'LIMITED', + FULL = 'FULL', +} + +/** Feature download status (common — type.graphql) */ +export enum FeatureStatus { + UNAVAILABLE = 'UNAVAILABLE', + DOWNLOADABLE = 'DOWNLOADABLE', + DOWNLOADING = 'DOWNLOADING', + AVAILABLE = 'AVAILABLE', +} + +/** Model execution state (common — type.graphql) */ +export enum ExecutionState { + IDLE = 'IDLE', + PREPARING = 'PREPARING', + PROCESSING = 'PROCESSING', + COMPLETED = 'COMPLETED', + FAILED = 'FAILED', + CANCELLED = 'CANCELLED', +} + +// ============================================================================ +// Enums (Web-specific — from type-web.graphql, Chrome Built-in AI) +// ============================================================================ + +export enum SummarizeType { + KEY_POINTS = 'KEY_POINTS', + TLDR = 'TLDR', + TEASER = 'TEASER', + HEADLINE = 'HEADLINE', +} + +export enum SummarizeLength { + SHORT = 'SHORT', + MEDIUM = 'MEDIUM', + LONG = 'LONG', +} + +export enum SummarizeFormat { + MARKDOWN = 'MARKDOWN', + PLAIN_TEXT = 'PLAIN_TEXT', +} + +export enum RewriteTone { + MORE_FORMAL = 'MORE_FORMAL', + AS_IS = 'AS_IS', + MORE_CASUAL = 'MORE_CASUAL', +} + +export enum RewriteLength { + SHORTER = 'SHORTER', + AS_IS = 'AS_IS', + LONGER = 'LONGER', +} + +export enum WriterTone { + FORMAL = 'FORMAL', + NEUTRAL = 'NEUTRAL', + CASUAL = 'CASUAL', +} + +export enum WriterLength { + SHORT = 'SHORT', + MEDIUM = 'MEDIUM', + LONG = 'LONG', +} + +// ============================================================================ +// Device Capability +// ============================================================================ + +export interface FeatureCapability { + feature: FeatureType + availability: FeatureAvailability +} + +export interface DeviceCapability { + platform: Platform + supportsOnDeviceAI: boolean + availableFeatures: FeatureCapability[] +} + +// ============================================================================ +// Summarize +// ============================================================================ + +export interface SummarizeOptions { + type?: SummarizeType + length?: SummarizeLength + format?: SummarizeFormat + context?: string + expectedInputLanguages?: string[] + outputLanguage?: string +} + +export interface SummarizeResult { + summary: string + originalLength: number + summaryLength: number +} + +// ============================================================================ +// Translate +// ============================================================================ + +export interface TranslateOptions { + sourceLanguage: string + targetLanguage: string +} + +export interface TranslateResult { + translatedText: string + sourceLanguage: string + targetLanguage: string +} + +// ============================================================================ +// Chat +// ============================================================================ + +export interface ChatMessage { + role: 'user' | 'assistant' | 'system' + content: string +} + +export interface ChatOptions { + systemPrompt?: string + temperature?: number + topK?: number + initialPrompts?: ChatMessage[] +} + +export interface ChatResult { + response: string +} + +// ============================================================================ +// Rewrite +// ============================================================================ + +export interface RewriteOptions { + tone?: RewriteTone + length?: RewriteLength + format?: SummarizeFormat + context?: string +} + +export interface RewriteResult { + rewrittenText: string +} + +// ============================================================================ +// Classify +// ============================================================================ + +export interface ClassifyOptions { + categories: string[] + context?: string +} + +export interface ClassifyResult { + category: string + confidence: number +} + +// ============================================================================ +// Extract +// ============================================================================ + +export interface ExtractOptions { + schema?: Record + context?: string +} + +export interface ExtractResult { + entities: Record +} + +// ============================================================================ +// Proofread +// ============================================================================ + +export interface ProofreadOptions { + context?: string +} + +export interface ProofreadResult { + correctedText: string + corrections: ProofreadCorrection[] + hasCorrections: boolean +} + +export interface ProofreadCorrection { + original: string + corrected: string + type?: string +} + +// ============================================================================ +// Describe Image +// ============================================================================ + +export interface DescribeImageOptions { + context?: string +} + +export interface DescribeImageResult { + description: string +} + +// ============================================================================ +// Language Detection +// ============================================================================ + +export interface DetectLanguageResult { + detectedLanguage: string + confidence: number +} + +// ============================================================================ +// Writer (Chrome-specific) +// ============================================================================ + +export interface WriteOptions { + tone?: WriterTone + length?: WriterLength + format?: SummarizeFormat + context?: string +} + +export interface WriteResult { + text: string +} + +// ============================================================================ +// Execution Result (Generic) +// ============================================================================ + +export interface ExecutionResult { + success: boolean + result?: T + error?: string +} + +// ============================================================================ +// Chrome Built-in AI Type Declarations +// ============================================================================ + +declare global { + interface Window { + Summarizer?: SummarizerConstructor + Translator?: TranslatorConstructor + Rewriter?: RewriterConstructor + Writer?: WriterConstructor + LanguageModel?: LanguageModelConstructor + LanguageDetector?: LanguageDetectorConstructor + } + + // Summarizer + interface SummarizerConstructor { + availability(): Promise + create(options?: ChromeSummarizerOptions): Promise + } + + interface ChromeSummarizerOptions { + type?: 'key-points' | 'tldr' | 'teaser' | 'headline' | undefined + length?: 'short' | 'medium' | 'long' | undefined + format?: 'markdown' | 'plain-text' | undefined + sharedContext?: string | undefined + expectedInputLanguages?: string[] | undefined + outputLanguage?: string | undefined + monitor?: ((m: EventTarget) => void) | undefined + } + + interface ChromeSummarizer { + summarize(text: string, options?: { context?: string }): Promise + summarizeStreaming(text: string, options?: { context?: string }): AsyncIterable + destroy(): void + } + + // Translator + interface TranslatorConstructor { + availability(options: { + sourceLanguage: string + targetLanguage: string + }): Promise + create(options: ChromeTranslatorOptions): Promise + } + + interface ChromeTranslatorOptions { + sourceLanguage: string + targetLanguage: string + monitor?: (m: EventTarget) => void + } + + interface ChromeTranslator { + translate(text: string): Promise + translateStreaming(text: string): AsyncIterable + destroy(): void + } + + // Rewriter + interface RewriterConstructor { + availability(): Promise + create(options?: ChromeRewriterOptions): Promise + } + + interface ChromeRewriterOptions { + tone?: 'more-formal' | 'as-is' | 'more-casual' + length?: 'shorter' | 'as-is' | 'longer' + format?: 'as-is' | 'markdown' | 'plain-text' + sharedContext?: string + monitor?: (m: EventTarget) => void + } + + interface ChromeRewriter { + rewrite(text: string, options?: { context?: string }): Promise + rewriteStreaming(text: string, options?: { context?: string }): AsyncIterable + destroy(): void + } + + // Writer + interface WriterConstructor { + availability(): Promise + create(options?: ChromeWriterOptions): Promise + } + + interface ChromeWriterOptions { + tone?: 'formal' | 'neutral' | 'casual' + length?: 'short' | 'medium' | 'long' + format?: 'markdown' | 'plain-text' + sharedContext?: string + monitor?: (m: EventTarget) => void + } + + interface ChromeWriter { + write(prompt: string, options?: { context?: string }): Promise + writeStreaming(prompt: string, options?: { context?: string }): AsyncIterable + destroy(): void + } + + // LanguageModel (Prompt API) + interface LanguageModelConstructor { + availability(): Promise<'readily' | 'after-download' | 'downloading' | 'unavailable'> + params(): Promise<{ + defaultTopK: number + maxTopK: number + defaultTemperature: number + maxTemperature: number + }> + create(options?: ChromeLanguageModelOptions): Promise + } + + interface ChromeLanguageModelOptions { + temperature?: number + topK?: number + initialPrompts?: Array<{ role: string; content: string }> + monitor?: (m: EventTarget) => void + } + + interface ChromeLanguageModelSession { + prompt( + input: string | Array<{ role: string; content: string | Blob | ImageData }>, + ): Promise + promptStreaming(input: string): ReadableStream + clone(): Promise + destroy(): void + inputUsage: number + inputQuota: number + } + + // LanguageDetector + interface LanguageDetectorConstructor { + availability(): Promise<'readily' | 'downloadable' | 'no'> + create(options?: { + monitor?: (m: EventTarget) => void + }): Promise + } + + interface ChromeLanguageDetector { + detect(text: string): Promise> + } +} diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts new file mode 100644 index 0000000..cea03f4 --- /dev/null +++ b/packages/web/src/index.ts @@ -0,0 +1,13 @@ +/** + * Locanara Web SDK + * Unified On-Device AI SDK for Chrome Built-in AI (Gemini Nano) + * + * @packageDocumentation + */ + +export { Locanara, type LocanaraOptions, type DownloadProgressCallback } from './Locanara' +export { LocanaraError, LocanaraErrorCode } from './Errors' +export * from './Types' + +// Default export +export { Locanara as default } from './Locanara' diff --git a/packages/web/tests/Locanara.test.ts b/packages/web/tests/Locanara.test.ts new file mode 100644 index 0000000..ff48951 --- /dev/null +++ b/packages/web/tests/Locanara.test.ts @@ -0,0 +1,343 @@ +/** + * Locanara Web SDK Tests + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + FeatureAvailability, + FeatureType, + Locanara, + LocanaraError, + LocanaraErrorCode, + Platform, + RewriteTone, + SummarizeLength, + SummarizeType, +} from '../src' + +// Mock Chrome Built-in AI APIs +const mockSummarizer = { + summarize: vi.fn().mockResolvedValue('This is a summary.'), + summarizeStreaming: vi.fn(), + destroy: vi.fn(), +} + +const mockTranslator = { + translate: vi.fn().mockResolvedValue('Hola! ¿Cómo estás?'), + translateStreaming: vi.fn(), + destroy: vi.fn(), +} + +const mockRewriter = { + rewrite: vi.fn().mockResolvedValue('Rewritten text here.'), + rewriteStreaming: vi.fn(), + destroy: vi.fn(), +} + +const mockLanguageModelSession = { + prompt: vi.fn().mockResolvedValue('AI response here.'), + promptStreaming: vi.fn().mockReturnValue({ + getReader: () => ({ + read: vi + .fn() + .mockResolvedValueOnce({ done: false, value: 'chunk1' }) + .mockResolvedValueOnce({ done: false, value: 'chunk2' }) + .mockResolvedValueOnce({ done: true, value: undefined }), + }), + }), + clone: vi.fn(), + destroy: vi.fn(), + inputUsage: 0, + inputQuota: 1000, +} + +const mockLanguageDetector = { + detect: vi.fn().mockResolvedValue([ + { detectedLanguage: 'fr', confidence: 0.95 }, + { detectedLanguage: 'en', confidence: 0.03 }, + ]), +} + +describe('Locanara', () => { + beforeEach(() => { + // Reset singleton + Locanara.resetInstance() + + // Setup window mocks + ;(window as unknown as Record).Summarizer = { + availability: vi.fn().mockResolvedValue('available'), + create: vi.fn().mockResolvedValue(mockSummarizer), + } + ;(window as unknown as Record).Translator = { + availability: vi.fn().mockResolvedValue('available'), + create: vi.fn().mockResolvedValue(mockTranslator), + } + ;(window as unknown as Record).Rewriter = { + availability: vi.fn().mockResolvedValue('available'), + create: vi.fn().mockResolvedValue(mockRewriter), + } + ;(window as unknown as Record).Writer = { + availability: vi.fn().mockResolvedValue('available'), + create: vi.fn().mockResolvedValue(mockRewriter), // Using same mock for simplicity + } + ;(window as unknown as Record).LanguageModel = { + availability: vi.fn().mockResolvedValue('readily'), + params: vi.fn().mockResolvedValue({ + defaultTopK: 3, + maxTopK: 128, + defaultTemperature: 1, + maxTemperature: 2, + }), + create: vi.fn().mockResolvedValue(mockLanguageModelSession), + } + ;(window as unknown as Record).LanguageDetector = { + availability: vi.fn().mockResolvedValue('readily'), + create: vi.fn().mockResolvedValue(mockLanguageDetector), + } + }) + + afterEach(() => { + Locanara.resetInstance() + vi.clearAllMocks() + + // Cleanup window mocks + ;(window as unknown as Record).Summarizer = undefined + ;(window as unknown as Record).Translator = undefined + ;(window as unknown as Record).Rewriter = undefined + ;(window as unknown as Record).Writer = undefined + ;(window as unknown as Record).LanguageModel = undefined + ;(window as unknown as Record).LanguageDetector = undefined + }) + + describe('getInstance', () => { + it('should return singleton instance', () => { + const instance1 = Locanara.getInstance() + const instance2 = Locanara.getInstance() + expect(instance1).toBe(instance2) + }) + + it('should accept options on first call', () => { + const onProgress = vi.fn() + const instance = Locanara.getInstance({ onDownloadProgress: onProgress }) + expect(instance).toBeDefined() + }) + }) + + describe('getDeviceCapability', () => { + it('should return device capabilities', async () => { + const locanara = Locanara.getInstance() + const capability = await locanara.getDeviceCapability() + + expect(capability.platform).toBe(Platform.WEB) + expect(capability.supportsOnDeviceAI).toBe(true) + expect(capability.availableFeatures).toHaveLength(8) + }) + + it('should report unavailable when APIs not present', async () => { + ;(window as unknown as Record).Summarizer = undefined + + const locanara = Locanara.getInstance() + const capability = await locanara.getDeviceCapability() + + const summarize = capability.availableFeatures.find( + (f) => f.feature === FeatureType.SUMMARIZE, + ) + expect(summarize?.availability).toBe(FeatureAvailability.UNAVAILABLE) + }) + }) + + describe('summarize', () => { + it('should summarize text', async () => { + const locanara = Locanara.getInstance() + const result = await locanara.summarize('Long text to summarize...') + + expect(result.summary).toBe('This is a summary.') + expect(result.originalLength).toBe('Long text to summarize...'.length) + expect(result.summaryLength).toBe('This is a summary.'.length) + expect(mockSummarizer.summarize).toHaveBeenCalledWith( + 'Long text to summarize...', + expect.any(Object), + ) + }) + + it('should accept options', async () => { + const locanara = Locanara.getInstance() + await locanara.summarize('Text', { + type: SummarizeType.TLDR, + length: SummarizeLength.SHORT, + }) + + expect(window.Summarizer?.create).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'tldr', + length: 'short', + }), + ) + }) + + it('should throw when API not supported', async () => { + ;(window as unknown as Record).Summarizer = undefined + + const locanara = Locanara.getInstance() + await expect(locanara.summarize('Text')).rejects.toThrow(LocanaraError) + }) + }) + + describe('translate', () => { + it('should translate text', async () => { + const locanara = Locanara.getInstance() + const result = await locanara.translate('Hello!', { + sourceLanguage: 'en', + targetLanguage: 'es', + }) + + expect(result.translatedText).toBe('Hola! ¿Cómo estás?') + expect(result.sourceLanguage).toBe('en') + expect(result.targetLanguage).toBe('es') + }) + + it('should cache translators by language pair', async () => { + const locanara = Locanara.getInstance() + + await locanara.translate('Hello', { sourceLanguage: 'en', targetLanguage: 'es' }) + await locanara.translate('World', { sourceLanguage: 'en', targetLanguage: 'es' }) + + // Should only create one translator + expect(window.Translator?.create).toHaveBeenCalledTimes(1) + }) + + it('should create different translators for different language pairs', async () => { + const locanara = Locanara.getInstance() + + await locanara.translate('Hello', { sourceLanguage: 'en', targetLanguage: 'es' }) + await locanara.translate('Hello', { sourceLanguage: 'en', targetLanguage: 'fr' }) + + expect(window.Translator?.create).toHaveBeenCalledTimes(2) + }) + }) + + describe('chat', () => { + it('should send chat message', async () => { + const locanara = Locanara.getInstance() + const result = await locanara.chat('Hello AI!') + + expect(result.response).toBe('AI response here.') + expect(mockLanguageModelSession.prompt).toHaveBeenCalledWith('Hello AI!') + }) + + it('should accept system prompt', async () => { + const locanara = Locanara.getInstance() + await locanara.chat('Hello', { + systemPrompt: 'You are a helpful assistant.', + }) + + expect(window.LanguageModel?.create).toHaveBeenCalledWith( + expect.objectContaining({ + initialPrompts: expect.arrayContaining([ + { role: 'system', content: 'You are a helpful assistant.' }, + ]), + }), + ) + }) + + it('should reset chat session', async () => { + const locanara = Locanara.getInstance() + await locanara.chat('Hello') + await locanara.resetChat() + + expect(mockLanguageModelSession.destroy).toHaveBeenCalled() + }) + }) + + describe('rewrite', () => { + it('should rewrite text', async () => { + const locanara = Locanara.getInstance() + const result = await locanara.rewrite('hey whats up') + + expect(result.rewrittenText).toBe('Rewritten text here.') + }) + + it('should accept tone option', async () => { + const locanara = Locanara.getInstance() + await locanara.rewrite('hey', { tone: RewriteTone.MORE_FORMAL }) + + expect(window.Rewriter?.create).toHaveBeenCalledWith( + expect.objectContaining({ + tone: 'more-formal', + }), + ) + }) + }) + + describe('classify', () => { + it('should classify text into categories', async () => { + mockLanguageModelSession.prompt.mockResolvedValueOnce('Technology') + + const locanara = Locanara.getInstance() + const result = await locanara.classify('The new iPhone is great', { + categories: ['Technology', 'Sports', 'Politics'], + }) + + expect(result.category).toBe('Technology') + expect(result.confidence).toBeGreaterThan(0) + }) + + it('should throw when no categories provided', async () => { + const locanara = Locanara.getInstance() + await expect(locanara.classify('Text', { categories: [] })).rejects.toThrow(LocanaraError) + }) + }) + + describe('detectLanguage', () => { + it('should detect language', async () => { + const locanara = Locanara.getInstance() + const results = await locanara.detectLanguage('Bonjour!') + + expect(results).toHaveLength(2) + expect(results[0].detectedLanguage).toBe('fr') + expect(results[0].confidence).toBe(0.95) + }) + }) + + describe('destroy', () => { + it('should destroy all cached instances', async () => { + const locanara = Locanara.getInstance() + + // Create some instances + await locanara.summarize('Text') + await locanara.translate('Hello', { sourceLanguage: 'en', targetLanguage: 'es' }) + await locanara.chat('Hello') + + locanara.destroy() + + expect(mockSummarizer.destroy).toHaveBeenCalled() + expect(mockTranslator.destroy).toHaveBeenCalled() + expect(mockLanguageModelSession.destroy).toHaveBeenCalled() + }) + }) +}) + +describe('LocanaraError', () => { + it('should create error with code and message', () => { + const error = new LocanaraError(LocanaraErrorCode.NOT_SUPPORTED, 'Feature not supported') + + expect(error.code).toBe(LocanaraErrorCode.NOT_SUPPORTED) + expect(error.message).toBe('Feature not supported') + expect(error.name).toBe('LocanaraError') + }) + + it('should create notSupported error', () => { + const error = LocanaraError.notSupported('Summarizer') + + expect(error.code).toBe(LocanaraErrorCode.NOT_SUPPORTED) + expect(error.message).toContain('Summarizer') + }) + + it('should create executionFailed error with details', () => { + const details = { originalError: 'Network error' } + const error = LocanaraError.executionFailed('summarize', details) + + expect(error.code).toBe(LocanaraErrorCode.EXECUTION_FAILED) + expect(error.details).toBe(details) + }) +}) diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 0000000..0c48f3d --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": false, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "example", "tests"] +} diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts new file mode 100644 index 0000000..34c644b --- /dev/null +++ b/packages/web/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: 'example', + server: { + port: 5173, + open: true, + }, + build: { + outDir: '../dist-example', + emptyOutDir: true, + }, + resolve: { + alias: { + '@locanara/web': new URL('./src/index.ts', import.meta.url).pathname, + }, + }, +}); diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts new file mode 100644 index 0000000..9f893c0 --- /dev/null +++ b/packages/web/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + include: ['tests/**/*.test.ts'], + coverage: { + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.d.ts'], + }, + }, + resolve: { + alias: { + '@locanara/web': new URL('./src/index.ts', import.meta.url).pathname, + }, + }, +});