Compare commits

..

No commits in common. "develop" and "v0.94.1" have entirely different histories.

506 changed files with 16888 additions and 26023 deletions

@ -1 +0,0 @@
NODE_OPTIONS=--max_old_space_size=4096

@ -39,7 +39,76 @@ jobs:
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
- name: Check affected - name: Check affected
run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build run: pnpm nx affected --verbose -t typecheck build rebuild-deps
report-electron-size:
name: Report Electron size
runs-on: ubuntu-latest
needs:
- check-affected
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run the build
uses: ./.github/actions/build-electron
with:
os: linux
arch: x64
shell: bash
forge_platform: linux
- name: Run the Electron size report
uses: ./.github/actions/report-size
with:
paths: 'upload/**/*'
onlyDiff: 'true'
branch: 'develop'
header: 'Electron size report'
unit: "MB"
ghToken: ${{ secrets.GITHUB_TOKEN }}
report-server-size:
name: Report server size
runs-on: ubuntu-latest
needs:
- check-affected
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Run the build
uses: ./.github/actions/build-server
with:
os: linux
arch: x64
- name: Run the server size report
uses: ./.github/actions/report-size
with:
paths: 'upload/**/*'
onlyDiff: 'true'
branch: 'develop'
header: 'Server size report'
unit: "MB"
ghToken: ${{ secrets.GITHUB_TOKEN }}
test_dev: test_dev:
name: Test development name: Test development
@ -74,14 +143,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Update build info - name: Update build info
run: pnpm run chore:update-build-info run: pnpm run chore:update-build-info
- name: Trigger client build - name: Trigger build
run: pnpm nx run client:build
- name: Send client bundle stats to RelativeCI
uses: relative-ci/agent-action@v3
with:
webpackStatsFile: ./apps/client/dist/webpack-stats.json
key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }}
- name: Trigger server build
run: pnpm nx run server:build run: pnpm nx run server:build
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6 - uses: docker/build-push-action@v6

@ -53,7 +53,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps run: pnpx playwright install --with-deps
- name: Run the TypeScript build - name: Run the TypeScript build
run: pnpm run server:build run: pnpm run server:build
@ -82,15 +82,7 @@ jobs:
require-healthy: true require-healthy: true
- name: Run Playwright tests - name: Run Playwright tests
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpx nx run server-e2e:e2e
- name: Upload Playwright trace
if: failure()
uses: actions/upload-artifact@v4
with:
name: Playwright trace (${{ matrix.dockerfile }})
path: test-output/playwright/output
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:

@ -11,8 +11,7 @@ on:
pull_request: pull_request:
paths: paths:
- .github/actions/build-electron/* - .github/actions/build-electron/*
- .github/workflows/nightly.yml - forge.config.cjs
- forge.config.ts
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -77,7 +76,7 @@ jobs:
WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
- name: Publish release - name: Publish release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@v2
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
with: with:
make_latest: false make_latest: false
@ -117,7 +116,7 @@ jobs:
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
- name: Publish release - name: Publish release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@v2
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
with: with:
make_latest: false make_latest: false

@ -33,11 +33,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps - run: pnpx playwright install --with-deps
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: npx nx-cloud record -- echo Hello World # - run: npx nx-cloud record -- echo Hello World
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
# When you enable task distribution, run the e2e-ci task instead of e2e # When you enable task distribution, run the e2e-ci task instead of e2e
- run: pnpm exec nx affected -t e2e - run: pnpx nx affected -t e2e

@ -114,7 +114,7 @@ jobs:
path: upload path: upload
- name: Publish stable release - name: Publish stable release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@v2
with: with:
draft: false draft: false
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
@ -122,5 +122,5 @@ jobs:
files: upload/*.* files: upload/*.*
discussion_category_name: Announcements discussion_category_name: Announcements
make_latest: ${{ !contains(github.ref, 'rc') }} make_latest: ${{ !contains(github.ref, 'rc') }}
prerelease: ${{ contains(github.ref, 'rc') }} prerelease: ${{ !contains(github.ref, 'rc') }}
token: ${{ secrets.RELEASE_PAT }} token: ${{ secrets.RELEASE_PAT }}

3
.gitignore vendored

@ -45,5 +45,4 @@ upload
.rollup.cache .rollup.cache
*.tsbuildinfo *.tsbuildinfo
/result /result
.svelte-kit

@ -1,2 +1,7 @@
_regroup _regroup
_regroup_monorepo _regroup_monorepo
# Asset copying respects .gitignore / .nxignore for some reason.
# See https://github.com/nrwl/nx/issues/20309
!dist
!node_modules

@ -9,8 +9,6 @@
"redhat.vscode-yaml", "redhat.vscode-yaml",
"tobermory.es6-string-html", "tobermory.es6-string-html",
"vitest.explorer", "vitest.explorer",
"yzhang.markdown-all-in-one", "yzhang.markdown-all-in-one"
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss"
] ]
} }

@ -1,2 +1,168 @@
> [!IMPORTANT] # TriliumNext Notes
> TriliumNext started as a fork of the original Trilium repository (`zadam/trilium`). @zadam transferred the original repo to us so the work will continue in https://github.com/TriliumNext/Trilium.
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran) ![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total)
[English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
TriliumNext Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
## 🎁 Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting
* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
* Direct [OpenID and TOTP integration](.docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md") for more secure login
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with location pins and GPX tracks
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support for user themes
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content
* Customizable UI (sidebar buttons, user-defined widgets, ...)
* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along with a [Grafana Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json)
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
## ⚠️ Why TriliumNext?
[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
### Migrating from Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
## 📖 Documentation
We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation.
Below are some quick links for your convenience to navigate the documentation:
- [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
- [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
Until we finish reorganizing the documentation, you may also want to [browse the old documentation](https://triliumnext.github.io/Docs).
## 💬 Discuss with us
Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have!
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
- The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
## 🏗 Installation
### Windows / MacOS
Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
### Linux
If your distribution is listed in the table below, use your distribution's package.
[![Packaging status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
### Browser (any OS)
If you use a server installation (see below), you can directly access the web interface (which is almost identical to the desktop app).
Currently only the latest versions of Chrome & Firefox are supported (and tested).
### Mobile
To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below).
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support.
### Server
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
## 💻 Contribute
### Code
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm run server:start
```
### Documentation
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm nx run edit-docs:edit-docs
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
```
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
### Developer Documentation
Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
## 👏 Shoutouts
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. TriliumNext Notes would not be the same without it.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
## 🤝 Support
Support for the TriliumNext organization will be possible in the near future. For now, you can:
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list)
- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
## 🔑 License
Copyright 2017-2025 zadam, Elian Doran, and other contributors
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

@ -25,16 +25,15 @@ stats() {
# Print the number of existing strings on the JSON files for each locale # Print the number of existing strings on the JSON files for each locale
s=$(number_of_keys "${paths[0]}/en/server.json") s=$(number_of_keys "${paths[0]}/en/server.json")
c=$(number_of_keys "${paths[1]}/en/translation.json") c=$(number_of_keys "${paths[1]}/en/translation.json")
echo "| locale | server strings | client strings |" echo "| locale |server strings |client strings |"
echo "|--------|----------------|----------------|" echo "|--------|---------------|---------------|"
echo "| en | ${s} | ${c} |" echo "| en | ${s} | ${c} |"
echo "|--------|----------------|----------------|"
for locale in "${locales[@]}"; do for locale in "${locales[@]}"; do
s=$(number_of_keys "${paths[0]}/${locale}/server.json") s=$(number_of_keys "${paths[0]}/${locale}/server.json")
c=$(number_of_keys "${paths[1]}/${locale}/translation.json") c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
n1=$(((8 - ${#locale}) / 2)) n1=$(((8 - ${#locale}) / 2))
n2=$((n1 == 1 ? n1 + 1 : n1)) n2=$((n1 == 1 ? n1 + 1 : n1))
echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |" echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |"
done done
} }
@ -79,10 +78,7 @@ file_path="$(
cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit
pwd -P pwd -P
)" )"
paths=( paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/")
"${file_path}/../../apps/server/src/assets/translations/"
"${file_path}/../../apps/client/src/translations/"
)
locales=(cn de es fr pt_br ro tw) locales=(cn de es fr pt_br ro tw)
if [ $# -eq 1 ]; then if [ $# -eq 1 ]; then

@ -44,6 +44,7 @@ export default tseslint.config(
"dist/*", "dist/*",
"docs/*", "docs/*",
"demo/*", "demo/*",
"libraries/*",
"src/public/app-dist/*", "src/public/app-dist/*",
"src/public/app/doc_notes/*" "src/public/app/doc_notes/*"
] ]

@ -38,6 +38,7 @@ export default [
"dist/*", "dist/*",
"docs/*", "docs/*",
"demo/*", "demo/*",
"libraries/*",
// TriliumNextTODO: check if we want to format packages here as well - for now skipping it // TriliumNextTODO: check if we want to format packages here as well - for now skipping it
"packages/*", "packages/*",
"src/public/app-dist/*", "src/public/app-dist/*",

@ -35,13 +35,13 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js" "chore:generate-openapi": "tsx bin/generate-openapi.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.53.1", "@playwright/test": "1.52.0",
"@stylistic/eslint-plugin": "4.4.1", "@stylistic/eslint-plugin": "4.4.1",
"@types/express": "5.0.3", "@types/express": "5.0.1",
"@types/node": "22.15.32", "@types/node": "22.15.30",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.2",
"eslint": "9.29.0", "eslint": "9.28.0",
"eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25", "esm": "3.2.25",
"jsdoc": "4.0.4", "jsdoc": "4.0.4",

@ -1,4 +0,0 @@
# The development license key for premium CKEditor features.
# Note: This key must only be used for the Trilium Notes project.
# Expires on: 2025-09-13
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w

@ -1 +0,0 @@
VITE_CKEDITOR_ENABLE_INSPECTOR=false

@ -1,6 +1,6 @@
{ {
"name": "@triliumnext/client", "name": "@triliumnext/client",
"version": "0.95.0", "version": "0.94.1",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true, "private": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
@ -10,7 +10,7 @@
"url": "https://github.com/TriliumNext/Notes" "url": "https://github.com/TriliumNext/Notes"
}, },
"dependencies": { "dependencies": {
"@eslint/js": "9.29.0", "@eslint/js": "9.28.0",
"@excalidraw/excalidraw": "0.18.0", "@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.17", "@fullcalendar/core": "6.1.17",
"@fullcalendar/daygrid": "6.1.17", "@fullcalendar/daygrid": "6.1.17",
@ -25,9 +25,8 @@
"@triliumnext/codemirror": "workspace:*", "@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*", "@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*", "@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
"bootstrap": "5.3.7", "bootstrap": "5.3.6",
"boxicons": "2.1.4", "boxicons": "2.1.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
@ -48,10 +47,11 @@
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "15.0.12", "marked": "15.0.12",
"mermaid": "11.6.0", "mermaid": "11.6.0",
"mind-elixir": "4.6.1", "mind-elixir": "4.6.0",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
"preact": "10.26.9", "react": "19.1.0",
"react-dom": "19.1.0",
"split.js": "1.6.5", "split.js": "1.6.5",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"vanilla-js-wheel-zoom": "9.0.4" "vanilla-js-wheel-zoom": "9.0.4"
@ -63,22 +63,14 @@
"@types/leaflet": "1.9.18", "@types/leaflet": "1.9.18",
"@types/leaflet-gpx": "1.3.7", "@types/leaflet-gpx": "1.3.7",
"@types/mark.js": "8.11.12", "@types/mark.js": "8.11.12",
"@types/react": "19.1.6",
"@types/react-dom": "19.1.6",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"happy-dom": "18.0.1", "happy-dom": "17.6.3",
"script-loader": "0.7.2", "script-loader": "0.7.2",
"vite-plugin-static-copy": "3.0.2" "vite-plugin-static-copy": "3.0.0"
}, },
"nx": { "nx": {
"name": "client", "name": "client"
"targets": {
"serve": {
"dependsOn": [
"^build"
]
},
"circular-deps": {
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
}
}
} }
} }

@ -0,0 +1,424 @@
/*
* Remove template code below
*/
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
}
svg {
display: block;
vertical-align: middle;
}
svg {
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
gap: 4rem;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 47%, 50%, 1);
}
.list-item-link:hover > span {
}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {
}
.button-pill {
padding: 1.5rem 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
#nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
#nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-console-jetbrains {
margin-top: 2rem;
}
#nx-console-jetbrains:hover {
background-color: rgba(255, 49, 140, 1);
}
#nx-console-jetbrains svg {
color: rgba(255, 49, 140, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

@ -0,0 +1,21 @@
import { AppElement } from './app.element';
describe('AppElement', () => {
let app: AppElement;
beforeEach(() => {
app = new AppElement();
});
it('should create successfully', () => {
expect(app).toBeTruthy();
});
it('should have a greeting', () => {
app.connectedCallback();
expect(app.querySelector('h1').innerHTML).toContain(
'Welcome @triliumnext/client'
);
});
});

@ -0,0 +1,409 @@
import './app.element.css';
export class AppElement extends HTMLElement {
public static observedAttributes = [
];
connectedCallback() {
const title = '@triliumnext/client';
this.innerHTML = `
<div class="wrapper">
<div class="container">
<!-- WELCOME -->
<div id="welcome">
<h1>
<span> Hello there, </span>
Welcome ${title} 👋
</h1>
</div>
<!-- HERO -->
<div id="hero" class="rounded">
<div class="text-container">
<h2>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
<span>You&apos;re up and running</span>
</h2>
<a href="#commands"> What&apos;s next? </a>
</div>
<div class="logo-container">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
/>
</svg>
</div>
</div>
<!-- MIDDLE CONTENT -->
<div id="middle-content">
<div id="learning-materials" class="rounded shadow">
<h2>Learning materials</h2>
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<span>
Documentation
<span> Everything is in there </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
/>
</svg>
<span>
Blog
<span> Changelog, features & events </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>YouTube</title>
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
/>
</svg>
<span>
YouTube channel
<span> Nx Show, talks & tutorials </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
<span>
Interactive tutorials
<span> Create an app, step-by-step </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 14l9-5-9-5-9 5 9 5z" />
<path
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
/>
</svg>
<span>
Video courses
<span> Nx custom courses </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
<div id="other-links">
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Visual Studio Code</title>
<path
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
/>
</svg>
<span>
Install Nx Console for VSCode
<span>The official VSCode extension for Nx.</span>
</span>
</a>
<a
id="nx-console-jetbrains"
class="button-pill rounded shadow"
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
target="_blank"
rel="noreferrer"
>
<svg
height="48"
width="48"
viewBox="20 20 60 60"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m22.5 22.5h60v60h-60z" />
<g fill="#fff">
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
</g>
</svg>
<span>
Install Nx Console for JetBrains
<span>
Available for WebStorm, Intellij IDEA Ultimate and more!
</span>
</span>
</a>
<div id="nx-cloud" class="rounded shadow">
<div>
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
</svg>
<h2>
Nx Cloud
<span>
Enable faster CI & better DX
</span>
</h2>
</div>
<p>
You can activate distributed tasks executions and caching by
running:
</p>
<pre>nx connect</pre>
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
</div>
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
<span>
Nx is open source
<span> Love Nx? Give us a star! </span>
</span>
</a>
</div>
</div>
<!-- COMMANDS -->
<div id="commands" class="rounded shadow">
<h2>Next steps</h2>
<p>Here are some things you can do with Nx:</p>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Add UI library
</summary>
<pre><span># Generate UI lib</span>
nx g @nx/angular:lib ui
<span># Add a component</span>
nx g @nx/angular:component ui/src/lib/button</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
View interactive project graph
</summary>
<pre>nx graph</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Run affected commands
</summary>
<pre><span># see what&apos;s been affected by changes</span>
nx affected:graph
<span># run tests for current changes</span>
nx affected:test
<span># run e2e tests for current changes</span>
nx affected:e2e</pre>
</details>
</div>
<p id="love">
Carefully crafted with
<svg
fill="currentColor"
stroke="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</p>
</div>
</div>
`;
}
}
customElements.define('triliumnext-root', AppElement);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Client</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<triliumnext-root></triliumnext-root>
</body>
</html>

@ -0,0 +1 @@
import './app/app.element';

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

@ -1,4 +1,5 @@
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import bundleService from "../services/bundle.js";
import RootCommandExecutor from "./root_command_executor.js"; import RootCommandExecutor from "./root_command_executor.js";
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
import options from "../services/options.js"; import options from "../services/options.js";
@ -27,7 +28,6 @@ import type { NativeImage, TouchBar } from "electron";
import TouchBarComponent from "./touch_bar.js"; import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror"; import type CodeMirror from "@triliumnext/codemirror";
import { StartupChecks } from "./startup_checks.js";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; getRootWidget: (appContext: AppContext) => RootWidget;
@ -128,7 +128,6 @@ export type CommandMappings = {
openAboutDialog: CommandData; openAboutDialog: CommandData;
hideFloatingButtons: {}; hideFloatingButtons: {};
hideLeftPane: CommandData; hideLeftPane: CommandData;
showCpuArchWarning: CommandData;
showLeftPane: CommandData; showLeftPane: CommandData;
hoistNote: CommandData & { noteId: string }; hoistNote: CommandData & { noteId: string };
leaveProtectedSession: CommandData; leaveProtectedSession: CommandData;
@ -280,7 +279,6 @@ export type CommandMappings = {
buildIcon(name: string): NativeImage; buildIcon(name: string): NativeImage;
}; };
refreshTouchBar: CommandData; refreshTouchBar: CommandData;
reloadTextEditor: CommandData;
}; };
type EventMappings = { type EventMappings = {
@ -469,21 +467,13 @@ export class AppContext extends Component {
this.tabManager.loadTabs(); this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000); setTimeout(() => bundleService.executeStartupBundles(), 2000);
} }
initComponents() { initComponents() {
this.tabManager = new TabManager(); this.tabManager = new TabManager();
this.components = [ this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()];
this.tabManager,
new RootCommandExecutor(),
new Entrypoints(),
new MainTreeExecutors(),
new ShortcutComponent(),
new StartupChecks()
];
if (utils.isMobile()) { if (utils.isMobile()) {
this.components.push(new MobileScreenSwitcherExecutor()); this.components.push(new MobileScreenSwitcherExecutor());

@ -12,7 +12,6 @@ import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror"; import type CodeMirror from "@triliumnext/codemirror";
import { closeActiveDialog } from "../services/dialog.js";
export interface SetNoteOpts { export interface SetNoteOpts {
triggerSwitchEvent?: unknown; triggerSwitchEvent?: unknown;
@ -84,7 +83,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
closeActiveDialog(); utils.closeActiveDialog();
this.notePath = resolvedNotePath; this.notePath = resolvedNotePath;
this.viewScope = opts.viewScope; this.viewScope = opts.viewScope;

@ -1,26 +0,0 @@
import server from "../services/server";
import Component from "./component";
// TODO: Deduplicate.
interface CpuArchResponse {
isCpuArchMismatch: boolean;
}
export class StartupChecks extends Component {
constructor() {
super();
this.checkCpuArchMismatch();
}
async checkCpuArchMismatch() {
try {
const response = await server.get("system-checks") as CpuArchResponse;
if (response.isCpuArchMismatch) {
this.triggerCommand("showCpuArchWarning", {});
}
} catch (error) {
console.warn("Could not check CPU arch status:", error);
}
}
}

@ -8,7 +8,6 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import { t } from "./services/i18n.js"; import { t } from "./services/i18n.js";
import options from "./services/options.js"; import options from "./services/options.js";
import server from "./services/server.js";
import type ElectronRemote from "@electron/remote"; import type ElectronRemote from "@electron/remote";
import type Electron from "electron"; import type Electron from "electron";
import "./stylesheets/bootstrap.scss"; import "./stylesheets/bootstrap.scss";

@ -1,6 +1,7 @@
import server from "../services/server.js"; import server from "../services/server.js";
import noteAttributeCache from "../services/note_attribute_cache.js"; import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js"; import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js"; import type { Froca } from "../services/froca-interface.js";
@ -409,8 +410,8 @@ class FNote {
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
notePath: path, notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived), isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"), isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden") isHidden: path.includes("_hidden")
})); }));
@ -981,7 +982,7 @@ class FNote {
continue; continue;
} }
const parentNote = this.froca.notes[parentNoteId]; const parentNote = froca.notes[parentNoteId];
if (!parentNote || parentNote.type === "search") { if (!parentNote || parentNote.type === "search") {
continue; continue;

@ -21,7 +21,6 @@ import ConfirmDialog from "../widgets/dialogs/confirm.js";
import RevisionsDialog from "../widgets/dialogs/revisions.js"; import RevisionsDialog from "../widgets/dialogs/revisions.js";
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
import InfoDialog from "../widgets/dialogs/info.js"; import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
export function applyModals(rootContainer: RootContainer) { export function applyModals(rootContainer: RootContainer) {
rootContainer rootContainer
@ -46,5 +45,4 @@ export function applyModals(rootContainer: RootContainer) {
.child(new InfoDialog()) .child(new InfoDialog())
.child(new ConfirmDialog()) .child(new ConfirmDialog())
.child(new PromptDialog()) .child(new PromptDialog())
.child(new IncorrectCpuArchDialog())
} }

@ -0,0 +1,204 @@
// Source: https://github.com/codemirror/codemirror5/pull/7080/files
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function (mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function (CodeMirror) {
"use strict";
CodeMirror.defineMode("hcl", function (config) {
var indentUnit = config.indentUnit;
var keywords = {
"resource": true,
"variable": true,
"output": true,
"module": true,
"provider": true,
"data": true,
"locals": true,
"terraform": true,
"if": true,
"else": true,
"for": true,
"foreach": true,
"in": true,
"true": true,
"false": true,
"null": true,
};
var atoms = {
"true": true,
"false": true,
"null": true,
};
var isOperatorChar = /[+\-*&^%:=<>!|\/]/;
var curPunc;
function tokenBase(stream, state) {
var ch = stream.next();
if (ch == '"' || ch == "'" || ch == "`") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
}
if (/[\d\.]/.test(ch)) {
if (ch == ".") {
stream.match(/^[0-9_]+([eE][\-+]?[0-9_]+)?/);
} else {
stream.match(/^[0-9_]*\.?[0-9_]*([eE][\-+]?[0-9_]+)?/);
}
return "number";
}
if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
curPunc = ch;
return null;
}
if (ch == "/") {
if (stream.eat("*")) {
state.tokenize = tokenComment;
return tokenComment(stream, state);
}
if (stream.eat("/")) {
stream.skipToEnd();
return "comment";
}
}
if (isOperatorChar.test(ch)) {
stream.eatWhile(isOperatorChar);
return "operator";
}
stream.eatWhile(/[\w\$_\xa1-\uffff]/);
var cur = stream.current();
if (keywords.propertyIsEnumerable(cur)) {
return "keyword";
}
if (atoms.propertyIsEnumerable(cur)) return "atom";
return "variable";
}
function tokenString(quote) {
return function (stream, state) {
var escaped = false,
next,
end = false;
while ((next = stream.next()) != null) {
if (next == quote && !escaped) {
end = true;
break;
}
escaped = !escaped && quote != "`" && next == "\\";
}
if (end || !(escaped || quote == "`"))
state.tokenize = tokenBase;
return "string";
};
}
function tokenComment(stream, state) {
var maybeEnd = false,
ch;
while (ch = stream.next()) {
if (ch == "/" && maybeEnd) {
state.tokenize = tokenBase;
break;
}
maybeEnd = (ch == "*");
}
return "comment";
}
function Context(indented, column, type, align, prev) {
this.indented = indented;
this.column = column;
this.type = type;
this.align = align;
this.prev = prev;
}
function pushContext(state, col, type) {
return state.context = new Context(state.indented, col, type, null, state.context);
}
function popContext(state) {
if (!state.context.prev) return;
var t = state.context.type;
if (t == ")" || t == "]" || t == "}")
state.indented = state.context.indented;
return state.context = state.context.prev;
}
// Interface
return {
startState: function (basecolumn) {
return {
tokenize: null,
context: new Context((basecolumn || 0) - indentUnit, 0, "top", false),
indented: 0,
startOfLine: true
};
},
token: function (stream, state) {
var ctx = state.context;
if (stream.sol()) {
if (ctx.align == null) ctx.align = false;
state.indented = stream.indentation();
state.startOfLine = true;
}
if (stream.eatSpace()) return null;
curPunc = null;
var style = (state.tokenize || tokenBase)(stream, state);
if (style == "comment") return style;
if (ctx.align == null) ctx.align = true;
if (curPunc == "{") pushContext(state, stream.column(), "}");
else if (curPunc == "[") pushContext(state, stream.column(), "]");
else if (curPunc == "(") pushContext(state, stream.column(), ")");
else if (curPunc == "}" && ctx.type == "}") popContext(state);
else if (curPunc == ctx.type) popContext(state);
state.startOfLine = false;
return style;
},
indent: function (state, textAfter) {
if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass;
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
if (firstChar == "#" || firstChar == ";") return 0;
if (stream.sol()) {
if (ctx.type == "case" && /^(?:case|default)\b/.test(textAfter)) {
state.context.type = "}";
return ctx.indented;
}
var closing = firstChar == ctx.type;
if (ctx.align) return ctx.column + (closing ? 0 : 1);
else return ctx.indented + (closing ? 0 : indentUnit);
}
},
electricChars: "{}):",
closeBrackets: "()[]{}''\"\"``",
fold: "brace",
blockCommentStart: "/*",
blockCommentEnd: "*/",
lineComment: "//"
};
});
CodeMirror.defineMIME("text/x-hcl", "hcl");
CodeMirror.modeInfo.push({
ext: [ "hcl " ],
mime: "text/x-hcl",
mode: "hcl",
name: "Terraform (HCL)"
});
});

@ -194,15 +194,14 @@ class ContextMenu {
return false; return false;
}); });
$item.on("mouseup", (e) => { if (!this.isMobile) {
// Prevent submenu from failing to expand on mobile $item.on("mouseup", (e) =>{
if (!this.isMobile || !("items" in item && item.items)) {
e.stopPropagation(); e.stopPropagation();
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below. // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
this.hide(); this.hide();
return false; return false;
} });
}); }
if ("enabled" in item && item.enabled !== undefined && !item.enabled) { if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
$item.addClass("disabled"); $item.addClass("disabled");

@ -23,4 +23,4 @@ export interface EntityChange {
instanceId?: string | null; instanceId?: string | null;
} }
export type EntityType = "notes" | "branches" | "attributes" | "note_reordering" | "revisions" | "options" | "attachments" | "blobs" | "etapi_tokens"; export type EntityType = "notes" | "branches" | "attributes" | "note_reordering" | "revisions" | "options" | "attachments" | "blobs" | "etapi_tokens" | "note_embeddings";

@ -1,6 +1,6 @@
import ScriptContext from "./script_context.js"; import ScriptContext from "./script_context.js";
import server from "./server.js"; import server from "./server.js";
import toastService, { showError } from "./toast.js"; import toastService from "./toast.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
@ -37,9 +37,7 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
} catch (e: any) { } catch (e: any) {
const note = await froca.getNote(bundle.noteId); const note = await froca.getNote(bundle.noteId);
const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`; toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`);
showError(message);
logError(message);
} }
} }

@ -4,7 +4,7 @@ import froca from "./froca.js";
import linkService from "./link.js"; import linkService from "./link.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import { throwError } from "./ws.js"; import toast from "./toast.js";
let clipboardBranchIds: string[] = []; let clipboardBranchIds: string[] = [];
let clipboardMode: string | null = null; let clipboardMode: string | null = null;
@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) {
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
} else { } else {
throwError(`Unrecognized clipboard mode=${clipboardMode}`); toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
} }
} }
@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) {
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
} else { } else {
throwError(`Unrecognized clipboard mode=${clipboardMode}`); toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`);
} }
} }
@ -109,6 +109,39 @@ function isClipboardEmpty() {
return clipboardBranchIds.length === 0; return clipboardBranchIds.length === 0;
} }
export function copyText(text: string) {
if (!text) {
return;
}
let succeeded = false;
try {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
succeeded = true;
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
succeeded = document.execCommand('copy');
document.body.removeChild(textArea);
}
} catch (e) {
console.warn(e);
succeeded = false;
}
if (succeeded) {
toast.showMessage(t("clipboard.copy_success"));
} else {
toast.showError(t("clipboard.copy_failed"));
}
}
export default { export default {
pasteAfter, pasteAfter,
pasteInto, pasteInto,

@ -1,37 +0,0 @@
export function copyText(text: string) {
if (!text) {
return;
}
try {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return true;
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
}
} catch (e) {
console.warn(e);
return false;
}
}
export async function copyTextWithToast(text: string) {
const t = (await import("./i18n.js")).t;
const toast = (await import("./toast.js")).default;
if (copyText(text)) {
toast.showMessage(t("clipboard.copy_success"));
} else {
toast.showError(t("clipboard.copy_failed"));
}
}

@ -1,41 +1,6 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0]).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
export function closeActiveDialog() {
if (glob.activeDialog) {
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
glob.activeDialog = null;
}
}
async function info(message: string) { async function info(message: string) {
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));

@ -1,29 +0,0 @@
let $lastFocusedElement: JQuery<HTMLElement> | null;
// perhaps there should be saved focused element per tab?
export function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
export function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
// the bug manifests itself in resetting the cursor position to the first character - jumping above
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}

@ -245,10 +245,6 @@ class FrocaImpl implements Froca {
} }
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> { async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
if (noteIds.length === 0) {
return [];
}
noteIds = Array.from(new Set(noteIds)); // make unique noteIds = Array.from(new Set(noteIds)); // make unique
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]); const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);

@ -35,7 +35,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
loadResults.addOption(attributeEntity.name); loadResults.addOption(attributeEntity.name);
} else if (ec.entityName === "attachments") { } else if (ec.entityName === "attachments") {
processAttachment(loadResults, ec); processAttachment(loadResults, ec);
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens" || ec.entityName === "note_embeddings") {
// NOOP - these entities are handled at the backend level and don't require frontend processing // NOOP - these entities are handled at the backend level and don't require frontend processing
} else { } else {
throw new Error(`Unknown entityName '${ec.entityName}'`); throw new Error(`Unknown entityName '${ec.entityName}'`);

@ -26,18 +26,12 @@ function setupGlobs() {
window.onerror = function (msg, url, lineNo, columnNo, error) { window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = String(msg).toLowerCase(); const string = String(msg).toLowerCase();
let errorObjectString = "";
try {
errorObjectString = JSON.stringify(error);
} catch (e: any) {
errorObjectString = e.toString();
}
let message = "Uncaught error: "; let message = "Uncaught error: ";
if (string.includes("script error")) { if (string.includes("script error")) {
message += "No details available"; message += "No details available";
} else { } else {
message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${errorObjectString}`, `Stack: ${error && error.stack}`].join(", "); message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${JSON.stringify(error)}`, `Stack: ${error && error.stack}`].join(", ");
} }
ws.logError(message); ws.logError(message);

@ -1,7 +1,6 @@
import { LOCALES } from "@triliumnext/commons"; import { LOCALES } from "@triliumnext/commons";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { join } from "path"; import { join } from "path";
import { describe, expect, it } from "vitest";
describe("i18n", () => { describe("i18n", () => {
it("translations are valid JSON", () => { it("translations are valid JSON", () => {

@ -1,5 +1,5 @@
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toastService, { showError } from "./toast.js"; import toastService from "./toast.js";
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) { function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
try { try {
@ -11,9 +11,7 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
if (success) { if (success) {
toastService.showMessage(t("image.copied-to-clipboard")); toastService.showMessage(t("image.copied-to-clipboard"));
} else { } else {
const message = t("image.cannot-copy"); toastService.showAndLogError(t("image.cannot-copy"));
showError(message);
logError(message);
} }
} finally { } finally {
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();

@ -22,11 +22,6 @@ describe("Link", () => {
expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" }); expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" });
}); });
it("parses notePath with extraWindow", () => {
const output = parseNavigationStateFromUrl(`127.0.0.1:8080/?extraWindow=1#root/QZGqKB7wVZF8?ntxId=0XPvXG`);
expect(output).toMatchObject({ notePath: "root/QZGqKB7wVZF8", noteId: "QZGqKB7wVZF8" });
});
it("ignores external URL with internal hash anchor", () => { it("ignores external URL with internal hash anchor", () => {
const output = parseNavigationStateFromUrl(`https://en.wikipedia.org/wiki/Bearded_Collie#Health`); const output = parseNavigationStateFromUrl(`https://en.wikipedia.org/wiki/Bearded_Collie#Health`);
expect(output).toMatchObject({}); expect(output).toMatchObject({});

@ -218,7 +218,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
} }
// Exclude external links that contain # // Exclude external links that contain #
if (hashIdx !== 0 && !url.includes("/#root") && !url.includes("/#?searchString") && !url.includes("/?extraWindow")) { if (hashIdx !== 0 && !url.includes("/#root") && !url.includes("/#?searchString")) {
return {}; return {};
} }

@ -44,7 +44,18 @@ interface OptionRow {}
interface NoteReorderingRow {} interface NoteReorderingRow {}
interface NoteEmbeddingRow {
embedId: string;
noteId: string;
providerId: string;
modelId: string;
dimension: number;
version: number;
dateCreated: string;
utcDateCreated: string;
dateModified: string;
utcDateModified: string;
}
type EntityRowMappings = { type EntityRowMappings = {
notes: NoteRow; notes: NoteRow;
@ -53,6 +64,7 @@ type EntityRowMappings = {
options: OptionRow; options: OptionRow;
revisions: RevisionRow; revisions: RevisionRow;
note_reordering: NoteReorderingRow; note_reordering: NoteReorderingRow;
note_embeddings: NoteEmbeddingRow;
}; };
export type EntityRowNames = keyof EntityRowMappings; export type EntityRowNames = keyof EntityRowMappings;

@ -289,11 +289,13 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
} }
if (suggestion.action === "create-note") { if (suggestion.action === "create-note") {
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType(); const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
if (!success) { if (!success) {
return; return;
} }
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
title: suggestion.noteTitle, title: suggestion.noteTitle,
activate: false, activate: false,
type: noteType, type: noteType,

@ -116,7 +116,7 @@ async function chooseNoteType() {
} }
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
const { success, noteType, templateNoteId, notePath } = await chooseNoteType(); const { success, noteType, templateNoteId } = await chooseNoteType();
if (!success) { if (!success) {
return; return;
@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN
options.type = noteType; options.type = noteType;
options.templateNoteId = templateNoteId; options.templateNoteId = templateNoteId;
return await createNote(notePath || parentNotePath, options); return await createNote(parentNotePath, options);
} }
/* If the first element is heading, parse it out and use it as a new heading. */ /* If the first element is heading, parse it out and use it as a new heading. */

@ -4,8 +4,6 @@ import { t } from "./i18n.js";
import type { MenuItem } from "../menus/context_menu.js"; import type { MenuItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js"; import type { TreeCommandNames } from "../menus/tree_context_menu.js";
const SEPARATOR = { title: "----" };
async function getNoteTypeItems(command?: TreeCommandNames) { async function getNoteTypeItems(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [ const items: MenuItem<TreeCommandNames>[] = [
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" }, { title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
@ -20,59 +18,25 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" }, { title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }, { title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" }, { title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
...await getBuiltInTemplates(command),
...await getUserTemplates(command)
]; ];
return items;
}
async function getUserTemplates(command?: TreeCommandNames) {
const templateNoteIds = await server.get<string[]>("search-templates"); const templateNoteIds = await server.get<string[]>("search-templates");
const templateNotes = await froca.getNotes(templateNoteIds); const templateNotes = await froca.getNotes(templateNoteIds);
if (templateNotes.length === 0) {
return [];
}
const items: MenuItem<TreeCommandNames>[] = [
SEPARATOR
];
for (const templateNote of templateNotes) {
items.push({
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
}
return items;
}
async function getBuiltInTemplates(command?: TreeCommandNames) {
const templatesRoot = await froca.getNote("_templates");
if (!templatesRoot) {
console.warn("Unable to find template root.");
return [];
}
const childNotes = await templatesRoot.getChildNotes(); if (templateNotes.length > 0) {
if (childNotes.length === 0) { items.push({ title: "----" });
return [];
for (const templateNote of templateNotes) {
items.push({
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
}
} }
const items: MenuItem<TreeCommandNames>[] = [
SEPARATOR
];
for (const templateNote of childNotes) {
items.push({
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
});
}
return items; return items;
} }

@ -1,5 +1,4 @@
import server from "./server.js"; import server from "./server.js";
import { isShare } from "./utils.js";
type OptionValue = number | string; type OptionValue = number | string;
@ -8,11 +7,7 @@ class Options {
private arr!: Record<string, OptionValue>; private arr!: Record<string, OptionValue>;
constructor() { constructor() {
if (!isShare) { this.initializedPromise = server.get<Record<string, OptionValue>>("options").then((data) => this.load(data));
this.initializedPromise = server.get<Record<string, OptionValue>>("options").then((data) => this.load(data));
} else {
this.initializedPromise = Promise.resolve();
}
} }
load(arr: Record<string, OptionValue>) { load(arr: Record<string, OptionValue>) {

@ -1,4 +1,4 @@
import type { Entity } from "./frontend_script_api.js"; import FrontendScriptApi, { type Entity } from "./frontend_script_api.js";
import utils from "./utils.js"; import utils from "./utils.js";
import froca from "./froca.js"; import froca from "./froca.js";
@ -14,8 +14,6 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn
throw new Error(`Could not find start note ${startNoteId}.`); throw new Error(`Could not find start note ${startNoteId}.`);
} }
const FrontendScriptApi = (await import("./frontend_script_api.js")).default;
return { return {
modules: modules, modules: modules,
notes: utils.toObject(allNotes, (note) => [note.noteId, note]), notes: utils.toObject(allNotes, (note) => [note.noteId, note]),

@ -1,4 +1,4 @@
import utils, { isShare } from "./utils.js"; import utils from "./utils.js";
import ValidationError from "./validation_error.js"; import ValidationError from "./validation_error.js";
type Headers = Record<string, string | null | undefined>; type Headers = Record<string, string | null | undefined>;
@ -28,10 +28,6 @@ export interface StandardResponse {
} }
async function getHeaders(headers?: Headers) { async function getHeaders(headers?: Headers) {
if (isShare) {
return {};
}
const appContext = (await import("../components/app_context.js")).default; const appContext = (await import("../components/app_context.js")).default;
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null; const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
@ -276,8 +272,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
} else { } else {
const title = `${statusCode} ${method} ${url}`; const title = `${statusCode} ${method} ${url}`;
toastService.showErrorTitleAndMessage(title, messageStr); toastService.showErrorTitleAndMessage(title, messageStr);
const { throwError } = await import("./ws.js"); toastService.throwError(`${title} - ${message}`);
throwError(`${title} - ${message}`);
} }
} }

@ -2,9 +2,7 @@ import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type Auto
import mime_types from "./mime_types.js"; import mime_types from "./mime_types.js";
import options from "./options.js"; import options from "./options.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import { copyText, copyTextWithToast } from "./clipboard_ext.js"; import { copyText } from "./clipboard.js";
import { isShare } from "./utils.js";
import { MimeType } from "@triliumnext/commons";
let highlightingLoaded = false; let highlightingLoaded = false;
@ -16,6 +14,9 @@ let highlightingLoaded = false;
*/ */
export async function formatCodeBlocks($container: JQuery<HTMLElement>) { export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
const syntaxHighlightingEnabled = isSyntaxHighlightEnabled(); const syntaxHighlightingEnabled = isSyntaxHighlightEnabled();
if (syntaxHighlightingEnabled) {
await ensureMimeTypesForHighlighting();
}
const codeBlocks = $container.find("pre code"); const codeBlocks = $container.find("pre code");
for (const codeBlock of codeBlocks) { for (const codeBlock of codeBlocks) {
@ -36,13 +37,7 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
const $copyButton = $("<button>") const $copyButton = $("<button>")
.addClass("bx component icon-action tn-tool-button bx-copy copy-button") .addClass("bx component icon-action tn-tool-button bx-copy copy-button")
.attr("title", t("code_block.copy_title")) .attr("title", t("code_block.copy_title"))
.on("click", () => { .on("click", () => copyText($codeBlock.text()));
if (!isShare) {
copyTextWithToast($codeBlock.text());
} else {
copyText($codeBlock.text());
}
});
$codeBlock.parent().append($copyButton); $codeBlock.parent().append($copyButton);
} }
@ -54,11 +49,11 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
const text = $codeBlock.text(); const text = $codeBlock.text();
let highlightedText: HighlightResult | AutoHighlightResult | null = null; let highlightedText: HighlightResult | AutoHighlightResult | null = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO && !isShare) { if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
await ensureMimeTypesForHighlighting(); await ensureMimeTypesForHighlighting();
highlightedText = highlightAuto(text); highlightedText = highlightAuto(text);
} else if (normalizedMimeType) { } else if (normalizedMimeType) {
await ensureMimeTypesForHighlighting(normalizedMimeType); await ensureMimeTypesForHighlighting();
highlightedText = highlight(text, { language: normalizedMimeType }); highlightedText = highlight(text, { language: normalizedMimeType });
} }
@ -67,7 +62,7 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
} }
} }
export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) { export async function ensureMimeTypesForHighlighting() {
if (highlightingLoaded) { if (highlightingLoaded) {
return; return;
} }
@ -77,20 +72,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
loadHighlightingTheme(currentThemeName); loadHighlightingTheme(currentThemeName);
// Load mime types. // Load mime types.
let mimeTypes: MimeType[]; const mimeTypes = mime_types.getMimeTypes();
if (mimeTypeHint) {
mimeTypes = [
{
title: mimeTypeHint,
enabled: true,
mime: mimeTypeHint.replace("-", "/")
}
]
} else {
mimeTypes = mime_types.getMimeTypes();
}
await ensureMimeTypes(mimeTypes); await ensureMimeTypes(mimeTypes);
highlightingLoaded = true; highlightingLoaded = true;
@ -114,12 +96,8 @@ export function loadHighlightingTheme(themeName: string) {
* @returns whether syntax highlighting should be enabled for code blocks. * @returns whether syntax highlighting should be enabled for code blocks.
*/ */
export function isSyntaxHighlightEnabled() { export function isSyntaxHighlightEnabled() {
if (!isShare) { const theme = options.get("codeBlockTheme");
const theme = options.get("codeBlockTheme"); return !!theme && theme !== "none";
return !!theme && theme !== "none";
} else {
return true;
}
} }
/** /**

@ -78,7 +78,13 @@ function showMessage(message: string, delay = 2000) {
}); });
} }
export function showError(message: string, delay = 10000) { function showAndLogError(message: string, delay = 10000) {
showError(message, delay);
ws.logError(message);
}
function showError(message: string, delay = 10000) {
console.log(utils.now(), "error: ", message); console.log(utils.now(), "error: ", message);
toast({ toast({
@ -102,10 +108,18 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000)
}); });
} }
function throwError(message: string) {
ws.logError(message);
throw new Error(message);
}
export default { export default {
showMessage, showMessage,
showError, showError,
showErrorTitleAndMessage, showErrorTitleAndMessage,
showAndLogError,
throwError,
showPersistent, showPersistent,
closePersistent closePersistent
}; };

@ -1,10 +1,9 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Modal } from "bootstrap";
import type { ViewScope } from "./link.js"; import type { ViewScope } from "./link.js";
const SVG_MIME = "image/svg+xml"; const SVG_MIME = "image/svg+xml";
export const isShare = !window.glob;
function reloadFrontendApp(reason?: string) { function reloadFrontendApp(reason?: string) {
if (reason) { if (reason) {
logInfo(`Frontend app reload: ${reason}`); logInfo(`Frontend app reload: ${reason}`);
@ -274,6 +273,71 @@ function getMimeTypeClass(mime: string) {
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
} }
function closeActiveDialog() {
if (glob.activeDialog) {
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
glob.activeDialog = null;
}
}
let $lastFocusedElement: JQuery<HTMLElement> | null;
// perhaps there should be saved focused element per tab?
function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
// the bug manifests itself in resetting the cursor position to the first character - jumping above
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0]).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
// TODO: Fix once keyboard_actions is ported.
// @ts-ignore
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
function isHtmlEmpty(html: string) { function isHtmlEmpty(html: string) {
if (!html) { if (!html) {
return true; return true;
@ -759,6 +823,10 @@ export default {
setCookie, setCookie,
getNoteTypeClass, getNoteTypeClass,
getMimeTypeClass, getMimeTypeClass,
closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
isHtmlEmpty, isHtmlEmpty,
clearBrowserCache, clearBrowserCache,
copySelectionToClipboard, copySelectionToClipboard,

@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastPingTs: number; let lastPingTs: number;
let frontendUpdateDataQueue: EntityChange[] = []; let frontendUpdateDataQueue: EntityChange[] = [];
export function logError(message: string) { function logError(message: string) {
console.error(utils.now(), message); // needs to be separate from .trace() console.error(utils.now(), message); // needs to be separate from .trace()
if (ws && ws.readyState === 1) { if (ws && ws.readyState === 1) {
@ -301,12 +301,6 @@ setTimeout(() => {
setInterval(sendPing, 1000); setInterval(sendPing, 1000);
}, 0); }, 0);
export function throwError(message: string) {
logError(message);
throw new Error(message);
}
export default { export default {
logError, logError,
subscribeToMessages, subscribeToMessages,

@ -1,33 +1,5 @@
import "normalize.css"; import "normalize.css";
import "boxicons/css/boxicons.min.css";
import "@triliumnext/ckeditor5/content.css"; import "@triliumnext/ckeditor5/content.css";
import "@triliumnext/share-theme/styles/index.css";
import "@triliumnext/share-theme/scripts/index.js";
async function ensureJQuery() {
const $ = (await import("jquery")).default;
(window as any).$ = $;
}
async function applyMath() {
const anyMathBlock = document.querySelector("#content .math-tex");
if (!anyMathBlock) {
return;
}
const renderMathInElement = (await import("./services/math.js")).renderMathInElement;
renderMathInElement(document.getElementById("content"));
}
async function formatCodeBlocks() {
const anyCodeBlock = document.querySelector("#content pre");
if (!anyCodeBlock) {
return;
}
await ensureJQuery();
const { formatCodeBlocks } = await import("./services/syntax_highlight.js");
await formatCodeBlocks($("#content"));
}
/** /**
* Fetch note with given ID from backend * Fetch note with given ID from backend
@ -47,9 +19,6 @@ async function fetchNote(noteId: string | null = null) {
document.addEventListener( document.addEventListener(
"DOMContentLoaded", "DOMContentLoaded",
() => { () => {
formatCodeBlocks();
applyMath();
const toggleMenuButton = document.getElementById("toggleMenuButton"); const toggleMenuButton = document.getElementById("toggleMenuButton");
const layout = document.getElementById("layout"); const layout = document.getElementById("layout");

@ -14,6 +14,33 @@ a {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
#menu {
padding: 25px;
flex-basis: 0;
flex-grow: 1;
overflow: auto;
}
#menu p {
margin: 0;
}
#menu > p {
font-weight: bold;
font-size: 110%;
}
#menu ul {
padding-left: 20px;
}
#main {
flex-basis: 0;
flex-grow: 3;
overflow: auto;
padding: 10px 20px 20px 20px;
}
#parentLink { #parentLink {
float: right; float: right;
margin-top: 20px; margin-top: 20px;
@ -53,6 +80,48 @@ iframe.pdf-view {
cursor: pointer; cursor: pointer;
} }
#childLinks.grid ul {
list-style-type: none;
display: flex;
flex-wrap: wrap;
padding: 0;
}
#childLinks.grid ul li {
width: 180px;
height: 140px;
padding: 10px;
}
#childLinks.grid ul li a {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
border: 1px solid #ddd;
border-radius: 5px;
justify-content: center;
align-content: center;
text-align: center;
font-size: large;
}
#childLinks.grid ul li a:hover {
background: #eee;
}
#childLinks.list ul {
list-style-type: none;
display: inline-flex;
flex-wrap: wrap;
padding: 0;
margin-top: 5px;
}
#childLinks.list ul li {
margin-right: 20px;
}
#noteClippedFrom { #noteClippedFrom {
padding: 10px 0 10px 0; padding: 10px 0 10px 0;
margin: 20px 0 20px 0; margin: 20px 0 20px 0;

@ -25,7 +25,6 @@
--bs-body-font-weight: var(--main-font-weight) !important; --bs-body-font-weight: var(--main-font-weight) !important;
--bs-body-color: var(--main-text-color) !important; --bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important; --bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
} }
.table { .table {
@ -392,7 +391,7 @@ body.desktop .dropdown-menu {
} }
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item, body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
body #context-menu-container .dropdown-item > span { body.desktop #context-menu-container .dropdown-item > span {
display: flex; display: flex;
align-items: center; align-items: center;
} }
@ -440,11 +439,10 @@ body #context-menu-container .dropdown-item > span {
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
margin: 4px; margin: 4px;
font-size: var(--monospace-font-size);
} }
.cm-scroller { body .cm-editor {
font-family: var(--monospace-font-family) !important; font-size: var(--monospace-font-size);
} }
body .cm-editor .cm-gutters { body .cm-editor .cm-gutters {
@ -1275,29 +1273,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
white-space: normal !important; white-space: normal !important;
} }
/* Slash commands */
.ck.ck-slash-command-button {
padding: 0.5em 1em !important;
}
.ck.ck-slash-command-button__text-part,
.ck.ck-template-form__text-part {
margin-left: 0.5em;
line-height: 1.2em !important;
}
.ck.ck-slash-command-button__text-part > span,
.ck.ck-template-form__text-part > span {
line-height: inherit !important;
}
.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description,
.ck.ck-template-form__text-part .ck-template-form__description {
display: block;
opacity: 0.8;
}
.area-expander { .area-expander {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -1813,9 +1788,7 @@ body.zen #left-pane,
body.zen #right-pane, body.zen #right-pane,
body.zen .tab-row-container, body.zen .tab-row-container,
body.zen .tab-row-widget, body.zen .tab-row-widget,
body.zen .ribbon-container:not(:has(.classic-toolbar-widget.visible)), body.zen .ribbon-container,
body.zen .ribbon-container:has(.classic-toolbar-widget.visible) .ribbon-top-row,
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget.visible)),
body.zen .note-icon-widget, body.zen .note-icon-widget,
body.zen .title-row .button-widget, body.zen .title-row .button-widget,
body.zen .floating-buttons-children > *:not(.bx-edit-alt) { body.zen .floating-buttons-children > *:not(.bx-edit-alt) {

@ -395,20 +395,4 @@ div.tn-tool-dialog {
padding-right: 12px; padding-right: 12px;
font-weight: normal; font-weight: normal;
white-space: nowrap; white-space: nowrap;
}
/*
* NOTE TYPE CHOOSER DIALOG
*/
.note-type-chooser-dialog div.note-type-dropdown {
/* Disable the active item highlighting since there is no use for it here */
--active-item-text-color: initial;
--active-item-background-color: initial;
font-size: unset;
}
.note-type-chooser-dialog div.note-type-dropdown .dropdown-item span.bx {
margin-right: .25em;
} }

@ -267,7 +267,7 @@ input::selection,
} }
.input-group button:focus-visible, .input-group button:focus-visible,
.input-group a:focus-visible:not(.dropdown-item) { .input-group a:focus-visible {
box-shadow: unset; box-shadow: unset;
outline: transparent; outline: transparent;
border: transparent; border: transparent;
@ -349,7 +349,7 @@ select:hover,
select.form-select:hover, select.form-select:hover,
select.form-control:hover, select.form-control:hover,
.select-button.dropdown-toggle.btn:hover { .select-button.dropdown-toggle.btn:hover {
background: var(--input-hover-background) var(--dropdown-arrow,); background: var(--input-hover-background) var(--dropdown-arrow);
color: var(--input-hover-color); color: var(--input-hover-color);
} }

@ -201,11 +201,6 @@
color: var(--menu-item-icon-color); color: var(--menu-item-icon-color);
} }
/* Slash commands */
.ck.ck-slash-command-button__text-part .ck.ck-button__label {
font-weight: bold;
}
/* Separator */ /* Separator */
:root .ck .ck-list__separator { :root .ck .ck-list__separator {
margin: .5em 0; margin: .5em 0;

@ -142,12 +142,6 @@ div.note-detail-empty {
border: unset; border: unset;
} }
/* NOTE ATTACHMENTS */
.attachment-list div.links-wrapper {
font-size: unset;
}
/* /*
* OPTIONS PAGES * OPTIONS PAGES
*/ */

@ -354,7 +354,7 @@ body.layout-horizontal > .horizontal {
} }
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button { .calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
--select-arrow-svg: initial; /* Disable the dropdown arrow */ --select-arrow-svg: ""; /* Disable the dropdown arrow */
} }
@media (max-width: 992px) { @media (max-width: 992px) {
@ -1145,18 +1145,12 @@ body.mobile .note-title {
/* The "Change note icon" button */ /* The "Change note icon" button */
:root .note-icon-widget button.note-icon, .note-icon-widget .note-icon {
:root .note-icon-widget button.note-icon:hover {
border: none; border: none;
border-radius: 8px; border-radius: 8px;
} }
/* Dropdown open */ .note-icon-widget .note-icon:hover {
:root .note-icon-widget button.note-icon.show {
background: var(--ck-editor-toolbar-dropdown-button-open-background);
}
:root .note-icon-widget button.note-icon:not(:disabled):hover {
background: var(--icon-button-hover-background); background: var(--icon-button-hover-background);
color: var(--icon-button-hover-color); color: var(--icon-button-hover-color);
} }

@ -1333,7 +1333,7 @@
"recovery_keys_used": "已使用: {{date}}", "recovery_keys_used": "已使用: {{date}}",
"recovery_keys_unused": "恢复代码 {{index}} 未使用", "recovery_keys_unused": "恢复代码 {{index}} 未使用",
"oauth_title": "OAuth/OpenID 认证", "oauth_title": "OAuth/OpenID 认证",
"oauth_description": "OpenID 是一种标准化方式,允许您使用其他服务(如 Google的账号登录网站来验证您的身份。默认的身份提供者是 Google但您可以更改为任何其他 OpenID 提供者。点击<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">这里</a>了解更多信息。请参阅这些 <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">指南</a> 通过 Google 设置 OpenID 服务。", "oauth_description": "OpenID 是一种标准化方式,允许您使用其他服务(如 Google的账户登录网站,以验证您的身份。请参阅这些 <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">指南</a> 通过 Google 设置 OpenID 服务。",
"oauth_description_warning": "要启用 OAuth/OpenID您需要设置 config.ini 文件中的 OAuth/OpenID 基础 URL、客户端 ID 和客户端密钥,并重新启动应用程序。如果要从环境变量设置,请设置 TRILIUM_OAUTH_BASE_URL、TRILIUM_OAUTH_CLIENT_ID 和 TRILIUM_OAUTH_CLIENT_SECRET 环境变量。", "oauth_description_warning": "要启用 OAuth/OpenID您需要设置 config.ini 文件中的 OAuth/OpenID 基础 URL、客户端 ID 和客户端密钥,并重新启动应用程序。如果要从环境变量设置,请设置 TRILIUM_OAUTH_BASE_URL、TRILIUM_OAUTH_CLIENT_ID 和 TRILIUM_OAUTH_CLIENT_SECRET 环境变量。",
"oauth_missing_vars": "缺少以下设置项: {{missingVars}}", "oauth_missing_vars": "缺少以下设置项: {{missingVars}}",
"oauth_user_account": "用户账号:", "oauth_user_account": "用户账号:",

@ -233,8 +233,6 @@
"move_success_message": "Selected notes have been moved into " "move_success_message": "Selected notes have been moved into "
}, },
"note_type_chooser": { "note_type_chooser": {
"change_path_prompt": "Change where to create the new note:",
"search_placeholder": "search path by name (default if empty)",
"modal_title": "Choose note type", "modal_title": "Choose note type",
"close": "Close", "close": "Close",
"modal_body": "Choose note type / template of the new note:", "modal_body": "Choose note type / template of the new note:",
@ -1126,8 +1124,10 @@
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width." "layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
}, },
"ai_llm": { "ai_llm": {
"embeddings_configuration": "Embeddings Configuration",
"not_started": "Not started", "not_started": "Not started",
"title": "AI Settings", "title": "AI & Embedding Settings",
"embedding_statistics": "Embedding Statistics",
"processed_notes": "Processed Notes", "processed_notes": "Processed Notes",
"total_notes": "Total Notes", "total_notes": "Total Notes",
"progress": "Progress", "progress": "Progress",
@ -1135,6 +1135,7 @@
"failed_notes": "Failed Notes", "failed_notes": "Failed Notes",
"last_processed": "Last Processed", "last_processed": "Last Processed",
"refresh_stats": "Refresh Statistics", "refresh_stats": "Refresh Statistics",
"no_failed_embeddings": "No failed embeddings found.",
"enable_ai_features": "Enable AI/LLM features", "enable_ai_features": "Enable AI/LLM features",
"enable_ai_description": "Enable AI features like note summarization, content generation, and other LLM capabilities", "enable_ai_description": "Enable AI features like note summarization, content generation, and other LLM capabilities",
"openai_tab": "OpenAI", "openai_tab": "OpenAI",
@ -1159,16 +1160,20 @@
"anthropic_api_key_description": "Your Anthropic API key for accessing Claude models", "anthropic_api_key_description": "Your Anthropic API key for accessing Claude models",
"default_model": "Default Model", "default_model": "Default Model",
"openai_model_description": "Examples: gpt-4o, gpt-4-turbo, gpt-3.5-turbo", "openai_model_description": "Examples: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
"embedding_model": "Embedding Model",
"openai_embedding_model_description": "Model used for generating embeddings (text-embedding-3-small recommended)",
"base_url": "Base URL", "base_url": "Base URL",
"openai_url_description": "Default: https://api.openai.com/v1", "openai_url_description": "Default: https://api.openai.com/v1",
"anthropic_settings": "Anthropic Settings", "anthropic_settings": "Anthropic Settings",
"anthropic_url_description": "Base URL for the Anthropic API (default: https://api.anthropic.com)", "anthropic_url_description": "Base URL for the Anthropic API (default: https://api.anthropic.com)",
"anthropic_model_description": "Anthropic Claude models for chat completion", "anthropic_model_description": "Anthropic Claude models for chat completion",
"voyage_settings": "Voyage AI Settings", "voyage_settings": "Voyage AI Settings",
"voyage_api_key_description": "Your Voyage AI API key for accessing embeddings services",
"ollama_settings": "Ollama Settings", "ollama_settings": "Ollama Settings",
"ollama_url_description": "URL for the Ollama API (default: http://localhost:11434)", "ollama_url_description": "URL for the Ollama API (default: http://localhost:11434)",
"ollama_model_description": "Ollama model to use for chat completion", "ollama_model_description": "Ollama model to use for chat completion",
"anthropic_configuration": "Anthropic Configuration", "anthropic_configuration": "Anthropic Configuration",
"voyage_embedding_model_description": "Voyage AI embedding models for text embeddings (voyage-2 recommended)",
"voyage_configuration": "Voyage AI Configuration", "voyage_configuration": "Voyage AI Configuration",
"voyage_url_description": "Default: https://api.voyageai.com/v1", "voyage_url_description": "Default: https://api.voyageai.com/v1",
"ollama_configuration": "Ollama Configuration", "ollama_configuration": "Ollama Configuration",
@ -1176,10 +1181,28 @@
"enable_ollama_description": "Enable Ollama for local AI model usage", "enable_ollama_description": "Enable Ollama for local AI model usage",
"ollama_url": "Ollama URL", "ollama_url": "Ollama URL",
"ollama_model": "Ollama Model", "ollama_model": "Ollama Model",
"ollama_embedding_model": "Embedding Model",
"ollama_embedding_model_description": "Specialized model for generating embeddings (vector representations)",
"refresh_models": "Refresh Models", "refresh_models": "Refresh Models",
"refreshing_models": "Refreshing...", "refreshing_models": "Refreshing...",
"embedding_configuration": "Embeddings Configuration",
"embedding_default_provider": "Default Provider",
"embedding_default_provider_description": "Select the default provider used for generating note embeddings",
"embedding_provider_precedence": "Embedding Provider Precedence",
"embedding_providers_order": "Embedding Provider Order",
"embedding_providers_order_description": "Set the order of embedding providers in comma-separated format (e.g., \"openai,voyage,ollama,local\")",
"enable_automatic_indexing": "Enable Automatic Indexing", "enable_automatic_indexing": "Enable Automatic Indexing",
"enable_automatic_indexing_description": "Automatically generate embeddings for new and updated notes",
"embedding_auto_update_enabled": "Auto-update Embeddings",
"embedding_auto_update_enabled_description": "Automatically update embeddings when notes are modified",
"recreate_embeddings": "Recreate All Embeddings",
"recreate_embeddings_description": "Regenerate all note embeddings from scratch (may take a long time for large note collections)",
"recreate_embeddings_started": "Embeddings regeneration started. This may take a long time for large note collections.",
"recreate_embeddings_error": "Error starting embeddings regeneration. Check logs for details.",
"recreate_embeddings_confirm": "Are you sure you want to recreate all embeddings? This may take a long time for large note collections.",
"rebuild_index": "Rebuild Index", "rebuild_index": "Rebuild Index",
"rebuild_index_description": "Rebuild the vector search index for better performance (much faster than recreating embeddings)",
"rebuild_index_started": "Embedding index rebuild started. This may take several minutes.",
"rebuild_index_error": "Error starting index rebuild. Check logs for details.", "rebuild_index_error": "Error starting index rebuild. Check logs for details.",
"note_title": "Note Title", "note_title": "Note Title",
"error": "Error", "error": "Error",
@ -1189,16 +1212,43 @@
"partial": "{{ percentage }}% completed", "partial": "{{ percentage }}% completed",
"retry_queued": "Note queued for retry", "retry_queued": "Note queued for retry",
"retry_failed": "Failed to queue note for retry", "retry_failed": "Failed to queue note for retry",
"embedding_provider_precedence_description": "Comma-separated list of providers in order of precedence for embeddings search (e.g., 'openai,ollama,anthropic')",
"embedding_dimension_strategy": "Embedding Dimension Strategy",
"embedding_dimension_auto": "Auto (Recommended)",
"embedding_dimension_fixed": "Fixed",
"embedding_similarity_threshold": "Similarity Threshold",
"embedding_similarity_threshold_description": "Minimum similarity score for notes to be included in search results (0-1)",
"max_notes_per_llm_query": "Max Notes Per Query", "max_notes_per_llm_query": "Max Notes Per Query",
"max_notes_per_llm_query_description": "Maximum number of similar notes to include in AI context", "max_notes_per_llm_query_description": "Maximum number of similar notes to include in AI context",
"embedding_dimension_strategy_description": "Choose how embeddings are handled. 'Native' preserves maximum information by adapting smaller vectors to match larger ones (recommended). 'Regenerate' creates new embeddings with the target model for specific search needs.",
"drag_providers_to_reorder": "Drag providers up or down to set your preferred order for embedding searches",
"active_providers": "Active Providers", "active_providers": "Active Providers",
"disabled_providers": "Disabled Providers", "disabled_providers": "Disabled Providers",
"remove_provider": "Remove provider from search", "remove_provider": "Remove provider from search",
"restore_provider": "Restore provider to search", "restore_provider": "Restore provider to search",
"embedding_generation_location": "Generation Location",
"embedding_generation_location_description": "Select where embedding generation should happen",
"embedding_generation_location_client": "Client/Server",
"embedding_generation_location_sync_server": "Sync Server",
"enable_auto_update_embeddings": "Auto-update Embeddings",
"enable_auto_update_embeddings_description": "Automatically update embeddings when notes are modified",
"auto_update_embeddings": "Auto-update Embeddings",
"auto_update_embeddings_desc": "Automatically update embeddings when notes are modified",
"similarity_threshold": "Similarity Threshold", "similarity_threshold": "Similarity Threshold",
"similarity_threshold_description": "Minimum similarity score (0-1) for notes to be included in context for LLM queries", "similarity_threshold_description": "Minimum similarity score (0-1) for notes to be included in context for LLM queries",
"embedding_batch_size": "Batch Size",
"embedding_batch_size_description": "Number of notes to process in a single batch (1-50)",
"embedding_update_interval": "Update Interval (ms)",
"embedding_update_interval_description": "Time between processing batches of embeddings (in milliseconds)",
"embedding_default_dimension": "Default Dimension",
"embedding_default_dimension_description": "Default embedding vector dimension when creating new embeddings",
"reprocess_all_embeddings": "Reprocess All Embeddings",
"reprocess_all_embeddings_description": "Queue all notes for embedding processing. This may take some time depending on your number of notes.",
"reprocessing_embeddings": "Reprocessing...",
"reprocess_started": "Embedding reprocessing started in the background",
"reprocess_error": "Error starting embedding reprocessing",
"reprocess_index": "Rebuild Search Index", "reprocess_index": "Rebuild Search Index",
"reprocess_index_description": "Optimize the search index for better performance. This uses existing embeddings without regenerating them (much faster than reprocessing all embeddings).",
"reprocessing_index": "Rebuilding...", "reprocessing_index": "Rebuilding...",
"reprocess_index_started": "Search index optimization started in the background", "reprocess_index_started": "Search index optimization started in the background",
"reprocess_index_error": "Error rebuilding search index", "reprocess_index_error": "Error rebuilding search index",
@ -1211,6 +1261,7 @@
"incomplete": "Incomplete ({{percentage}}%)", "incomplete": "Incomplete ({{percentage}}%)",
"complete": "Complete (100%)", "complete": "Complete (100%)",
"refreshing": "Refreshing...", "refreshing": "Refreshing...",
"stats_error": "Error fetching embedding statistics",
"auto_refresh_notice": "Auto-refreshes every {{seconds}} seconds", "auto_refresh_notice": "Auto-refreshes every {{seconds}} seconds",
"note_queued_for_retry": "Note queued for retry", "note_queued_for_retry": "Note queued for retry",
"failed_to_retry_note": "Failed to retry note", "failed_to_retry_note": "Failed to retry note",
@ -1218,6 +1269,7 @@
"failed_to_retry_all": "Failed to retry notes", "failed_to_retry_all": "Failed to retry notes",
"ai_settings": "AI Settings", "ai_settings": "AI Settings",
"api_key_tooltip": "API key for accessing the service", "api_key_tooltip": "API key for accessing the service",
"confirm_delete_embeddings": "Are you sure you want to delete all AI embeddings? This will remove all semantic search capabilities until notes are reindexed, which can take a significant amount of time.",
"empty_key_warning": { "empty_key_warning": {
"anthropic": "Anthropic API key is empty. Please enter a valid API key.", "anthropic": "Anthropic API key is empty. Please enter a valid API key.",
"openai": "OpenAI API key is empty. Please enter a valid API key.", "openai": "OpenAI API key is empty. Please enter a valid API key.",
@ -1250,6 +1302,7 @@
"note_chat": "Note Chat", "note_chat": "Note Chat",
"notes_indexed": "{{ count }} note indexed", "notes_indexed": "{{ count }} note indexed",
"notes_indexed_plural": "{{ count }} notes indexed", "notes_indexed_plural": "{{ count }} notes indexed",
"reset_embeddings": "Reset Embeddings",
"sources": "Sources", "sources": "Sources",
"start_indexing": "Start Indexing", "start_indexing": "Start Indexing",
"use_advanced_context": "Use Advanced Context", "use_advanced_context": "Use Advanced Context",
@ -1262,11 +1315,24 @@
}, },
"create_new_ai_chat": "Create new AI Chat", "create_new_ai_chat": "Create new AI Chat",
"configuration_warnings": "There are some issues with your AI configuration. Please check your settings.", "configuration_warnings": "There are some issues with your AI configuration. Please check your settings.",
"experimental_warning": "The LLM feature is currently experimental - you have been warned.", "embeddings_started": "Embedding generation started",
"embeddings_stopped": "Embedding generation stopped",
"embeddings_toggle_error": "Error toggling embeddings",
"local_embedding_description": "Uses local embedding models for offline text embedding generation",
"local_embedding_settings": "Local Embedding Settings",
"ollama_embedding_settings": "Ollama Embedding Settings",
"ollama_embedding_url_description": "URL for the Ollama API for embedding generation (default: http://localhost:11434)",
"openai_embedding_api_key_description": "Your OpenAI API key for embedding generation (can be different from chat API key)",
"openai_embedding_settings": "OpenAI Embedding Settings",
"openai_embedding_url_description": "Base URL for OpenAI embedding API (default: https://api.openai.com/v1)",
"selected_embedding_provider": "Selected Embedding Provider",
"selected_embedding_provider_description": "Choose the provider for generating note embeddings",
"selected_provider": "Selected Provider", "selected_provider": "Selected Provider",
"selected_provider_description": "Choose the AI provider for chat and completion features", "selected_provider_description": "Choose the AI provider for chat and completion features",
"select_embedding_provider": "Select embedding provider...",
"select_model": "Select model...", "select_model": "Select model...",
"select_provider": "Select provider..." "select_provider": "Select provider...",
"voyage_embedding_url_description": "Base URL for the Voyage AI embedding API (default: https://api.voyageai.com/v1)"
}, },
"zoom_factor": { "zoom_factor": {
"title": "Zoom Factor (desktop build only)", "title": "Zoom Factor (desktop build only)",
@ -1495,7 +1561,7 @@
"recovery_keys_used": "Used: {{date}}", "recovery_keys_used": "Used: {{date}}",
"recovery_keys_unused": "Recovery code {{index}} is unused", "recovery_keys_unused": "Recovery code {{index}} is unused",
"oauth_title": "OAuth/OpenID", "oauth_title": "OAuth/OpenID",
"oauth_description": "OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity. The default issuer is Google, but you can change it to any other OpenID provider. Check <a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">here</a> for more information. Follow these <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">instructions</a> to setup an OpenID service through Google.", "oauth_description": "OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity. Follow these <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">instructions</a> to setup an OpenID service through Google.",
"oauth_description_warning": "To enable OAuth/OpenID, you need to set the OAuth/OpenID base URL, client ID and client secret in the config.ini file and restart the application. If you want to set from environment variables, please set TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID and TRILIUM_OAUTH_CLIENT_SECRET.", "oauth_description_warning": "To enable OAuth/OpenID, you need to set the OAuth/OpenID base URL, client ID and client secret in the config.ini file and restart the application. If you want to set from environment variables, please set TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID and TRILIUM_OAUTH_CLIENT_SECRET.",
"oauth_missing_vars": "Missing settings: {{variables}}", "oauth_missing_vars": "Missing settings: {{variables}}",
"oauth_user_account": "User Account: ", "oauth_user_account": "User Account: ",
@ -1920,14 +1986,5 @@
"title": "Appearance", "title": "Appearance",
"word_wrapping": "Word wrapping", "word_wrapping": "Word wrapping",
"color-scheme": "Color scheme" "color-scheme": "Color scheme"
},
"cpu_arch_warning": {
"title": "Please download the ARM64 version",
"message_macos": "TriliumNext is currently running under Rosetta 2 translation, which means you're using the Intel (x64) version on Apple Silicon Mac. This will significantly impact performance and battery life.",
"message_windows": "TriliumNext is currently running emulation, which means you're using the Intel (x64) version on a Windows on ARM device. This will significantly impact performance and battery life.",
"recommendation": "For the best experience, please download the native ARM64 version of TriliumNext from our releases page.",
"download_link": "Download Native Version",
"continue_anyway": "Continue Anyway",
"dont_show_again": "Don't show this warning again"
} }
} }

@ -16,7 +16,7 @@
"message": "Ha ocurrido un error crítico que previene que el cliente de la aplicación inicie:\n\n{{message}}\n\nMuy probablemente es causado por un script que falla de forma inesperada. Intente iniciar la aplicación en modo seguro y atienda el error." "message": "Ha ocurrido un error crítico que previene que el cliente de la aplicación inicie:\n\n{{message}}\n\nMuy probablemente es causado por un script que falla de forma inesperada. Intente iniciar la aplicación en modo seguro y atienda el error."
}, },
"widget-error": { "widget-error": {
"title": "Hubo un fallo al inicializar un widget", "title": "No se pudo inicializar un widget",
"message-custom": "El widget personalizado de la nota con ID \"{{id}}\", titulada \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}", "message-custom": "El widget personalizado de la nota con ID \"{{id}}\", titulada \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}",
"message-unknown": "Un widget no pudo ser inicializado debido a:\n\n{{message}}" "message-unknown": "Un widget no pudo ser inicializado debido a:\n\n{{message}}"
}, },
@ -127,7 +127,6 @@
"collapseSubTree": "colapsar subárbol", "collapseSubTree": "colapsar subárbol",
"tabShortcuts": "Atajos de pestañas", "tabShortcuts": "Atajos de pestañas",
"newTabNoteLink": "<kbd>CTRL+clic</kbd> - (o clic central del mouse) en el enlace de la nota abre la nota en una nueva pestaña", "newTabNoteLink": "<kbd>CTRL+clic</kbd> - (o clic central del mouse) en el enlace de la nota abre la nota en una nueva pestaña",
"newTabWithActivationNoteLink": "<kbd>Ctrl+Shift+clic</kbd> - (o <kbd>Shift+clic de rueda de ratón</kbd>) en el enlace de la nota abre y activa la nota en una nueva pestaña",
"onlyInDesktop": "Solo en escritorio (compilación con Electron)", "onlyInDesktop": "Solo en escritorio (compilación con Electron)",
"openEmptyTab": "abrir pestaña vacía", "openEmptyTab": "abrir pestaña vacía",
"closeActiveTab": "cerrar pestaña activa", "closeActiveTab": "cerrar pestaña activa",
@ -233,8 +232,6 @@
"move_success_message": "Las notas seleccionadas se han movido a " "move_success_message": "Las notas seleccionadas se han movido a "
}, },
"note_type_chooser": { "note_type_chooser": {
"change_path_prompt": "Cambiar donde se creará la nueva nota:",
"search_placeholder": "ruta de búsqueda por nombre (por defecto si está vacío)",
"modal_title": "Elija el tipo de nota", "modal_title": "Elija el tipo de nota",
"close": "Cerrar", "close": "Cerrar",
"modal_body": "Elija el tipo de nota/plantilla de la nueva nota:", "modal_body": "Elija el tipo de nota/plantilla de la nueva nota:",
@ -277,9 +274,9 @@
"revision_last_edited": "Esta revisión se editó por última vez en {{date}}", "revision_last_edited": "Esta revisión se editó por última vez en {{date}}",
"confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?", "confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
"no_revisions": "Aún no hay revisiones para esta nota...", "no_revisions": "Aún no hay revisiones para esta nota...",
"restore_button": "Restaurar", "restore_button": "",
"confirm_restore": "¿Quiere restaurar esta revisión? Esto sobrescribirá el título actual y el contenido de la nota con esta revisión.", "confirm_restore": "¿Quiere restaurar esta revisión? Esto sobrescribirá el título actual y el contenido de la nota con esta revisión.",
"delete_button": "Eliminar", "delete_button": "",
"confirm_delete": "¿Quieres eliminar esta revisión?", "confirm_delete": "¿Quieres eliminar esta revisión?",
"revisions_deleted": "Se han eliminado las revisiones de nota.", "revisions_deleted": "Se han eliminado las revisiones de nota.",
"revision_restored": "Se ha restaurado la revisión de nota.", "revision_restored": "Se ha restaurado la revisión de nota.",
@ -591,7 +588,6 @@
"sat": "Sáb", "sat": "Sáb",
"sun": "Dom", "sun": "Dom",
"cannot_find_day_note": "No se puede encontrar la nota del día", "cannot_find_day_note": "No se puede encontrar la nota del día",
"cannot_find_week_note": "No se puede encontrar la nota de la semana",
"january": "Enero", "january": "Enero",
"febuary": "Febrero", "febuary": "Febrero",
"march": "Marzo", "march": "Marzo",
@ -1125,148 +1121,6 @@
"layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)", "layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)",
"layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo." "layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo."
}, },
"ai_llm": {
"not_started": "No iniciado",
"title": "IA y ajustes de embeddings",
"processed_notes": "Notas procesadas",
"total_notes": "Notas totales",
"progress": "Progreso",
"queued_notes": "Notas en fila",
"failed_notes": "Notas fallidas",
"last_processed": "Última procesada",
"refresh_stats": "Recargar estadísticas",
"enable_ai_features": "Habilitar características IA/LLM",
"enable_ai_description": "Habilitar características de IA como resumen de notas, generación de contenido y otras capacidades LLM",
"openai_tab": "OpenAI",
"anthropic_tab": "Anthropic",
"voyage_tab": "Voyage AI",
"ollama_tab": "Ollama",
"enable_ai": "Habilitar características IA/LLM",
"enable_ai_desc": "Habilitar características de IA como resumen de notas, generación de contenido y otras capacidades LLM",
"provider_configuration": "Configuración de proveedor de IA",
"provider_precedence": "Precedencia de proveedor",
"provider_precedence_description": "Lista de proveedores en orden de precedencia separada por comas (p.e., 'openai,anthropic,ollama')",
"temperature": "Temperatura",
"temperature_description": "Controla la aleatoriedad de las respuestas (0 = determinista, 2 = aleatoriedad máxima)",
"system_prompt": "Mensaje de sistema",
"system_prompt_description": "Mensaje de sistema predeterminado utilizado para todas las interacciones de IA",
"openai_configuration": "Configuración de OpenAI",
"openai_settings": "Ajustes de OpenAI",
"api_key": "Clave API",
"url": "URL base",
"model": "Modelo",
"openai_api_key_description": "Tu clave API de OpenAI para acceder a sus servicios de IA",
"anthropic_api_key_description": "Tu clave API de Anthropic para acceder a los modelos Claude",
"default_model": "Modelo por defecto",
"openai_model_description": "Ejemplos: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
"base_url": "URL base",
"openai_url_description": "Por defecto: https://api.openai.com/v1",
"anthropic_settings": "Ajustes de Anthropic",
"anthropic_url_description": "URL base para la API de Anthropic (por defecto: https://api.anthropic.com)",
"anthropic_model_description": "Modelos Claude de Anthropic para el completado de chat",
"voyage_settings": "Ajustes de Voyage AI",
"ollama_settings": "Ajustes de Ollama",
"ollama_url_description": "URL para la API de Ollama (por defecto: http://localhost:11434)",
"ollama_model_description": "Modelo de Ollama a usar para el completado de chat",
"anthropic_configuration": "Configuración de Anthropic",
"voyage_configuration": "Configuración de Voyage AI",
"voyage_url_description": "Por defecto: https://api.voyageai.com/v1",
"ollama_configuration": "Configuración de Ollama",
"enable_ollama": "Habilitar Ollama",
"enable_ollama_description": "Habilitar Ollama para uso de modelo de IA local",
"ollama_url": "URL de Ollama",
"ollama_model": "Modelo de Ollama",
"refresh_models": "Refrescar modelos",
"refreshing_models": "Refrescando...",
"enable_automatic_indexing": "Habilitar indexado automático",
"rebuild_index": "Recrear índice",
"rebuild_index_error": "Error al comenzar la reconstrucción del índice. Consulte los registros para más detalles.",
"note_title": "Título de nota",
"error": "Error",
"last_attempt": "Último intento",
"actions": "Acciones",
"retry": "Reintentar",
"partial": "{{ percentage }}% completado",
"retry_queued": "Nota en la cola para reintento",
"retry_failed": "Hubo un fallo al poner en la cola a la nota para reintento",
"max_notes_per_llm_query": "Máximo de notas por consulta",
"max_notes_per_llm_query_description": "Número máximo de notas similares a incluir en el contexto IA",
"active_providers": "Proveedores activos",
"disabled_providers": "Proveedores deshabilitados",
"remove_provider": "Eliminar proveedor de la búsqueda",
"restore_provider": "Restaurar proveedor a la búsqueda",
"similarity_threshold": "Bias de similaridad",
"similarity_threshold_description": "Puntuación de similaridad mínima (0-1) para incluir notas en el contexto para consultas LLM",
"reprocess_index": "Reconstruir el índice de búsqueda",
"reprocessing_index": "Reconstruyendo...",
"reprocess_index_started": "La optimización de índice de búsqueda comenzó en segundo plano",
"reprocess_index_error": "Error al reconstruir el índice de búsqueda",
"index_rebuild_progress": "Progreso de reconstrucción de índice",
"index_rebuilding": "Optimizando índice ({{percentage}}%)",
"index_rebuild_complete": "Optimización de índice completa",
"index_rebuild_status_error": "Error al comprobar el estado de reconstrucción del índice",
"never": "Nunca",
"processing": "Procesando ({{percentage}}%)",
"incomplete": "Incompleto ({{percentage}}%)",
"complete": "Completo (100%)",
"refreshing": "Refrescando...",
"auto_refresh_notice": "Refrescar automáticamente cada {{seconds}} segundos",
"note_queued_for_retry": "Nota en la cola para reintento",
"failed_to_retry_note": "Hubo un fallo al reintentar nota",
"all_notes_queued_for_retry": "Todas las notas con fallo agregadas a la cola para reintento",
"failed_to_retry_all": "Hubo un fallo al reintentar notas",
"ai_settings": "Ajustes de IA",
"api_key_tooltip": "Clave API para acceder al servicio",
"empty_key_warning": {
"anthropic": "La clave API de Anthropic está vacía. Por favor, ingrese una clave API válida.",
"openai": "La clave API de OpenAI está vacía. Por favor, ingrese una clave API válida.",
"voyage": "La clave API de Voyage está vacía. Por favor, ingrese una clave API válida.",
"ollama": "La clave API de Ollama está vacía. Por favor, ingrese una clave API válida."
},
"agent": {
"processing": "Procesando...",
"thinking": "Pensando...",
"loading": "Cargando...",
"generating": "Generando..."
},
"name": "IA",
"openai": "OpenAI",
"use_enhanced_context": "Utilizar contexto mejorado",
"enhanced_context_description": "Provee a la IA con más contexto de la nota y sus notas relacionadas para obtener mejores respuestas",
"show_thinking": "Mostrar pensamiento",
"show_thinking_description": "Mostrar la cadena del proceso de pensamiento de la IA",
"enter_message": "Ingrese su mensaje...",
"error_contacting_provider": "Error al contactar con su proveedor de IA. Por favor compruebe sus ajustes y conexión a internet.",
"error_generating_response": "Error al generar respuesta de IA",
"index_all_notes": "Indexar todas las notas",
"index_status": "Estado de índice",
"indexed_notes": "Notas indexadas",
"indexing_stopped": "Indexado detenido",
"indexing_in_progress": "Indexado en progreso...",
"last_indexed": "Último indexado",
"n_notes_queued": "{{ count }} nota agregada a la cola para indexado",
"n_notes_queued_plural": "{{ count }} notas agregadas a la cola para indexado",
"note_chat": "Chat de nota",
"notes_indexed": "{{ count }} nota indexada",
"notes_indexed_plural": "{{ count }} notas indexadas",
"sources": "Fuentes",
"start_indexing": "Comenzar indexado",
"use_advanced_context": "Usar contexto avanzado",
"ollama_no_url": "Ollama no está configurado. Por favor ingrese una URL válida.",
"chat": {
"root_note_title": "Chats de IA",
"root_note_content": "Esta nota contiene tus conversaciones de chat de IA guardadas.",
"new_chat_title": "Nuevo chat",
"create_new_ai_chat": "Crear nuevo chat de IA"
},
"create_new_ai_chat": "Crear nuevo chat de IA",
"configuration_warnings": "Hay algunos problemas con su configuración de IA. Por favor compruebe sus ajustes.",
"experimental_warning": "La característica de LLM aún es experimental - ha sido advertido.",
"selected_provider": "Proveedor seleccionado",
"selected_provider_description": "Elija el proveedor de IA para el chat y características de completado",
"select_model": "Seleccionar modelo...",
"select_provider": "Seleccionar proveedor..."
},
"zoom_factor": { "zoom_factor": {
"title": "Factor de zoom (solo versión de escritorio)", "title": "Factor de zoom (solo versión de escritorio)",
"description": "El zoom también se puede controlar con los atajos CTRL+- y CTRL+=." "description": "El zoom también se puede controlar con los atajos CTRL+- y CTRL+=."
@ -1382,26 +1236,12 @@
"label": "Tamaño para modo de solo lectura automático (notas de texto)", "label": "Tamaño para modo de solo lectura automático (notas de texto)",
"unit": "caracteres" "unit": "caracteres"
}, },
"custom_date_time_format": {
"title": "Formato de fecha/hora personalizada",
"description": "Personalizar el formado de fecha y la hora insertada vía <kbd></kbd> o la barra de herramientas. Véa la <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">documentación de Day.js</a> para más tokens de formato disponibles.",
"format_string": "Cadena de formato:",
"formatted_time": "Fecha/hora personalizada:"
},
"i18n": { "i18n": {
"title": "Localización", "title": "Localización",
"language": "Idioma", "language": "Idioma",
"first-day-of-the-week": "Primer día de la semana", "first-day-of-the-week": "Primer día de la semana",
"sunday": "Domingo", "sunday": "Domingo",
"monday": "Lunes", "monday": "Lunes"
"first-week-of-the-year": "Primer semana del año",
"first-week-contains-first-day": "Primer semana que contiene al primer día del año",
"first-week-contains-first-thursday": "Primer semana que contiene al primer jueves del año",
"first-week-has-minimum-days": "Primer semana que contiene un mínimo de días",
"min-days-in-first-week": "Días mínimos en la primer semana",
"first-week-info": "Primer semana que contiene al primer jueves del año está basado en el estándar<a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
"first-week-warning": "Cambiar las opciones de primer semana puede causar duplicados con las Notas Semanales existentes y las Notas Semanales existentes no serán actualizadas respectivamente.",
"formatting-locale": "Fecha y formato de número"
}, },
"backup": { "backup": {
"automatic_backup": "Copia de seguridad automática", "automatic_backup": "Copia de seguridad automática",
@ -1468,39 +1308,6 @@
"password_mismatch": "Las nuevas contraseñas no son las mismas.", "password_mismatch": "Las nuevas contraseñas no son las mismas.",
"password_changed_success": "La contraseña ha sido cambiada. Trilium se recargará después de presionar Aceptar." "password_changed_success": "La contraseña ha sido cambiada. Trilium se recargará después de presionar Aceptar."
}, },
"multi_factor_authentication": {
"title": "Autenticación Multi-Factor",
"description": "La autenticación multifactor (MFA) agrega una capa adicional de seguridad a su cuenta. En lugar de solo ingresar una contraseña para iniciar sesión, MFA requiere que proporcione una o más pruebas adicionales para verificar su identidad. De esta manera, incluso si alguien se apodera de su contraseña, aún no puede acceder a su cuenta sin la segunda pieza de información. Es como agregar una cerradura adicional a su puerta, lo que hace que sea mucho más difícil para cualquier otra persona entrar.<br><br>Por favor siga las instrucciones a continuación para habilitar MFA. Si no lo configura correctamente, el inicio de sesión volverá a solo contraseña.",
"mfa_enabled": "Habilitar la autenticación multifactor",
"mfa_method": "Método MFA",
"electron_disabled": "Actualmente la autenticación multifactor no está soportada en la compilación de escritorio.",
"totp_title": "Contraseña de un solo uso basada en el tiempo (TOTP)",
"totp_description": "TOTP (contraseña de un solo uso basada en el tiempo) es una característica de seguridad que genera un código temporal único que cambia cada 30 segundos. Utiliza este código, junto con su contraseña para iniciar sesión en su cuenta, lo que hace que sea mucho más difícil para cualquier otra persona acceder a ella.",
"totp_secret_title": "Generar secreto TOTP",
"totp_secret_generate": "Generar secreto TOTP",
"totp_secret_regenerate": "Regenerar secreto TOTP",
"no_totp_secret_warning": "Para habilitar TOTP, primero debe de generar un secreto TOTP.",
"totp_secret_description_warning": "Después de generar un nuevo secreto TOTP, le será requerido que inicie sesión otra vez con el nuevo secreto TOTP.",
"totp_secret_generated": "Secreto TOTP generado",
"totp_secret_warning": "Por favor guarde el secreto generado en una ubicación segura. No será mostrado de nuevo.",
"totp_secret_regenerate_confirm": "¿Está seguro que desea regenerar el secreto TOTP? Esto va a invalidar el secreto TOTP previo y todos los códigos de recuperación existentes.",
"recovery_keys_title": "Claves de recuperación para un solo inicio de sesión",
"recovery_keys_description": "Las claves de recuperación para un solo inicio de sesión son usadas para iniciar sesión incluso cuando no puede acceder a los códigos de su autentificador.",
"recovery_keys_description_warning": "Las claves de recuperación no son mostrada de nuevo después de dejar esta página, manténgalas en un lugar seguro.<br>Después de que una clave de recuperación es utilizada ya no puede utilizarse de nuevo.",
"recovery_keys_error": "Error al generar códigos de recuperación",
"recovery_keys_no_key_set": "No hay códigos de recuperación establecidos",
"recovery_keys_generate": "Generar códigos de recuperación",
"recovery_keys_regenerate": "Regenerar códigos de recuperación",
"recovery_keys_used": "Usado: {{date}}",
"recovery_keys_unused": "El código de recuperación {{index}} está sin usar",
"oauth_title": "OAuth/OpenID",
"oauth_description": "OpenID es una forma estandarizada de permitirle iniciar sesión en sitios web utilizando una cuenta de otro servicio, como Google, para verificar su identidad. Siga estas <a href = \"https://developers.google.com/identity/openid-connect/openid-connect\">instrucciones</a> para configurar un servicio OpenID a través de Google.",
"oauth_description_warning": "Para habilitar OAuth/OpenID, necesita establecer la URL base de OAuth/OpenID, ID de cliente y secreto de cliente en el archivo config.ini y reiniciar la aplicación. Si desea establecerlas desde variables de ambiente, por favor establezca TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID y TRILIUM_OAUTH_CLIENT_SECRET.",
"oauth_missing_vars": "Ajustes faltantes: {{variables}}",
"oauth_user_account": "Cuenta de usuario: ",
"oauth_user_email": "Correo electrónico de usuario: ",
"oauth_user_not_logged_in": "¡No ha iniciado sesión!"
},
"shortcuts": { "shortcuts": {
"keyboard_shortcuts": "Atajos de teclado", "keyboard_shortcuts": "Atajos de teclado",
"multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.", "multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.",
@ -1624,9 +1431,7 @@
"widget": "Widget", "widget": "Widget",
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?", "confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?",
"geo-map": "Mapa Geo", "geo-map": "Mapa Geo",
"beta-feature": "Beta", "beta-feature": "Beta"
"ai-chat": "Chat de IA",
"task-list": "Lista de tareas"
}, },
"protect_note": { "protect_note": {
"toggle-on": "Proteger la nota", "toggle-on": "Proteger la nota",
@ -1736,9 +1541,7 @@
}, },
"clipboard": { "clipboard": {
"cut": "La(s) notas(s) han sido cortadas al portapapeles.", "cut": "La(s) notas(s) han sido cortadas al portapapeles.",
"copied": "La(s) notas(s) han sido copiadas al portapapeles.", "copied": "La(s) notas(s) han sido copiadas al portapapeles."
"copy_failed": "No se puede copiar al portapapeles debido a problemas de permisos.",
"copy_success": "Copiado al portapapeles."
}, },
"entrypoints": { "entrypoints": {
"note-revision-created": "Una revisión de nota ha sido creada.", "note-revision-created": "Una revisión de nota ha sido creada.",
@ -1781,7 +1584,7 @@
"auto-detect-language": "Detectado automáticamente" "auto-detect-language": "Detectado automáticamente"
}, },
"highlighting": { "highlighting": {
"title": "Bloques de código", "title": "",
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.", "description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",
"color-scheme": "Esquema de color" "color-scheme": "Esquema de color"
}, },
@ -1789,8 +1592,7 @@
"word_wrapping": "Ajuste de palabras", "word_wrapping": "Ajuste de palabras",
"theme_none": "Sin resaltado de sintaxis", "theme_none": "Sin resaltado de sintaxis",
"theme_group_light": "Temas claros", "theme_group_light": "Temas claros",
"theme_group_dark": "Temas oscuros", "theme_group_dark": "Temas oscuros"
"copy_title": "Copiar al portapapeles"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Formato" "title": "Formato"
@ -1911,22 +1713,5 @@
}, },
"png_export_button": { "png_export_button": {
"button_title": "Exportar diagrama como PNG" "button_title": "Exportar diagrama como PNG"
},
"svg": {
"export_to_png": "El diagrama no pudo ser exportado a PNG."
},
"code_theme": {
"title": "Apariencia",
"word_wrapping": "Ajuste de palabras",
"color-scheme": "Esquema de color"
},
"cpu_arch_warning": {
"title": "Por favor descargue la versión ARM64",
"message_macos": "TriliumNext está siendo ejecutado bajo traducción Rosetta 2, lo que significa que está usando la versión Intel (x64) en Apple Silicon Mac. Esto impactará significativamente en el rendimiento y la vida de la batería.",
"message_windows": "TriliumNext está siendo ejecutado bajo emulación, lo que significa que está usando la version Intel (x64) en Windows en un dispositivo ARM. Esto impactará significativamente en el rendimiento y la vida de la batería.",
"recommendation": "Para la mejor experiencia, por favor descargue la versión nativa ARM64 de TriliumNext desde nuestra página de lanzamientos.",
"download_link": "Descargar versión nativa",
"continue_anyway": "Continuar de todas maneras",
"dont_show_again": "No mostrar esta advertencia otra vez"
} }
} }

@ -3,14 +3,7 @@ declare module "*.png" {
export default path; export default path;
} }
declare module "*?url" { declare module "@triliumnext/ckeditor5/emoji_definitions/en.json?url" {
var path: string; var path: string;
export default path; export default path;
} }
declare module "*?raw" {
var content: string;
export default content;
}
declare module "boxicons/css/boxicons.min.css" { }

@ -57,8 +57,6 @@ declare global {
process?: ElectronProcess; process?: ElectronProcess;
glob?: CustomGlobals; glob?: CustomGlobals;
EXCALIDRAW_ASSET_PATH?: string;
} }
interface AutoCompleteConfig { interface AutoCompleteConfig {

@ -1,16 +0,0 @@
/// <reference types="vite/client" />
interface ViteTypeOptions {
strictImportMetaEnv: unknown
}
interface ImportMetaEnv {
/** The license key for CKEditor premium features. */
readonly VITE_CKEDITOR_KEY?: string;
/** Whether to enable the CKEditor inspector (see https://ckeditor.com/docs/ckeditor5/latest/framework/develpment-tools/inspector.html). */
readonly VITE_CKEDITOR_ENABLE_INSPECTOR?: "true" | "false";
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

@ -11,7 +11,6 @@ import utils from "../../services/utils.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import type { Attribute } from "../../services/attribute_parser.js"; import type { Attribute } from "../../services/attribute_parser.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="attr-detail tn-tool-dialog"> <div class="attr-detail tn-tool-dialog">
@ -484,7 +483,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
return; return;
} }
saveFocusedElement(); utils.saveFocusedElement();
this.attrType = this.getAttrType(attribute); this.attrType = this.getAttrType(attribute);
@ -606,7 +605,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.hide(); this.hide();
focusSavedElement(); utils.focusSavedElement();
} }
async cancelAndClose() { async cancelAndClose() {
@ -614,7 +613,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.hide(); this.hide();
focusSavedElement(); utils.focusSavedElement();
} }
userEditedAttribute() { userEditedAttribute() {

@ -4,7 +4,6 @@ import BasicWidget from "../basic_widget.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { openDialog } from "../../services/dialog.js";
interface AppInfo { interface AppInfo {
appVersion: string; appVersion: string;
@ -112,6 +111,6 @@ export default class AboutDialog extends BasicWidget {
async openAboutDialogEvent() { async openAboutDialogEvent() {
await this.refresh(); await this.refresh();
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
} }

@ -1,11 +1,11 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import type { Suggestion } from "../../services/note_autocomplete.js"; import type { Suggestion } from "../../services/note_autocomplete.js";
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js"; import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -111,7 +111,7 @@ export default class AddLinkDialog extends BasicWidget {
this.updateTitleSettingsVisibility(); this.updateTitleSettingsVisibility();
await openDialog(this.$widget); utils.openDialog(this.$widget);
this.$autoComplete.val(""); this.$autoComplete.val("");
this.$linkTitle.val(""); this.$linkTitle.val("");

@ -2,11 +2,11 @@ import treeService from "../../services/tree.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog"> const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
@ -93,7 +93,7 @@ export default class BranchPrefixDialog extends BasicWidget {
} }
await this.refresh(notePath); await this.refresh(notePath);
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
async savePrefix() { async savePrefix() {

@ -1,11 +1,11 @@
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import bulkActionService from "../../services/bulk_action.js"; import bulkActionService from "../../services/bulk_action.js";
import utils from "../../services/utils.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
@ -104,7 +104,7 @@ export default class BulkActionsDialog extends BasicWidget {
}); });
toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
closeActiveDialog(); utils.closeActiveDialog();
}); });
} }
@ -170,6 +170,6 @@ export default class BulkActionsDialog extends BasicWidget {
this.$includeDescendants.prop("checked", false); this.$includeDescendants.prop("checked", false);
await this.refresh(); await this.refresh();
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
} }

@ -1,4 +1,5 @@
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
@ -7,7 +8,6 @@ import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
@ -94,7 +94,7 @@ export default class CloneToDialog extends BasicWidget {
} }
} }
openDialog(this.$widget); utils.openDialog(this.$widget);
this.$noteAutoComplete.val("").trigger("focus"); this.$noteAutoComplete.val("").trigger("focus");
this.$noteList.empty(); this.$noteList.empty();

@ -1,10 +1,10 @@
import server from "../../services/server.js"; import server from "../../services/server.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { FAttributeRow } from "../../entities/fattribute.js"; import type { FAttributeRow } from "../../entities/fattribute.js";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
// TODO: Use common with server. // TODO: Use common with server.
interface Response { interface Response {
@ -119,13 +119,13 @@ export default class DeleteNotesDialog extends BasicWidget {
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus")); this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
this.$cancelButton.on("click", () => { this.$cancelButton.on("click", () => {
closeActiveDialog(); utils.closeActiveDialog();
this.resolve({ proceed: false }); this.resolve({ proceed: false });
}); });
this.$okButton.on("click", () => { this.$okButton.on("click", () => {
closeActiveDialog(); utils.closeActiveDialog();
this.resolve({ this.resolve({
proceed: true, proceed: true,
@ -179,7 +179,7 @@ export default class DeleteNotesDialog extends BasicWidget {
await this.renderDeletePreview(); await this.renderDeletePreview();
openDialog(this.$widget); utils.openDialog(this.$widget);
this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones); this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones);

@ -8,7 +8,6 @@ import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -215,7 +214,7 @@ export default class ExportDialog extends BasicWidget {
this.$widget.find(".opml-v2").prop("checked", true); // setting default this.$widget.find(".opml-v2").prop("checked", true); // setting default
openDialog(this.$widget); utils.openDialog(this.$widget);
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);

@ -1,6 +1,6 @@
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog"> <div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog">
@ -155,6 +155,6 @@ export default class HelpDialog extends BasicWidget {
} }
showCheatsheetEvent() { showCheatsheetEvent() {
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
} }

@ -1,4 +1,4 @@
import { escapeQuotes } from "../../services/utils.js"; import utils, { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import importService, { type UploadFilesOptions } from "../../services/import.js"; import importService, { type UploadFilesOptions } from "../../services/import.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
@ -6,7 +6,6 @@ import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { Modal, Tooltip } from "bootstrap"; import { Modal, Tooltip } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -156,7 +155,7 @@ export default class ImportDialog extends BasicWidget {
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId)); this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
async importIntoNote(parentNoteId: string) { async importIntoNote(parentNoteId: string) {

@ -1,12 +1,12 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import type EditableTextTypeWidget from "../type_widgets/editable_text.js"; import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -83,7 +83,7 @@ export default class IncludeNoteDialog extends BasicWidget {
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) { async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
this.textTypeWidget = textTypeWidget; this.textTypeWidget = textTypeWidget;
await this.refresh(); await this.refresh();
openDialog(this.$widget); utils.openDialog(this.$widget);
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
} }

@ -1,59 +0,0 @@
import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap";
import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js";
const TPL = /*html*/`
<div class="cpu-arch-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${t("cpu_arch_warning.title")}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>${utils.isMac() ? t("cpu_arch_warning.message_macos") : t("cpu_arch_warning.message_windows")}</p>
<p>${t("cpu_arch_warning.recommendation")}</p>
</div>
<div class="modal-footer d-flex justify-content-between align-items-center">
<button class="download-correct-version-button btn btn-primary btn-lg me-2">
<span class="bx bx-download"></span>
${t("cpu_arch_warning.download_link")}
</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">${t("cpu_arch_warning.continue_anyway")}</button>
</div>
</div>
</div>
</div>`;
export default class IncorrectCpuArchDialog extends BasicWidget {
private modal!: Modal;
private $downloadButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$downloadButton = this.$widget.find(".download-correct-version-button");
this.$downloadButton.on("click", () => {
// Open the releases page where users can download the correct version
if (utils.isElectron()) {
const { shell } = utils.dynamicRequire("electron");
shell.openExternal("https://github.com/TriliumNext/Notes/releases/latest");
} else {
window.open("https://github.com/TriliumNext/Notes/releases/latest", "_blank");
}
});
// Auto-focus the download button when shown
this.$widget.on("shown.bs.modal", () => {
this.$downloadButton.trigger("focus");
});
}
showCpuArchWarningEvent() {
this.modal.show();
}
}

@ -1,9 +1,9 @@
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import type { ConfirmDialogCallback } from "./confirm.js"; import type { ConfirmDialogCallback } from "./confirm.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;"> <div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
@ -72,7 +72,7 @@ export default class InfoDialog extends BasicWidget {
} }
openDialog(this.$widget); utils.openDialog(this.$widget);
this.resolve = callback; this.resolve = callback;
} }

@ -5,7 +5,6 @@ import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog"> const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
@ -55,7 +54,7 @@ export default class JumpToNoteDialog extends BasicWidget {
} }
async jumpToNoteEvent() { async jumpToNoteEvent() {
const dialogPromise = openDialog(this.$widget); const dialogPromise = utils.openDialog(this.$widget);
if (utils.isMobile()) { if (utils.isMobile()) {
dialogPromise.then(($dialog) => { dialogPromise.then(($dialog) => {
const el = $dialog.find(">.modal-dialog")[0]; const el = $dialog.find(">.modal-dialog")[0];

@ -6,7 +6,6 @@ import BasicWidget from "../basic_widget.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -70,7 +69,6 @@ export default class MarkdownImportDialog extends BasicWidget {
const modelFragment = textEditor.data.toModel(viewFragment); const modelFragment = textEditor.data.toModel(viewFragment);
textEditor.model.insertContent(modelFragment, textEditor.model.document.selection); textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
textEditor.editing.view.focus();
toastService.showMessage(t("markdown_import.import_success")); toastService.showMessage(t("markdown_import.import_success"));
} }
@ -90,7 +88,7 @@ export default class MarkdownImportDialog extends BasicWidget {
this.convertMarkdownToHtml(text); this.convertMarkdownToHtml(text);
} else { } else {
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
} }

@ -1,4 +1,5 @@
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import branchService from "../../services/branches.js"; import branchService from "../../services/branches.js";
@ -6,7 +7,6 @@ import treeService from "../../services/tree.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -83,7 +83,7 @@ export default class MoveToDialog extends BasicWidget {
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) { async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
this.movedBranchIds = branchIds; this.movedBranchIds = branchIds;
openDialog(this.$widget); utils.openDialog(this.$widget);
this.$noteAutoComplete.val("").trigger("focus"); this.$noteAutoComplete.val("").trigger("focus");

@ -2,7 +2,6 @@ import type { CommandNames } from "../../components/app_context.js";
import type { MenuCommandItem } from "../../menus/context_menu.js"; import type { MenuCommandItem } from "../../menus/context_menu.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import noteTypesService from "../../services/note_types.js"; import noteTypesService from "../../services/note_types.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Dropdown, Modal } from "bootstrap"; import { Dropdown, Modal } from "bootstrap";
@ -14,11 +13,6 @@ const TPL = /*html*/`
z-index: 1100 !important; z-index: 1100 !important;
} }
.note-type-chooser-dialog .input-group {
margin-top: 15px;
margin-bottom: 15px;
}
.note-type-chooser-dialog .note-type-dropdown { .note-type-chooser-dialog .note-type-dropdown {
position: relative; position: relative;
font-size: large; font-size: large;
@ -36,12 +30,6 @@ const TPL = /*html*/`
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
${t("note_type_chooser.change_path_prompt")}
<div class="input-group">
<input class="choose-note-path form-control" placeholder="${t("note_type_chooser.search_placeholder")}">
</div>
${t("note_type_chooser.modal_body")} ${t("note_type_chooser.modal_body")}
<div class="dropdown" style="display: flex;"> <div class="dropdown" style="display: flex;">
@ -49,7 +37,7 @@ const TPL = /*html*/`
data-bs-toggle="dropdown" data-bs-display="static"> data-bs-toggle="dropdown" data-bs-display="static">
</button> </button>
<div class="note-type-dropdown dropdown-menu static"></div> <div class="note-type-dropdown dropdown-menu"></div>
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +48,6 @@ export interface ChooseNoteTypeResponse {
success: boolean; success: boolean;
noteType?: string; noteType?: string;
templateNoteId?: string; templateNoteId?: string;
notePath?: string;
} }
type Callback = (data: ChooseNoteTypeResponse) => void; type Callback = (data: ChooseNoteTypeResponse) => void;
@ -70,7 +57,6 @@ export default class NoteTypeChooserDialog extends BasicWidget {
private dropdown!: Dropdown; private dropdown!: Dropdown;
private modal!: Modal; private modal!: Modal;
private $noteTypeDropdown!: JQuery<HTMLElement>; private $noteTypeDropdown!: JQuery<HTMLElement>;
private $autoComplete!: JQuery<HTMLElement>;
private $originalFocused: JQuery<HTMLElement> | null; private $originalFocused: JQuery<HTMLElement> | null;
private $originalDialog: JQuery<HTMLElement> | null; private $originalDialog: JQuery<HTMLElement> | null;
@ -85,8 +71,7 @@ export default class NoteTypeChooserDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget[0]); this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$autoComplete = this.$widget.find(".choose-note-path");
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown"); this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]); this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]);
@ -131,20 +116,9 @@ export default class NoteTypeChooserDialog extends BasicWidget {
}); });
} }
async refresh() {
noteAutocompleteService
.initNoteAutocomplete(this.$autoComplete, {
allowCreatingNotes: false,
hideGoToSelectedNoteButton: true,
allowJumpToSearchNotes: false,
})
}
async chooseNoteTypeEvent({ callback }: { callback: Callback }) { async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
this.$originalFocused = $(":focus"); this.$originalFocused = $(":focus");
await this.refresh();
const noteTypes = await noteTypesService.getNoteTypeItems(); const noteTypes = await noteTypesService.getNoteTypeItems();
this.$noteTypeDropdown.empty(); this.$noteTypeDropdown.empty();
@ -179,14 +153,12 @@ export default class NoteTypeChooserDialog extends BasicWidget {
const $item = $(e.target).closest(".dropdown-item"); const $item = $(e.target).closest(".dropdown-item");
const noteType = $item.attr("data-note-type"); const noteType = $item.attr("data-note-type");
const templateNoteId = $item.attr("data-template-note-id"); const templateNoteId = $item.attr("data-template-note-id");
const notePath = this.$autoComplete.getSelectedNotePath() || undefined;
if (this.resolve) { if (this.resolve) {
this.resolve({ this.resolve({
success: true, success: true,
noteType, noteType,
templateNoteId, templateNoteId
notePath
}); });
} }
this.resolve = null; this.resolve = null;

@ -1,5 +1,5 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -37,6 +37,6 @@ export default class PasswordNoteSetDialog extends BasicWidget {
} }
showPasswordNotSetEvent() { showPasswordNotSetEvent() {
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
} }

@ -1,5 +1,5 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -110,6 +110,6 @@ export default class PromptDialog extends BasicWidget {
this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer)); this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer));
openDialog(this.$widget, false); utils.openDialog(this.$widget, false);
} }
} }

@ -1,6 +1,6 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -49,7 +49,7 @@ export default class ProtectedSessionPasswordDialog extends BasicWidget {
} }
showProtectedSessionPasswordDialogEvent() { showProtectedSessionPasswordDialogEvent() {
openDialog(this.$widget); utils.openDialog(this.$widget);
this.$passwordInput.trigger("focus"); this.$passwordInput.trigger("focus");
} }

@ -2,12 +2,13 @@ import { formatDateTime } from "../../utils/formatters.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import appContext, { type EventData } from "../../components/app_context.js"; import appContext, { type EventData } from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import dialogService, { openDialog } from "../../services/dialog.js"; import dialogService from "../../services/dialog.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import hoistedNoteService from "../../services/hoisted_note.js"; import hoistedNoteService from "../../services/hoisted_note.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
import ws from "../../services/ws.js"; import ws from "../../services/ws.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -61,7 +62,7 @@ export default class RecentChangesDialog extends BasicWidget {
await this.refresh(); await this.refresh();
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
async refresh() { async refresh() {

@ -6,7 +6,7 @@ import appContext from "../../components/app_context.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import protectedSessionHolder from "../../services/protected_session_holder.js"; import protectedSessionHolder from "../../services/protected_session_holder.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import dialogService, { openDialog } from "../../services/dialog.js"; import dialogService from "../../services/dialog.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js"; import type { NoteType } from "../../entities/fnote.js";
@ -182,7 +182,7 @@ export default class RevisionsDialog extends BasicWidget {
return; return;
} }
openDialog(this.$widget); utils.openDialog(this.$widget);
await this.loadRevisions(noteId); await this.loadRevisions(noteId);
} }

@ -1,7 +1,7 @@
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog"> const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -97,14 +97,14 @@ export default class SortChildNotesDialog extends BasicWidget {
await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale }); await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale });
closeActiveDialog(); utils.closeActiveDialog();
}); });
} }
async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) { async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) {
this.parentNoteId = node.data.noteId; this.parentNoteId = node.data.noteId;
openDialog(this.$widget); utils.openDialog(this.$widget);
this.$form.find("input:first").focus(); this.$form.find("input:first").focus();
} }

@ -1,12 +1,11 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { escapeQuotes } from "../../services/utils.js"; import utils, { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import importService from "../../services/import.js"; import importService from "../../services/import.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal, Tooltip } from "bootstrap"; import { Modal, Tooltip } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -99,7 +98,7 @@ export default class UploadAttachmentsDialog extends BasicWidget {
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId)); this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
openDialog(this.$widget); utils.openDialog(this.$widget);
} }
async uploadAttachments(parentNoteId: string) { async uploadAttachments(parentNoteId: string) {

@ -52,9 +52,9 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
toastService.showMessage(t("code_buttons.opening_api_docs_message")); toastService.showMessage(t("code_buttons.opening_api_docs_message"));
if (this.note?.mime.endsWith("frontend")) { if (this.note?.mime.endsWith("frontend")) {
window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html", "_blank"); window.open("https://zadam.github.io/trilium/frontend_api/FrontendScriptApi.html", "_blank");
} else { } else {
window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html", "_blank"); window.open("https://zadam.github.io/trilium/backend_api/BackendScriptApi.html", "_blank");
} }
}); });

@ -56,8 +56,6 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
return byNoteType[note.type]; return byNoteType[note.type];
} else if (note?.hasLabel("calendarRoot")) { } else if (note?.hasLabel("calendarRoot")) {
return "l0tKav7yLHGF"; return "l0tKav7yLHGF";
} else if (note?.hasLabel("textSnippet")) {
return "pwc194wlRzcH";
} else if (note && note.type === "book") { } else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""] return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
} }

@ -258,3 +258,9 @@ export async function getDirectResponse(noteId: string, messageParams: any): Pro
} }
} }
/**
* Get embedding statistics
*/
export async function getEmbeddingStats(): Promise<any> {
return server.get('llm/embeddings/stats');
}

@ -11,7 +11,7 @@ import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator,
import { formatMarkdown } from "./utils.js"; import { formatMarkdown } from "./utils.js";
import { createChatSession, checkSessionExists, setupStreamingResponse, getDirectResponse } from "./communication.js"; import { createChatSession, checkSessionExists, setupStreamingResponse, getDirectResponse } from "./communication.js";
import { extractInChatToolSteps } from "./message_processor.js"; import { extractInChatToolSteps } from "./message_processor.js";
import { validateProviders } from "./validation.js"; import { validateEmbeddingProviders } from "./validation.js";
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js"; import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
import { formatCodeBlocks } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import { ClassicEditor, type CKTextEditor, type MentionFeed } from "@triliumnext/ckeditor5"; import { ClassicEditor, type CKTextEditor, type MentionFeed } from "@triliumnext/ckeditor5";
@ -350,115 +350,6 @@ export default class LlmChatPanel extends BasicWidget {
} }
} }
/**
* Save current chat data to a specific note ID
*/
async saveCurrentDataToSpecificNote(targetNoteId: string | null) {
if (!this.onSaveData || !targetNoteId) {
console.warn('Cannot save chat data: no saveData callback or no targetNoteId available');
return;
}
try {
// Extract current tool execution steps if any exist
const toolSteps = extractInChatToolSteps(this.noteContextChatMessages);
// Get tool executions from both UI and any cached executions in metadata
let toolExecutions: Array<{
id: string;
name: string;
arguments: any;
result: any;
error?: string;
timestamp: string;
}> = [];
// First include any tool executions already in metadata (from streaming events)
if (this.metadata?.toolExecutions && Array.isArray(this.metadata.toolExecutions)) {
toolExecutions = [...this.metadata.toolExecutions];
console.log(`Including ${toolExecutions.length} tool executions from metadata`);
}
// Also extract any visible tool steps from the UI
const extractedExecutions = toolSteps.map(step => {
// Parse tool execution information
if (step.type === 'tool-execution') {
try {
const content = JSON.parse(step.content);
return {
id: content.toolCallId || `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
name: content.tool || 'unknown',
arguments: content.args || {},
result: content.result || {},
error: content.error,
timestamp: new Date().toISOString()
};
} catch (e) {
// If we can't parse it, create a basic record
return {
id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
name: 'unknown',
arguments: {},
result: step.content,
timestamp: new Date().toISOString()
};
}
} else if (step.type === 'result' && step.name) {
// Handle result steps with a name
return {
id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
name: step.name,
arguments: {},
result: step.content,
timestamp: new Date().toISOString()
};
}
return {
id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
name: 'unknown',
arguments: {},
result: 'Unrecognized tool step',
timestamp: new Date().toISOString()
};
});
// Merge the tool executions, keeping only unique IDs
const existingIds = new Set(toolExecutions.map((t: {id: string}) => t.id));
for (const exec of extractedExecutions) {
if (!existingIds.has(exec.id)) {
toolExecutions.push(exec);
existingIds.add(exec.id);
}
}
const dataToSave = {
messages: this.messages,
noteId: targetNoteId,
chatNoteId: targetNoteId, // For backward compatibility
toolSteps: toolSteps,
// Add sources if we have them
sources: this.sources || [],
// Add metadata
metadata: {
model: this.metadata?.model || undefined,
provider: this.metadata?.provider || undefined,
temperature: this.metadata?.temperature || 0.7,
lastUpdated: new Date().toISOString(),
// Add tool executions
toolExecutions: toolExecutions
}
};
console.log(`Saving chat data to specific note ${targetNoteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`);
// Save the data to the note attribute via the callback
// This is the ONLY place we should save data, letting the container widget handle persistence
await this.onSaveData(dataToSave);
} catch (error) {
console.error('Error saving chat data to specific note:', error);
}
}
/** /**
* Load saved chat data from the note attribute * Load saved chat data from the note attribute
*/ */
@ -725,7 +616,7 @@ export default class LlmChatPanel extends BasicWidget {
} }
// Check for any provider validation issues when refreshing // Check for any provider validation issues when refreshing
await validateProviders(this.validationWarning); await validateEmbeddingProviders(this.validationWarning);
// Get current note context if needed // Get current note context if needed
const currentActiveNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null; const currentActiveNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null;
@ -876,7 +767,7 @@ export default class LlmChatPanel extends BasicWidget {
*/ */
private async processUserMessage(content: string) { private async processUserMessage(content: string) {
// Check for validation issues first // Check for validation issues first
await validateProviders(this.validationWarning); await validateEmbeddingProviders(this.validationWarning);
// Make sure we have a valid session // Make sure we have a valid session
if (!this.noteId) { if (!this.noteId) {
@ -976,8 +867,8 @@ export default class LlmChatPanel extends BasicWidget {
this.showSources(postResponse.sources); this.showSources(postResponse.sources);
} }
// Process the assistant response with original chat note ID // Process the assistant response
this.processAssistantResponse(postResponse.content, postResponse, this.noteId); this.processAssistantResponse(postResponse.content, postResponse);
hideLoadingIndicator(this.loadingIndicator); hideLoadingIndicator(this.loadingIndicator);
return true; return true;
@ -993,7 +884,7 @@ export default class LlmChatPanel extends BasicWidget {
/** /**
* Process an assistant response - add to UI and save * Process an assistant response - add to UI and save
*/ */
private async processAssistantResponse(content: string, fullResponse?: any, originalChatNoteId?: string | null) { private async processAssistantResponse(content: string, fullResponse?: any) {
// Add the response to the chat UI // Add the response to the chat UI
this.addMessageToChat('assistant', content); this.addMessageToChat('assistant', content);
@ -1019,8 +910,8 @@ export default class LlmChatPanel extends BasicWidget {
]; ];
} }
// Save to note - use original chat note ID if provided // Save to note
this.saveCurrentDataToSpecificNote(originalChatNoteId || this.noteId).catch(err => { this.saveCurrentData().catch(err => {
console.error("Failed to save assistant response to note:", err); console.error("Failed to save assistant response to note:", err);
}); });
} }
@ -1045,15 +936,12 @@ export default class LlmChatPanel extends BasicWidget {
timestamp: string; timestamp: string;
}> = []; }> = [];
// Store the original chat note ID to ensure we save to the correct note even if user switches
const originalChatNoteId = this.noteId;
return setupStreamingResponse( return setupStreamingResponse(
this.noteId, this.noteId,
messageParams, messageParams,
// Content update handler // Content update handler
(content: string, isDone: boolean = false) => { (content: string, isDone: boolean = false) => {
this.updateStreamingUI(content, isDone, originalChatNoteId); this.updateStreamingUI(content, isDone);
// Update session data with additional metadata when streaming is complete // Update session data with additional metadata when streaming is complete
if (isDone) { if (isDone) {
@ -1179,13 +1067,13 @@ export default class LlmChatPanel extends BasicWidget {
/** /**
* Update the UI with streaming content * Update the UI with streaming content
*/ */
private updateStreamingUI(assistantResponse: string, isDone: boolean = false, originalChatNoteId?: string | null) { private updateStreamingUI(assistantResponse: string, isDone: boolean = false) {
// Track if we have a streaming message in progress // Track if we have a streaming message in progress
const hasStreamingMessage = !!this.noteContextChatMessages.querySelector('.assistant-message.streaming'); const hasStreamingMessage = !!this.noteContextChatMessages.querySelector('.assistant-message.streaming');
// Create a new message element or use the existing streaming one // Create a new message element or use the existing streaming one
let assistantMessageEl: HTMLElement; let assistantMessageEl: HTMLElement;
if (hasStreamingMessage) { if (hasStreamingMessage) {
// Use the existing streaming message // Use the existing streaming message
assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message.streaming')!; assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message.streaming')!;
@ -1215,7 +1103,7 @@ export default class LlmChatPanel extends BasicWidget {
if (isDone) { if (isDone) {
// Remove the streaming class to mark this message as complete // Remove the streaming class to mark this message as complete
assistantMessageEl.classList.remove('streaming'); assistantMessageEl.classList.remove('streaming');
// Apply syntax highlighting // Apply syntax highlighting
formatCodeBlocks($(assistantMessageEl as HTMLElement)); formatCodeBlocks($(assistantMessageEl as HTMLElement));
@ -1230,8 +1118,8 @@ export default class LlmChatPanel extends BasicWidget {
timestamp: new Date() timestamp: new Date()
}); });
// Save the updated message list to the original chat note // Save the updated message list
this.saveCurrentDataToSpecificNote(originalChatNoteId || this.noteId); this.saveCurrentData();
} }
// Scroll to bottom // Scroll to bottom

@ -2,12 +2,12 @@
* Validation functions for LLM Chat * Validation functions for LLM Chat
*/ */
import options from "../../services/options.js"; import options from "../../services/options.js";
import { t } from "../../services/i18n.js"; import { getEmbeddingStats } from "./communication.js";
/** /**
* Validate providers configuration * Validate embedding providers configuration
*/ */
export async function validateProviders(validationWarning: HTMLElement): Promise<void> { export async function validateEmbeddingProviders(validationWarning: HTMLElement): Promise<void> {
try { try {
// Check if AI is enabled // Check if AI is enabled
const aiEnabled = options.is('aiEnabled'); const aiEnabled = options.is('aiEnabled');
@ -38,9 +38,6 @@ export async function validateProviders(validationWarning: HTMLElement): Promise
// Check for configuration issues with providers in the precedence list // Check for configuration issues with providers in the precedence list
const configIssues: string[] = []; const configIssues: string[] = [];
// Always add experimental warning as the first item
configIssues.push(t("ai_llm.experimental_warning"));
// Check each provider in the precedence list for proper configuration // Check each provider in the precedence list for proper configuration
for (const provider of precedenceList) { for (const provider of precedenceList) {
if (provider === 'openai') { if (provider === 'openai') {
@ -65,8 +62,23 @@ export async function validateProviders(validationWarning: HTMLElement): Promise
// Add checks for other providers as needed // Add checks for other providers as needed
} }
// Show warning if there are configuration issues // Fetch embedding stats to check if there are any notes being processed
if (configIssues.length > 0) { const embeddingStats = await getEmbeddingStats() as {
success: boolean,
stats: {
totalNotesCount: number;
embeddedNotesCount: number;
queuedNotesCount: number;
failedNotesCount: number;
lastProcessedDate: string | null;
percentComplete: number;
}
};
const queuedNotes = embeddingStats?.stats?.queuedNotesCount || 0;
const hasEmbeddingsInQueue = queuedNotes > 0;
// Show warning if there are configuration issues or embeddings in queue
if (configIssues.length > 0 || hasEmbeddingsInQueue) {
let message = '<i class="bx bx-error-circle me-2"></i><strong>AI Provider Configuration Issues</strong>'; let message = '<i class="bx bx-error-circle me-2"></i><strong>AI Provider Configuration Issues</strong>';
message += '<ul class="mb-1 ps-4">'; message += '<ul class="mb-1 ps-4">';
@ -75,6 +87,11 @@ export async function validateProviders(validationWarning: HTMLElement): Promise
for (const issue of configIssues) { for (const issue of configIssues) {
message += `<li>${issue}</li>`; message += `<li>${issue}</li>`;
} }
// Show warning about embeddings queue if applicable
if (hasEmbeddingsInQueue) {
message += `<li>Currently processing embeddings for ${queuedNotes} notes. Some AI features may produce incomplete results until processing completes.</li>`;
}
message += '</ul>'; message += '</ul>';
message += '<div class="mt-2"><a href="javascript:" class="settings-link btn btn-sm btn-outline-secondary"><i class="bx bx-cog me-1"></i>Open AI Settings</a></div>'; message += '<div class="mt-2"><a href="javascript:" class="settings-link btn btn-sm btn-outline-secondary"><i class="bx bx-cog me-1"></i>Open AI Settings</a></div>';
@ -86,7 +103,7 @@ export async function validateProviders(validationWarning: HTMLElement): Promise
validationWarning.style.display = 'none'; validationWarning.style.display = 'none';
} }
} catch (error) { } catch (error) {
console.error('Error validating providers:', error); console.error('Error validating embedding providers:', error);
validationWarning.style.display = 'none'; validationWarning.style.display = 'none';
} }
} }

Some files were not shown because too many files have changed in this diff Show More