mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-28 18:42:28 +08:00
Merge branch 'develop' of https://github.com/TriliumNext/Notes into develop
This commit is contained in:
commit
acaaf1f1f4
3
.github/workflows/main-docker.yml
vendored
3
.github/workflows/main-docker.yml
vendored
@ -111,6 +111,9 @@ jobs:
|
||||
- dockerfile: Dockerfile
|
||||
platform: linux/arm/v7
|
||||
image: ubuntu-24.04-arm
|
||||
- dockerfile: Dockerfile
|
||||
platform: linux/arm/v8
|
||||
image: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.image }}
|
||||
needs:
|
||||
- test_docker
|
||||
|
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
shell: bash
|
||||
forge_platform: darwin
|
||||
- name: linux
|
||||
image: ubuntu-latest
|
||||
image: ubuntu-22.04
|
||||
shell: bash
|
||||
forge_platform: linux
|
||||
- name: windows
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
- arch: x64
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
- arch: arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -73,7 +73,7 @@ jobs:
|
||||
arch: [x64, arm64]
|
||||
include:
|
||||
- arch: x64
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
- arch: arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -44,3 +44,5 @@ upload
|
||||
|
||||
.rollup.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
/result
|
@ -36,6 +36,7 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
|
||||
* [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:
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e # Fail on any command error
|
||||
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
SERIES=${VERSION:0:4}-latest
|
||||
|
||||
sudo docker build -t triliumnext/notes:$VERSION --network host -t triliumnext/notes:$SERIES .
|
||||
|
||||
if [[ $VERSION != *"beta"* ]]; then
|
||||
sudo docker tag triliumnext/notes:$VERSION triliumnext/notes:latest
|
||||
fi
|
@ -36,12 +36,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.52.0",
|
||||
"@stylistic/eslint-plugin": "4.4.0",
|
||||
"@stylistic/eslint-plugin": "4.4.1",
|
||||
"@types/express": "5.0.1",
|
||||
"@types/node": "22.15.29",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.1.4",
|
||||
"eslint": "9.27.0",
|
||||
"@vitest/coverage-v8": "3.2.2",
|
||||
"eslint": "9.28.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.4",
|
||||
|
@ -1,12 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "1234"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
|
||||
client.global.set("authToken", response.body.authToken);
|
||||
%}
|
@ -1,43 +0,0 @@
|
||||
### Test regular API metrics endpoint (requires session authentication)
|
||||
|
||||
### Get metrics from regular API (default Prometheus format)
|
||||
GET {{triliumHost}}/api/metrics
|
||||
|
||||
> {%
|
||||
client.test("API metrics endpoint returns Prometheus format by default", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
|
||||
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
|
||||
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
|
||||
client.assert(response.body.includes("# HELP"), "Should contain HELP comments");
|
||||
client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments");
|
||||
});
|
||||
%}
|
||||
|
||||
### Get metrics in JSON format
|
||||
GET {{triliumHost}}/api/metrics?format=json
|
||||
|
||||
> {%
|
||||
client.test("API metrics endpoint returns JSON when requested", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json");
|
||||
client.assert(response.body.version, "Version info not present");
|
||||
client.assert(response.body.database, "Database info not present");
|
||||
client.assert(response.body.timestamp, "Timestamp not present");
|
||||
client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number");
|
||||
client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number");
|
||||
client.assert(response.body.noteTypes, "Note types breakdown not present");
|
||||
client.assert(response.body.attachmentTypes, "Attachment types breakdown not present");
|
||||
client.assert(response.body.statistics, "Statistics not present");
|
||||
});
|
||||
%}
|
||||
|
||||
### Test invalid format parameter
|
||||
GET {{triliumHost}}/api/metrics?format=xml
|
||||
|
||||
> {%
|
||||
client.test("Invalid format parameter returns error", function() {
|
||||
client.assert(response.status === 500, "Response status should be 500");
|
||||
client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats");
|
||||
});
|
||||
%}
|
@ -1,7 +0,0 @@
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.clipperProtocolVersion === "1.0");
|
||||
%}
|
@ -1,21 +0,0 @@
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
Authorization: Basic etapi {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.clipperProtocolVersion === "1.0");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
Authorization: Basic etapi wrong
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
Authorization: Basic wrong {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
@ -1,4 +0,0 @@
|
||||
PUT {{triliumHost}}/etapi/backup/etapi_test
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 201); %}
|
@ -1,158 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "forcedId{{$randomInt}}",
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!",
|
||||
"dateCreated": "2023-08-21 23:38:51.123+0200",
|
||||
"utcDateCreated": "2023-08-21 23:38:51.123Z"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.assert(response.body.note.noteId.startsWith("forcedId"));
|
||||
client.assert(response.body.note.title == "Hello");
|
||||
client.assert(response.body.note.dateCreated == "2023-08-21 23:38:51.123+0200");
|
||||
client.assert(response.body.note.utcDateCreated == "2023-08-21 23:38:51.123Z");
|
||||
client.assert(response.body.branch.parentNoteId == "root");
|
||||
|
||||
client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId);
|
||||
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
### Clone to another location
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"parentNoteId": "_hidden"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.assert(response.body.parentNoteId == "_hidden");
|
||||
|
||||
client.global.set("clonedBranchId", response.body.branchId);
|
||||
|
||||
client.log(`Created cloned branch ` + response.body.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.noteId == client.global.get("createdNoteId"));
|
||||
client.assert(response.body.title == "Hello");
|
||||
// order is not defined and may fail in the future
|
||||
client.assert(response.body.parentBranchIds[0] == client.global.get("clonedBranchId"))
|
||||
client.assert(response.body.parentBranchIds[1] == client.global.get("createdBranchId"));
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body == "Hi there!");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.branchId == client.global.get("createdBranchId"));
|
||||
client.assert(response.body.parentNoteId == "root");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.branchId == client.global.get("clonedBranchId"));
|
||||
client.assert(response.body.parentNoteId == "_hidden");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Content-Type: application/json
|
||||
Authorization: {{authToken}}
|
||||
|
||||
{
|
||||
"attributeId": "forcedAttributeId{{$randomInt}}",
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": true
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.assert(response.body.attributeId.startsWith("forcedAttributeId"));
|
||||
|
||||
client.global.set("createdAttributeId", response.body.attributeId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.attributeId == client.global.get("createdAttributeId"));
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attachments
|
||||
Content-Type: application/json
|
||||
Authorization: {{authToken}}
|
||||
|
||||
{
|
||||
"ownerId": "{{createdNoteId}}",
|
||||
"role": "file",
|
||||
"mime": "plain/text",
|
||||
"title": "my attachment",
|
||||
"content": "my text"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
|
||||
client.global.set("createdAttachmentId", response.body.attachmentId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.attachmentId == client.global.get("createdAttachmentId"));
|
||||
client.assert(response.body.role == "file");
|
||||
client.assert(response.body.mime == "plain/text");
|
||||
client.assert(response.body.title == "my attachment");
|
||||
%}
|
@ -1,52 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attachments
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ownerId": "{{createdNoteId}}",
|
||||
"role": "file",
|
||||
"mime": "text/plain",
|
||||
"title": "my attachment",
|
||||
"content": "text"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttachmentId", response.body.attachmentId); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
### repeat the DELETE request to test the idempotency
|
||||
|
||||
DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code === "ATTACHMENT_NOT_FOUND");
|
||||
%}
|
@ -1,52 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": true
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
### repeat the DELETE request to test the idempotency
|
||||
|
||||
DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND");
|
||||
%}
|
@ -1,87 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
### Clone to another location
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"parentNoteId": "_hidden"
|
||||
}
|
||||
|
||||
> {% client.global.set("clonedBranchId", response.body.branchId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
### repeat the DELETE request to test the idempotency
|
||||
|
||||
DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code === "BRANCH_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
@ -1,126 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": true
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
### Clone to another location
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"parentNoteId": "_hidden"
|
||||
}
|
||||
|
||||
> {% client.global.set("clonedBranchId", response.body.branchId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
### repeat the DELETE request to test the idempotency
|
||||
|
||||
DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 204, "Response status is not 204"); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code === "BRANCH_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{clonedBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code == "BRANCH_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code === "NOTE_NOT_FOUND");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 404, "Response status is not 404");
|
||||
client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND");
|
||||
%}
|
@ -1,37 +0,0 @@
|
||||
GET {{triliumHost}}/etapi/notes/root/export
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.headers.valueOf("Content-Type") == "application/zip");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/root/export?format=html
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.headers.valueOf("Content-Type") == "application/zip");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/root/export?format=markdown
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.headers.valueOf("Content-Type") == "application/zip");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/root/export?format=wrong
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code === "UNRECOGNIZED_EXPORT_FORMAT");
|
||||
%}
|
@ -1,72 +0,0 @@
|
||||
GET {{triliumHost}}/etapi/inbox/2022-01-01
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/days/2022-01-01
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/days/2022-1
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code === "DATE_INVALID");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/weeks/2022-01-01
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/weeks/2022-1
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code === "DATE_INVALID");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/months/2022-01
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/months/2022-1
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code === "MONTH_INVALID");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/years/2022
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/years/202
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code === "YEAR_INVALID");
|
||||
%}
|
@ -1,116 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello parent",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.global.set("parentNoteId", response.body.note.noteId);
|
||||
client.global.set("parentBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
### Create inheritable parent attribute
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{parentNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "",
|
||||
"isInheritable": true,
|
||||
"position": 10
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.global.set("parentAttributeId", response.body.attributeId);
|
||||
%}
|
||||
|
||||
### Create child note under root
|
||||
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello child",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.global.set("childNoteId", response.body.note.noteId);
|
||||
client.global.set("childBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
### Create child attribute
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{childNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.global.set("childAttributeId", response.body.attributeId);
|
||||
%}
|
||||
|
||||
### Clone child to parent
|
||||
|
||||
POST {{triliumHost}}/etapi/branches
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{childNoteId}}",
|
||||
"parentNoteId": "{{parentNoteId}}"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.assert(response.body.parentNoteId == client.global.get("parentNoteId"));
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{childNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
|
||||
function hasAttribute(list, attributeId) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i]["attributeId"] === attributeId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
client.log(JSON.stringify(response.body.attributes));
|
||||
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.noteId == client.global.get("childNoteId"));
|
||||
client.assert(response.body.attributes.length == 2);
|
||||
client.assert(hasAttribute(response.body.attributes, client.global.get("parentAttributeId")));
|
||||
client.assert(hasAttribute(response.body.attributes, client.global.get("childAttributeId")));
|
||||
%}
|
@ -1,61 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "GetInheritedAttributes Test Note",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.global.set("parentNoteId", response.body.note.noteId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{parentNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": true
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "{{parentNoteId}}",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.noteId == client.global.get("createdNoteId"));
|
||||
client.assert(response.body.attributes.length == 1);
|
||||
client.assert(response.body.attributes[0].attributeId == client.global.get("createdAttributeId"));
|
||||
%}
|
@ -1,25 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body === "Hi there!");
|
||||
%}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"dev": {
|
||||
"triliumHost": "http://localhost:37740"
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/notes/root/import
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/octet-stream
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
< ../db/demo.zip
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
client.assert(response.body.note.title == "Trilium Demo");
|
||||
client.assert(response.body.branch.parentNoteId == "root");
|
||||
%}
|
@ -1,34 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "1234"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 201);
|
||||
|
||||
client.global.set("testAuthToken", response.body.authToken);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/root
|
||||
Authorization: {{testAuthToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/auth/logout
|
||||
Authorization: {{testAuthToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
> {% client.assert(response.status === 204); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/root
|
||||
Authorization: {{testAuthToken}}
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
@ -1,82 +0,0 @@
|
||||
### Test ETAPI metrics endpoint
|
||||
|
||||
# First login to get a token
|
||||
POST {{triliumHost}}/etapi/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"password": "{{password}}"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.test("Login successful", function() {
|
||||
client.assert(response.status === 201, "Response status is not 201");
|
||||
client.assert(response.body.authToken, "Auth token not present");
|
||||
client.global.set("authToken", response.body.authToken);
|
||||
});
|
||||
%}
|
||||
|
||||
### Get metrics with authentication (default Prometheus format)
|
||||
GET {{triliumHost}}/etapi/metrics
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.test("Metrics endpoint returns Prometheus format by default", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
|
||||
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
|
||||
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
|
||||
client.assert(response.body.includes("# HELP"), "Should contain HELP comments");
|
||||
client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments");
|
||||
});
|
||||
%}
|
||||
|
||||
### Get metrics in JSON format
|
||||
GET {{triliumHost}}/etapi/metrics?format=json
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.test("Metrics endpoint returns JSON when requested", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json");
|
||||
client.assert(response.body.version, "Version info not present");
|
||||
client.assert(response.body.database, "Database info not present");
|
||||
client.assert(response.body.timestamp, "Timestamp not present");
|
||||
client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number");
|
||||
client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number");
|
||||
});
|
||||
%}
|
||||
|
||||
### Get metrics in Prometheus format explicitly
|
||||
GET {{triliumHost}}/etapi/metrics?format=prometheus
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.test("Metrics endpoint returns Prometheus format when requested", function() {
|
||||
client.assert(response.status === 200, "Response status is not 200");
|
||||
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
|
||||
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
|
||||
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
|
||||
});
|
||||
%}
|
||||
|
||||
### Test invalid format parameter
|
||||
GET {{triliumHost}}/etapi/metrics?format=xml
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.test("Invalid format parameter returns error", function() {
|
||||
client.assert(response.status === 400, "Response status should be 400");
|
||||
client.assert(response.body.code === "INVALID_FORMAT", "Error code should be INVALID_FORMAT");
|
||||
client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats");
|
||||
});
|
||||
%}
|
||||
|
||||
### Test without authentication (should fail)
|
||||
GET {{triliumHost}}/etapi/metrics
|
||||
|
||||
> {%
|
||||
client.test("Metrics endpoint requires authentication", function() {
|
||||
client.assert(response.status === 401, "Response status should be 401");
|
||||
});
|
||||
%}
|
@ -1,109 +0,0 @@
|
||||
GET {{triliumHost}}/etapi/notes?search=aaa
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/root
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/root
|
||||
Authorization: fakeauth
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/notes/root
|
||||
Authorization: fakeauth
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/root
|
||||
Authorization: fakeauth
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/root
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/branches/root
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/000
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/000
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{triliumHost}}/etapi/attributes/000
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/inbox/2022-02-22
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/days/2022-02-22
|
||||
Authorization: fakeauth
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/weeks/2022-02-22
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/months/2022-02
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/calendar/years/2022
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/app-info
|
||||
|
||||
> {% client.assert(response.status === 401); %}
|
||||
|
||||
### Fake URL will get a 404 even without token
|
||||
|
||||
GET {{triliumHost}}/etapi/zzzzzz
|
||||
|
||||
> {% client.assert(response.status === 404); %}
|
@ -1,4 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/refresh-note-ordering/root
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.status === 200); %}
|
@ -1,79 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attachments
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ownerId": "{{createdNoteId}}",
|
||||
"role": "file",
|
||||
"mime": "text/plain",
|
||||
"title": "my attachment",
|
||||
"content": "text"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttachmentId", response.body.attachmentId); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "CHANGED",
|
||||
"position": 999
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.body.title === "CHANGED");
|
||||
client.assert(response.body.position === 999);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ownerId": "root"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": null
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
@ -1,80 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.global.set("createdNoteId", response.body.note.noteId);
|
||||
client.global.set("createdBranchId", response.body.branch.branchId);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attributes
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "{{createdNoteId}}",
|
||||
"type": "label",
|
||||
"name": "mylabel",
|
||||
"value": "val",
|
||||
"isInheritable": true
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttributeId", response.body.attributeId); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"value": "CHANGED"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.body.value === "CHANGED");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"noteId": "root"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"value": null
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
@ -1,66 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"type": "text",
|
||||
"title": "Hello",
|
||||
"content": ""
|
||||
}
|
||||
|
||||
> {% client.global.set("createdBranchId", response.body.branch.branchId); %}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"prefix": "pref",
|
||||
"notePosition": 666,
|
||||
"isExpanded": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.prefix === 'pref');
|
||||
client.assert(response.body.notePosition === 666);
|
||||
client.assert(response.body.isExpanded === true);
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root"
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"prefix": 123
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
@ -1,83 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "code",
|
||||
"mime": "application/json",
|
||||
"content": "{}"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.title === 'Hello');
|
||||
client.assert(response.body.type === 'code');
|
||||
client.assert(response.body.mime === 'application/json');
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Wassup",
|
||||
"type": "html",
|
||||
"mime": "text/html",
|
||||
"dateCreated": "2023-08-21 23:38:51.123+0200",
|
||||
"utcDateCreated": "2023-08-21 23:38:51.123Z"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.title === 'Wassup');
|
||||
client.assert(response.body.type === 'html');
|
||||
client.assert(response.body.mime === 'text/html');
|
||||
client.assert(response.body.dateCreated == "2023-08-21 23:38:51.123+0200");
|
||||
client.assert(response.body.utcDateCreated == "2023-08-21 23:38:51.123Z");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"isProtected": true
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_NOT_ALLOWED");
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}}
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": true
|
||||
}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 400);
|
||||
client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR");
|
||||
%}
|
@ -1,23 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "code",
|
||||
"mime": "text/plain",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/notes/{{createdNoteId}}/revision
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: text/plain
|
||||
|
||||
Changed content
|
||||
|
||||
> {% client.assert(response.status === 204); %}
|
@ -1,39 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attachments
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ownerId": "{{createdNoteId}}",
|
||||
"role": "file",
|
||||
"mime": "text/plain",
|
||||
"title": "my attachment",
|
||||
"content": "text"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttachmentId", response.body.attachmentId); %}
|
||||
|
||||
###
|
||||
|
||||
PUT {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/octet-stream
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
< ../images/icon-color.png
|
||||
|
||||
> {% client.assert(response.status === 204); %}
|
@ -1,45 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "text",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
POST {{triliumHost}}/etapi/attachments
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ownerId": "{{createdNoteId}}",
|
||||
"role": "file",
|
||||
"mime": "text/plain",
|
||||
"title": "my attachment",
|
||||
"content": "text"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdAttachmentId", response.body.attachmentId); %}
|
||||
|
||||
###
|
||||
|
||||
PUT {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: text/plain
|
||||
|
||||
Changed content
|
||||
|
||||
> {% client.assert(response.status === 204); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.body === "Changed content"); %}
|
@ -1,25 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "image",
|
||||
"mime": "image/png",
|
||||
"content": ""
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/octet-stream
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
< ../images/icon-color.png
|
||||
|
||||
> {% client.assert(response.status === 204); %}
|
||||
|
@ -1,30 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "Hello",
|
||||
"type": "code",
|
||||
"mime": "text/plain",
|
||||
"content": "Hi there!"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: text/plain
|
||||
|
||||
Changed content
|
||||
|
||||
> {% client.assert(response.status === 204); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.assert(response.body === "Changed content"); %}
|
@ -1,39 +0,0 @@
|
||||
POST {{triliumHost}}/etapi/create-note
|
||||
Authorization: {{authToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"parentNoteId": "root",
|
||||
"title": "title",
|
||||
"type": "text",
|
||||
"content": "{{$uuid}}"
|
||||
}
|
||||
|
||||
> {% client.global.set("createdNoteId", response.body.note.noteId); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {% client.global.set("content", response.body); %}
|
||||
|
||||
###
|
||||
|
||||
GET {{triliumHost}}/etapi/notes?search={{content}}&debug=true
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.results.length === 1);
|
||||
%}
|
||||
|
||||
### Same but with fast search which doesn't look in the content so 0 notes should be found
|
||||
|
||||
GET {{triliumHost}}/etapi/notes?search={{content}}&fastSearch=true
|
||||
Authorization: {{authToken}}
|
||||
|
||||
> {%
|
||||
client.assert(response.status === 200);
|
||||
client.assert(response.body.results.length === 0);
|
||||
%}
|
@ -10,7 +10,7 @@
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.27.0",
|
||||
"@eslint/js": "9.28.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.17",
|
||||
"@fullcalendar/daygrid": "6.1.17",
|
||||
@ -64,9 +64,9 @@
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/react": "19.1.6",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"happy-dom": "17.5.6",
|
||||
"happy-dom": "17.6.3",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.0.0"
|
||||
},
|
||||
|
@ -269,14 +269,32 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
return true;
|
||||
}
|
||||
|
||||
// Store the initial decision about read-only status in the viewScope
|
||||
// This will be "remembered" until the viewScope is refreshed
|
||||
if (!this.viewScope) {
|
||||
this.resetViewScope();
|
||||
}
|
||||
|
||||
const viewScope = this.viewScope!;
|
||||
|
||||
if (viewScope.isReadOnly === undefined) {
|
||||
const blob = await this.note.getBlob();
|
||||
if (!blob) {
|
||||
viewScope.isReadOnly = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode");
|
||||
const sizeLimit = this.note.type === "text"
|
||||
? options.getInt("autoReadonlySizeText")
|
||||
: options.getInt("autoReadonlySizeCode");
|
||||
|
||||
return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled");
|
||||
viewScope.isReadOnly = Boolean(sizeLimit &&
|
||||
blob.contentLength > sizeLimit &&
|
||||
!this.note.isLabelTruthy("autoReadOnlyDisabled"));
|
||||
}
|
||||
|
||||
// Return the cached decision, which won't change until viewScope is reset
|
||||
return viewScope.isReadOnly || false;
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
|
@ -192,13 +192,16 @@ class ContextMenu {
|
||||
// it's important to stop the propagation especially for sub-menus, otherwise the event
|
||||
// might be handled again by top-level menu
|
||||
return false;
|
||||
})
|
||||
.on("mouseup", (e) =>{
|
||||
});
|
||||
|
||||
if (!this.isMobile) {
|
||||
$item.on("mouseup", (e) =>{
|
||||
e.stopPropagation();
|
||||
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
|
||||
this.hide();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
|
||||
$item.addClass("disabled");
|
||||
|
@ -8,7 +8,7 @@ interface Entity {
|
||||
export interface EntityChange {
|
||||
id?: number | null;
|
||||
noteId?: string;
|
||||
entityName: EntityRowNames;
|
||||
entityName: EntityType;
|
||||
entityId: string;
|
||||
entity?: Entity;
|
||||
positions?: Record<string, number>;
|
||||
@ -22,3 +22,5 @@ export interface EntityChange {
|
||||
changeId?: string | null;
|
||||
instanceId?: string | null;
|
||||
}
|
||||
|
||||
export type EntityType = "notes" | "branches" | "attributes" | "note_reordering" | "revisions" | "options" | "attachments" | "blobs" | "etapi_tokens" | "note_embeddings";
|
||||
|
@ -35,8 +35,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addOption(attributeEntity.name);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
processAttachment(loadResults, ec);
|
||||
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
|
||||
// NOOP
|
||||
} 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
|
||||
} else {
|
||||
throw new Error(`Unknown entityName '${ec.entityName}'`);
|
||||
}
|
||||
|
@ -115,6 +115,7 @@ function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
||||
export default {
|
||||
updateDisplayedShortcuts,
|
||||
setupActionsForElement,
|
||||
getAction,
|
||||
getActions,
|
||||
getActionsForScope
|
||||
};
|
||||
|
@ -16,4 +16,24 @@ describe("Link", () => {
|
||||
const output = parseNavigationStateFromUrl(`#root/WWaBNf3SSA1b/mQ2tIzLVFKHL`);
|
||||
expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" });
|
||||
});
|
||||
|
||||
it("parses notePath with spaces", () => {
|
||||
const output = parseNavigationStateFromUrl(` #root/WWaBNf3SSA1b/mQ2tIzLVFKHL`);
|
||||
expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" });
|
||||
});
|
||||
|
||||
it("ignores external URL with internal hash anchor", () => {
|
||||
const output = parseNavigationStateFromUrl(`https://en.wikipedia.org/wiki/Bearded_Collie#Health`);
|
||||
expect(output).toMatchObject({});
|
||||
});
|
||||
|
||||
it("ignores malformed but hash-containing external URL", () => {
|
||||
const output = parseNavigationStateFromUrl("https://abc.com/#drop?searchString=firefox");
|
||||
expect(output).toStrictEqual({});
|
||||
});
|
||||
|
||||
it("ignores non-hash internal path", () => {
|
||||
const output = parseNavigationStateFromUrl("/root/abc123");
|
||||
expect(output).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
@ -48,6 +48,13 @@ export interface ViewScope {
|
||||
viewMode?: ViewMode;
|
||||
attachmentId?: string;
|
||||
readOnlyTemporarilyDisabled?: boolean;
|
||||
/**
|
||||
* If true, it indicates that the note in the view should be opened in read-only mode (for supported note types such as text or code).
|
||||
*
|
||||
* The reason why we store this information here is that a note can become read-only as the user types content in it, and we wouldn't want
|
||||
* to immediately enter read-only mode.
|
||||
*/
|
||||
isReadOnly?: boolean;
|
||||
highlightsListPreviousVisible?: boolean;
|
||||
highlightsListTemporarilyHidden?: boolean;
|
||||
tocTemporarilyHidden?: boolean;
|
||||
@ -204,11 +211,17 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
url = url.trim();
|
||||
const hashIdx = url.indexOf("#");
|
||||
if (hashIdx === -1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Exclude external links that contain #
|
||||
if (hashIdx !== 0 && !url.includes("/#root") && !url.includes("/#?searchString")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const hash = url.substr(hashIdx + 1); // strip also the initial '#'
|
||||
let [notePath, paramString] = hash.split("?");
|
||||
|
||||
|
@ -44,9 +44,17 @@ interface OptionRow {}
|
||||
|
||||
interface NoteReorderingRow {}
|
||||
|
||||
interface ContentNoteIdToComponentIdRow {
|
||||
interface NoteEmbeddingRow {
|
||||
embedId: string;
|
||||
noteId: string;
|
||||
componentId: string;
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
dimension: number;
|
||||
version: number;
|
||||
dateCreated: string;
|
||||
utcDateCreated: string;
|
||||
dateModified: string;
|
||||
utcDateModified: string;
|
||||
}
|
||||
|
||||
type EntityRowMappings = {
|
||||
@ -56,6 +64,7 @@ type EntityRowMappings = {
|
||||
options: OptionRow;
|
||||
revisions: RevisionRow;
|
||||
note_reordering: NoteReorderingRow;
|
||||
note_embeddings: NoteEmbeddingRow;
|
||||
};
|
||||
|
||||
export type EntityRowNames = keyof EntityRowMappings;
|
||||
|
@ -124,9 +124,13 @@ function formatDateISO(date: Date) {
|
||||
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
|
||||
}
|
||||
|
||||
function formatDateTime(date: Date) {
|
||||
function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
if (userSuppliedFormat?.trim()) {
|
||||
return dayjs(date).format(userSuppliedFormat);
|
||||
} else {
|
||||
return `${formatDate(date)} ${formatTime(date)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function localNowDateTime() {
|
||||
return dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZZ");
|
||||
|
@ -326,6 +326,7 @@ button kbd {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
--bs-dropdown-zindex: 999;
|
||||
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu {
|
||||
|
@ -70,6 +70,7 @@
|
||||
|
||||
--scrollbar-border-color: #666;
|
||||
--scrollbar-background-color: #333;
|
||||
--selection-background-color: #3399FF70;
|
||||
--tooltip-background-color: #333;
|
||||
--link-color: lightskyblue;
|
||||
|
||||
|
@ -74,6 +74,7 @@ html {
|
||||
|
||||
--scrollbar-border-color: #ddd;
|
||||
--scrollbar-background-color: #ddd;
|
||||
--selection-background-color: #3399FF70;
|
||||
--tooltip-background-color: #f8f8f8;
|
||||
--link-color: blue;
|
||||
|
||||
|
@ -108,6 +108,25 @@ div.editability-dropdown a.dropdown-item {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/*
|
||||
* Edited notes (for calendar notes)
|
||||
*/
|
||||
|
||||
/* The path of the note */
|
||||
.edited-notes-list small {
|
||||
margin-inline-start: 4px;
|
||||
font-size: inherit;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.edited-notes-list small::before {
|
||||
content: "(";
|
||||
}
|
||||
|
||||
.edited-notes-list small::after {
|
||||
content: ")";
|
||||
}
|
||||
|
||||
/*
|
||||
* Owned attributes
|
||||
*/
|
||||
|
@ -1402,6 +1402,7 @@ div.floating-buttons .show-floating-buttons-button:active {
|
||||
div.floating-buttons-children .close-floating-buttons-button::before,
|
||||
div.floating-buttons .show-floating-buttons-button::before {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* "Show buttons" button */
|
||||
|
@ -1431,6 +1431,12 @@
|
||||
"label": "Automatic read-only size (text notes)",
|
||||
"unit": "characters"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Custom Date/Time Format",
|
||||
"description": "Customize the format of the date and time inserted via <kbd></kbd> or the toolbar. See <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a> for available format tokens.",
|
||||
"format_string": "Format string:",
|
||||
"formatted_time": "Formatted date/time:"
|
||||
},
|
||||
"i18n": {
|
||||
"title": "Localization",
|
||||
"language": "Language",
|
||||
|
@ -6,8 +6,10 @@ import type { SessionResponse } from "./types.js";
|
||||
|
||||
/**
|
||||
* Create a new chat session
|
||||
* @param currentNoteId - Optional current note ID for context
|
||||
* @returns The noteId of the created chat note
|
||||
*/
|
||||
export async function createChatSession(currentNoteId?: string): Promise<{chatNoteId: string | null, noteId: string | null}> {
|
||||
export async function createChatSession(currentNoteId?: string): Promise<string | null> {
|
||||
try {
|
||||
const resp = await server.post<SessionResponse>('llm/chat', {
|
||||
title: 'Note Chat',
|
||||
@ -15,48 +17,42 @@ export async function createChatSession(currentNoteId?: string): Promise<{chatNo
|
||||
});
|
||||
|
||||
if (resp && resp.id) {
|
||||
// The backend might provide the noteId separately from the chatNoteId
|
||||
// If noteId is provided, use it; otherwise, we'll need to query for it separately
|
||||
return {
|
||||
chatNoteId: resp.id,
|
||||
noteId: resp.noteId || null
|
||||
};
|
||||
// Backend returns the chat note ID as 'id'
|
||||
return resp.id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create chat session:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
chatNoteId: null,
|
||||
noteId: null
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session exists
|
||||
* Check if a chat note exists
|
||||
* @param noteId - The ID of the chat note
|
||||
*/
|
||||
export async function checkSessionExists(chatNoteId: string): Promise<boolean> {
|
||||
export async function checkSessionExists(noteId: string): Promise<boolean> {
|
||||
try {
|
||||
// Validate that we have a proper note ID format, not a session ID
|
||||
// Note IDs in Trilium are typically longer or in a different format
|
||||
if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) {
|
||||
console.warn(`Invalid note ID format detected: ${chatNoteId} appears to be a legacy session ID`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const sessionCheck = await server.getWithSilentNotFound<any>(`llm/chat/${chatNoteId}`);
|
||||
const sessionCheck = await server.getWithSilentNotFound<any>(`llm/chat/${noteId}`);
|
||||
return !!(sessionCheck && sessionCheck.id);
|
||||
} catch (error: any) {
|
||||
console.log(`Error checking chat note ${chatNoteId}:`, error);
|
||||
console.log(`Error checking chat note ${noteId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up streaming response via WebSocket
|
||||
* @param noteId - The ID of the chat note
|
||||
* @param messageParams - Message parameters
|
||||
* @param onContentUpdate - Callback for content updates
|
||||
* @param onThinkingUpdate - Callback for thinking updates
|
||||
* @param onToolExecution - Callback for tool execution
|
||||
* @param onComplete - Callback for completion
|
||||
* @param onError - Callback for errors
|
||||
*/
|
||||
export async function setupStreamingResponse(
|
||||
chatNoteId: string,
|
||||
noteId: string,
|
||||
messageParams: any,
|
||||
onContentUpdate: (content: string, isDone?: boolean) => void,
|
||||
onThinkingUpdate: (thinking: string) => void,
|
||||
@ -64,35 +60,24 @@ export async function setupStreamingResponse(
|
||||
onComplete: () => void,
|
||||
onError: (error: Error) => void
|
||||
): Promise<void> {
|
||||
// Validate that we have a proper note ID format, not a session ID
|
||||
if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) {
|
||||
console.error(`Invalid note ID format: ${chatNoteId} appears to be a legacy session ID`);
|
||||
onError(new Error("Invalid note ID format - using a legacy session ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let assistantResponse = '';
|
||||
let postToolResponse = ''; // Separate accumulator for post-tool execution content
|
||||
let receivedAnyContent = false;
|
||||
let receivedPostToolContent = false; // Track if we've started receiving post-tool content
|
||||
let timeoutId: number | null = null;
|
||||
let initialTimeoutId: number | null = null;
|
||||
let cleanupTimeoutId: number | null = null;
|
||||
let receivedAnyMessage = false;
|
||||
let toolsExecuted = false; // Flag to track if tools were executed in this session
|
||||
let toolExecutionCompleted = false; // Flag to track if tool execution is completed
|
||||
let eventListener: ((event: Event) => void) | null = null;
|
||||
let lastMessageTimestamp = 0;
|
||||
|
||||
// Create a unique identifier for this response process
|
||||
const responseId = `llm-stream-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${chatNoteId}`);
|
||||
console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${noteId}`);
|
||||
|
||||
// Send the initial request to initiate streaming
|
||||
(async () => {
|
||||
try {
|
||||
const streamResponse = await server.post<any>(`llm/chat/${chatNoteId}/messages/stream`, {
|
||||
const streamResponse = await server.post<any>(`llm/chat/${noteId}/messages/stream`, {
|
||||
content: messageParams.content,
|
||||
useAdvancedContext: messageParams.useAdvancedContext,
|
||||
showThinking: messageParams.showThinking,
|
||||
@ -129,28 +114,14 @@ export async function setupStreamingResponse(
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Function to schedule cleanup with ability to cancel
|
||||
const scheduleCleanup = (delay: number) => {
|
||||
// Clear any existing cleanup timeout
|
||||
if (cleanupTimeoutId) {
|
||||
window.clearTimeout(cleanupTimeoutId);
|
||||
}
|
||||
|
||||
console.log(`[${responseId}] Scheduling listener cleanup in ${delay}ms`);
|
||||
|
||||
// Set new cleanup timeout
|
||||
cleanupTimeoutId = window.setTimeout(() => {
|
||||
// Only clean up if no messages received recently (in last 2 seconds)
|
||||
const timeSinceLastMessage = Date.now() - lastMessageTimestamp;
|
||||
if (timeSinceLastMessage > 2000) {
|
||||
// Set initial timeout to catch cases where no message is received at all
|
||||
initialTimeoutId = window.setTimeout(() => {
|
||||
if (!receivedAnyMessage) {
|
||||
console.error(`[${responseId}] No initial message received within timeout`);
|
||||
performCleanup();
|
||||
} else {
|
||||
console.log(`[${responseId}] Received message recently, delaying cleanup`);
|
||||
// Reschedule cleanup
|
||||
scheduleCleanup(2000);
|
||||
reject(new Error('No response received from server'));
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
}, 10000);
|
||||
|
||||
// Create a message handler for CustomEvents
|
||||
eventListener = (event: Event) => {
|
||||
@ -158,7 +129,7 @@ export async function setupStreamingResponse(
|
||||
const message = customEvent.detail;
|
||||
|
||||
// Only process messages for our chat note
|
||||
if (!message || message.chatNoteId !== chatNoteId) {
|
||||
if (!message || message.chatNoteId !== noteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -172,12 +143,12 @@ export async function setupStreamingResponse(
|
||||
cleanupTimeoutId = null;
|
||||
}
|
||||
|
||||
console.log(`[${responseId}] LLM Stream message received via CustomEvent: chatNoteId=${chatNoteId}, content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}, type=${message.type || 'llm-stream'}`);
|
||||
console.log(`[${responseId}] LLM Stream message received: content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}`);
|
||||
|
||||
// Mark first message received
|
||||
if (!receivedAnyMessage) {
|
||||
receivedAnyMessage = true;
|
||||
console.log(`[${responseId}] First message received for chat note ${chatNoteId}`);
|
||||
console.log(`[${responseId}] First message received for chat note ${noteId}`);
|
||||
|
||||
// Clear the initial timeout since we've received a message
|
||||
if (initialTimeoutId !== null) {
|
||||
@ -186,109 +157,33 @@ export async function setupStreamingResponse(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle specific message types
|
||||
if (message.type === 'tool_execution_start') {
|
||||
toolsExecuted = true; // Mark that tools were executed
|
||||
onThinkingUpdate('Executing tools...');
|
||||
// Also trigger tool execution UI with a specific format
|
||||
onToolExecution({
|
||||
action: 'start',
|
||||
tool: 'tools',
|
||||
result: 'Executing tools...'
|
||||
});
|
||||
return; // Skip accumulating content from this message
|
||||
// Handle error
|
||||
if (message.error) {
|
||||
console.error(`[${responseId}] Stream error: ${message.error}`);
|
||||
performCleanup();
|
||||
reject(new Error(message.error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'tool_result' && message.toolExecution) {
|
||||
toolsExecuted = true; // Mark that tools were executed
|
||||
console.log(`[${responseId}] Processing tool result: ${JSON.stringify(message.toolExecution)}`);
|
||||
|
||||
// If tool execution doesn't have an action, add 'result' as the default
|
||||
if (!message.toolExecution.action) {
|
||||
message.toolExecution.action = 'result';
|
||||
// Handle thinking updates - only show if showThinking is enabled
|
||||
if (message.thinking && messageParams.showThinking) {
|
||||
console.log(`[${responseId}] Received thinking: ${message.thinking.substring(0, 100)}...`);
|
||||
onThinkingUpdate(message.thinking);
|
||||
}
|
||||
|
||||
// First send a 'start' action to ensure the container is created
|
||||
onToolExecution({
|
||||
action: 'start',
|
||||
tool: 'tools',
|
||||
result: 'Tool execution initialized'
|
||||
});
|
||||
|
||||
// Then send the actual tool execution data
|
||||
// Handle tool execution updates
|
||||
if (message.toolExecution) {
|
||||
console.log(`[${responseId}] Tool execution update:`, message.toolExecution);
|
||||
onToolExecution(message.toolExecution);
|
||||
|
||||
// Mark tool execution as completed if this is a result or error
|
||||
if (message.toolExecution.action === 'result' || message.toolExecution.action === 'complete' || message.toolExecution.action === 'error') {
|
||||
toolExecutionCompleted = true;
|
||||
console.log(`[${responseId}] Tool execution completed`);
|
||||
}
|
||||
|
||||
return; // Skip accumulating content from this message
|
||||
}
|
||||
|
||||
if (message.type === 'tool_execution_error' && message.toolExecution) {
|
||||
toolsExecuted = true; // Mark that tools were executed
|
||||
toolExecutionCompleted = true; // Mark tool execution as completed
|
||||
onToolExecution({
|
||||
...message.toolExecution,
|
||||
action: 'error',
|
||||
error: message.toolExecution.error || 'Unknown error during tool execution'
|
||||
});
|
||||
return; // Skip accumulating content from this message
|
||||
}
|
||||
|
||||
if (message.type === 'tool_completion_processing') {
|
||||
toolsExecuted = true; // Mark that tools were executed
|
||||
toolExecutionCompleted = true; // Tools are done, now processing the result
|
||||
onThinkingUpdate('Generating response with tool results...');
|
||||
// Also trigger tool execution UI with a specific format
|
||||
onToolExecution({
|
||||
action: 'generating',
|
||||
tool: 'tools',
|
||||
result: 'Generating response with tool results...'
|
||||
});
|
||||
return; // Skip accumulating content from this message
|
||||
}
|
||||
|
||||
// Handle content updates
|
||||
if (message.content) {
|
||||
console.log(`[${responseId}] Received content chunk of length ${message.content.length}, preview: "${message.content.substring(0, 50)}${message.content.length > 50 ? '...' : ''}"`);
|
||||
|
||||
// If tools were executed and completed, and we're now getting new content,
|
||||
// this is likely the final response after tool execution from Anthropic
|
||||
if (toolsExecuted && toolExecutionCompleted && message.content) {
|
||||
console.log(`[${responseId}] Post-tool execution content detected`);
|
||||
|
||||
// If this is the first post-tool chunk, indicate we're starting a new response
|
||||
if (!receivedPostToolContent) {
|
||||
receivedPostToolContent = true;
|
||||
postToolResponse = ''; // Clear any previous post-tool response
|
||||
console.log(`[${responseId}] First post-tool content chunk, starting fresh accumulation`);
|
||||
}
|
||||
|
||||
// Accumulate post-tool execution content
|
||||
postToolResponse += message.content;
|
||||
console.log(`[${responseId}] Accumulated post-tool content, now ${postToolResponse.length} chars`);
|
||||
|
||||
// Update the UI with the accumulated post-tool content
|
||||
// This replaces the pre-tool content with our accumulated post-tool content
|
||||
onContentUpdate(postToolResponse, message.done || false);
|
||||
} else {
|
||||
// Standard content handling for non-tool cases or initial tool response
|
||||
|
||||
// Check if this is a duplicated message containing the same content we already have
|
||||
if (message.done && assistantResponse.includes(message.content)) {
|
||||
console.log(`[${responseId}] Ignoring duplicated content in done message`);
|
||||
} else {
|
||||
// Add to our accumulated response
|
||||
// Simply append the new content - no complex deduplication
|
||||
assistantResponse += message.content;
|
||||
}
|
||||
|
||||
// Update the UI immediately with each chunk
|
||||
onContentUpdate(assistantResponse, message.done || false);
|
||||
}
|
||||
|
||||
receivedAnyContent = true;
|
||||
|
||||
// Reset timeout since we got content
|
||||
@ -298,151 +193,33 @@ export async function setupStreamingResponse(
|
||||
|
||||
// Set new timeout
|
||||
timeoutId = window.setTimeout(() => {
|
||||
console.warn(`[${responseId}] Stream timeout for chat note ${chatNoteId}`);
|
||||
|
||||
// Clean up
|
||||
console.warn(`[${responseId}] Stream timeout for chat note ${noteId}`);
|
||||
performCleanup();
|
||||
reject(new Error('Stream timeout'));
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Handle tool execution updates (legacy format and standard format with llm-stream type)
|
||||
if (message.toolExecution) {
|
||||
// Only process if we haven't already handled this message via specific message types
|
||||
if (message.type === 'llm-stream' || !message.type) {
|
||||
console.log(`[${responseId}] Received tool execution update: action=${message.toolExecution.action || 'unknown'}`);
|
||||
toolsExecuted = true; // Mark that tools were executed
|
||||
|
||||
// Mark tool execution as completed if this is a result or error
|
||||
if (message.toolExecution.action === 'result' ||
|
||||
message.toolExecution.action === 'complete' ||
|
||||
message.toolExecution.action === 'error') {
|
||||
toolExecutionCompleted = true;
|
||||
console.log(`[${responseId}] Tool execution completed via toolExecution message`);
|
||||
}
|
||||
|
||||
onToolExecution(message.toolExecution);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle tool calls from the raw data or direct in message (OpenAI format)
|
||||
const toolCalls = message.tool_calls || (message.raw && message.raw.tool_calls);
|
||||
if (toolCalls && Array.isArray(toolCalls)) {
|
||||
console.log(`[${responseId}] Received tool calls: ${toolCalls.length} tools`);
|
||||
toolsExecuted = true; // Mark that tools were executed
|
||||
|
||||
// First send a 'start' action to ensure the container is created
|
||||
onToolExecution({
|
||||
action: 'start',
|
||||
tool: 'tools',
|
||||
result: 'Tool execution initialized'
|
||||
});
|
||||
|
||||
// Then process each tool call
|
||||
for (const toolCall of toolCalls) {
|
||||
let args = toolCall.function?.arguments || {};
|
||||
|
||||
// Try to parse arguments if they're a string
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args);
|
||||
} catch (e) {
|
||||
console.log(`[${responseId}] Could not parse tool arguments as JSON: ${e}`);
|
||||
args = { raw: args };
|
||||
}
|
||||
}
|
||||
|
||||
onToolExecution({
|
||||
action: 'executing',
|
||||
tool: toolCall.function?.name || 'unknown',
|
||||
toolCallId: toolCall.id,
|
||||
args: args
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle thinking state updates
|
||||
if (message.thinking) {
|
||||
console.log(`[${responseId}] Received thinking update: ${message.thinking.substring(0, 50)}...`);
|
||||
onThinkingUpdate(message.thinking);
|
||||
}
|
||||
|
||||
// Handle completion
|
||||
if (message.done) {
|
||||
console.log(`[${responseId}] Stream completed for chat note ${chatNoteId}, has content: ${!!message.content}, content length: ${message.content?.length || 0}, current response: ${assistantResponse.length} chars`);
|
||||
console.log(`[${responseId}] Stream completed for chat note ${noteId}, final response: ${assistantResponse.length} chars`);
|
||||
|
||||
// Dump message content to console for debugging
|
||||
if (message.content) {
|
||||
console.log(`[${responseId}] CONTENT IN DONE MESSAGE (first 200 chars): "${message.content.substring(0, 200)}..."`);
|
||||
|
||||
// Check if the done message contains the exact same content as our accumulated response
|
||||
// We normalize by removing whitespace to avoid false negatives due to spacing differences
|
||||
const normalizedMessage = message.content.trim();
|
||||
const normalizedResponse = assistantResponse.trim();
|
||||
|
||||
if (normalizedMessage === normalizedResponse) {
|
||||
console.log(`[${responseId}] Final message is identical to accumulated response, no need to update`);
|
||||
}
|
||||
// If the done message is longer but contains our accumulated response, use the done message
|
||||
else if (normalizedMessage.includes(normalizedResponse) && normalizedMessage.length > normalizedResponse.length) {
|
||||
console.log(`[${responseId}] Final message is more complete than accumulated response, using it`);
|
||||
assistantResponse = message.content;
|
||||
}
|
||||
// If the done message is different and not already included, append it to avoid duplication
|
||||
else if (!normalizedResponse.includes(normalizedMessage) && normalizedMessage.length > 0) {
|
||||
console.log(`[${responseId}] Final message has unique content, using it`);
|
||||
assistantResponse = message.content;
|
||||
}
|
||||
// Otherwise, we already have the content accumulated, so no need to update
|
||||
else {
|
||||
console.log(`[${responseId}] Already have this content accumulated, not updating`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear timeout if set
|
||||
// Clear all timeouts
|
||||
if (timeoutId !== null) {
|
||||
window.clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
// Always mark as done when we receive the done flag
|
||||
onContentUpdate(assistantResponse, true);
|
||||
|
||||
// Set a longer delay before cleanup to allow for post-tool execution messages
|
||||
// Especially important for Anthropic which may send final message after tool execution
|
||||
const cleanupDelay = toolsExecuted ? 15000 : 1000; // 15 seconds if tools were used, otherwise 1 second
|
||||
console.log(`[${responseId}] Setting cleanup delay of ${cleanupDelay}ms since toolsExecuted=${toolsExecuted}`);
|
||||
scheduleCleanup(cleanupDelay);
|
||||
// Schedule cleanup after a brief delay to ensure all processing is complete
|
||||
cleanupTimeoutId = window.setTimeout(() => {
|
||||
performCleanup();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// Register event listener for the custom event
|
||||
try {
|
||||
// Register the event listener for WebSocket messages
|
||||
window.addEventListener('llm-stream-message', eventListener);
|
||||
console.log(`[${responseId}] Event listener added for llm-stream-message events`);
|
||||
} catch (err) {
|
||||
console.error(`[${responseId}] Error setting up event listener:`, err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial timeout for receiving any message
|
||||
initialTimeoutId = window.setTimeout(() => {
|
||||
console.warn(`[${responseId}] No messages received for initial period in chat note ${chatNoteId}`);
|
||||
if (!receivedAnyMessage) {
|
||||
console.error(`[${responseId}] WebSocket connection not established for chat note ${chatNoteId}`);
|
||||
|
||||
if (timeoutId !== null) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
cleanupEventListener(eventListener);
|
||||
|
||||
// Show error message to user
|
||||
reject(new Error('WebSocket connection not established'));
|
||||
}
|
||||
}, 10000);
|
||||
console.log(`[${responseId}] Event listener registered, waiting for messages...`);
|
||||
});
|
||||
}
|
||||
|
||||
@ -463,15 +240,9 @@ function cleanupEventListener(listener: ((event: Event) => void) | null): void {
|
||||
/**
|
||||
* Get a direct response from the server without streaming
|
||||
*/
|
||||
export async function getDirectResponse(chatNoteId: string, messageParams: any): Promise<any> {
|
||||
export async function getDirectResponse(noteId: string, messageParams: any): Promise<any> {
|
||||
try {
|
||||
// Validate that we have a proper note ID format, not a session ID
|
||||
if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) {
|
||||
console.error(`Invalid note ID format: ${chatNoteId} appears to be a legacy session ID`);
|
||||
throw new Error("Invalid note ID format - using a legacy session ID");
|
||||
}
|
||||
|
||||
const postResponse = await server.post<any>(`llm/chat/${chatNoteId}/messages`, {
|
||||
const postResponse = await server.post<any>(`llm/chat/${noteId}/messages`, {
|
||||
message: messageParams.content,
|
||||
includeContext: messageParams.useAdvancedContext,
|
||||
options: {
|
||||
|
@ -37,9 +37,10 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
private thinkingBubble!: HTMLElement;
|
||||
private thinkingText!: HTMLElement;
|
||||
private thinkingToggle!: HTMLElement;
|
||||
private chatNoteId: string | null = null;
|
||||
private noteId: string | null = null; // The actual noteId for the Chat Note
|
||||
private currentNoteId: string | null = null;
|
||||
|
||||
// Simplified to just use noteId - this represents the AI Chat note we're working with
|
||||
private noteId: string | null = null;
|
||||
private currentNoteId: string | null = null; // The note providing context (for regular notes)
|
||||
private _messageHandlerId: number | null = null;
|
||||
private _messageHandler: any = null;
|
||||
|
||||
@ -68,7 +69,6 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
totalTokens?: number;
|
||||
};
|
||||
} = {
|
||||
model: 'default',
|
||||
temperature: 0.7,
|
||||
toolExecutions: []
|
||||
};
|
||||
@ -90,12 +90,21 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public getChatNoteId(): string | null {
|
||||
return this.chatNoteId;
|
||||
public getNoteId(): string | null {
|
||||
return this.noteId;
|
||||
}
|
||||
|
||||
public setChatNoteId(chatNoteId: string | null): void {
|
||||
this.chatNoteId = chatNoteId;
|
||||
public setNoteId(noteId: string | null): void {
|
||||
this.noteId = noteId;
|
||||
}
|
||||
|
||||
// Deprecated - keeping for backward compatibility but mapping to noteId
|
||||
public getChatNoteId(): string | null {
|
||||
return this.noteId;
|
||||
}
|
||||
|
||||
public setChatNoteId(noteId: string | null): void {
|
||||
this.noteId = noteId;
|
||||
}
|
||||
|
||||
public getNoteContextChatMessages(): HTMLElement {
|
||||
@ -307,16 +316,22 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
}
|
||||
}
|
||||
|
||||
const dataToSave: ChatData = {
|
||||
// Only save if we have a valid note ID
|
||||
if (!this.noteId) {
|
||||
console.warn('Cannot save chat data: no noteId available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dataToSave = {
|
||||
messages: this.messages,
|
||||
chatNoteId: this.chatNoteId,
|
||||
noteId: this.noteId,
|
||||
chatNoteId: this.noteId, // For backward compatibility
|
||||
toolSteps: toolSteps,
|
||||
// Add sources if we have them
|
||||
sources: this.sources || [],
|
||||
// Add metadata
|
||||
metadata: {
|
||||
model: this.metadata?.model || 'default',
|
||||
model: this.metadata?.model || undefined,
|
||||
provider: this.metadata?.provider || undefined,
|
||||
temperature: this.metadata?.temperature || 0.7,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
@ -325,7 +340,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`Saving chat data with chatNoteId: ${this.chatNoteId}, noteId: ${this.noteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`);
|
||||
console.log(`Saving chat data with noteId: ${this.noteId}, ${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
|
||||
@ -347,9 +362,44 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
const savedData = await this.onGetData() as ChatData;
|
||||
|
||||
if (savedData?.messages?.length > 0) {
|
||||
// Check if we actually have new content to avoid unnecessary UI rebuilds
|
||||
const currentMessageCount = this.messages.length;
|
||||
const savedMessageCount = savedData.messages.length;
|
||||
|
||||
// If message counts are the same, check if content is different
|
||||
const hasNewContent = savedMessageCount > currentMessageCount ||
|
||||
JSON.stringify(this.messages) !== JSON.stringify(savedData.messages);
|
||||
|
||||
if (!hasNewContent) {
|
||||
console.log("No new content detected, skipping UI rebuild");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Loading saved data: ${currentMessageCount} -> ${savedMessageCount} messages`);
|
||||
|
||||
// Store current scroll position if we need to preserve it
|
||||
const shouldPreserveScroll = savedMessageCount > currentMessageCount && currentMessageCount > 0;
|
||||
const currentScrollTop = shouldPreserveScroll ? this.chatContainer.scrollTop : 0;
|
||||
const currentScrollHeight = shouldPreserveScroll ? this.chatContainer.scrollHeight : 0;
|
||||
|
||||
// Load messages
|
||||
const oldMessages = [...this.messages];
|
||||
this.messages = savedData.messages;
|
||||
|
||||
// Only rebuild UI if we have significantly different content
|
||||
if (savedMessageCount > currentMessageCount) {
|
||||
// We have new messages - just add the new ones instead of rebuilding everything
|
||||
const newMessages = savedData.messages.slice(currentMessageCount);
|
||||
console.log(`Adding ${newMessages.length} new messages to UI`);
|
||||
|
||||
newMessages.forEach(message => {
|
||||
const role = message.role as 'user' | 'assistant';
|
||||
this.addMessageToChat(role, message.content);
|
||||
});
|
||||
} else {
|
||||
// Content changed but count is same - need to rebuild
|
||||
console.log("Message content changed, rebuilding UI");
|
||||
|
||||
// Clear and rebuild the chat UI
|
||||
this.noteContextChatMessages.innerHTML = '';
|
||||
|
||||
@ -357,6 +407,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
const role = message.role as 'user' | 'assistant';
|
||||
this.addMessageToChat(role, message.content);
|
||||
});
|
||||
}
|
||||
|
||||
// Restore tool execution steps if they exist
|
||||
if (savedData.toolSteps && Array.isArray(savedData.toolSteps) && savedData.toolSteps.length > 0) {
|
||||
@ -400,13 +451,33 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
// Load Chat Note ID if available
|
||||
if (savedData.noteId) {
|
||||
console.log(`Using noteId as Chat Note ID: ${savedData.noteId}`);
|
||||
this.chatNoteId = savedData.noteId;
|
||||
this.noteId = savedData.noteId;
|
||||
} else {
|
||||
console.log(`No noteId found in saved data, cannot load chat session`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restore scroll position if we were preserving it
|
||||
if (shouldPreserveScroll) {
|
||||
// Calculate the new scroll position to maintain relative position
|
||||
const newScrollHeight = this.chatContainer.scrollHeight;
|
||||
const scrollDifference = newScrollHeight - currentScrollHeight;
|
||||
const newScrollTop = currentScrollTop + scrollDifference;
|
||||
|
||||
// Only scroll down if we're near the bottom, otherwise preserve exact position
|
||||
const wasNearBottom = (currentScrollTop + this.chatContainer.clientHeight) >= (currentScrollHeight - 50);
|
||||
|
||||
if (wasNearBottom) {
|
||||
// User was at bottom, scroll to new bottom
|
||||
this.chatContainer.scrollTop = newScrollHeight;
|
||||
console.log("User was at bottom, scrolling to new bottom");
|
||||
} else {
|
||||
// User was not at bottom, try to preserve their position
|
||||
this.chatContainer.scrollTop = newScrollTop;
|
||||
console.log(`Preserving scroll position: ${currentScrollTop} -> ${newScrollTop}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
@ -550,6 +621,15 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
// Get current note context if needed
|
||||
const currentActiveNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null;
|
||||
|
||||
// For AI Chat notes, the note itself IS the chat session
|
||||
// So currentNoteId and noteId should be the same
|
||||
if (this.noteId && currentActiveNoteId === this.noteId) {
|
||||
// We're in an AI Chat note - don't reset, just load saved data
|
||||
console.log(`Refreshing AI Chat note ${this.noteId} - loading saved data`);
|
||||
await this.loadSavedData();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're switching to a different note, we need to reset
|
||||
if (this.currentNoteId !== currentActiveNoteId) {
|
||||
console.log(`Note ID changed from ${this.currentNoteId} to ${currentActiveNoteId}, resetting chat panel`);
|
||||
@ -557,7 +637,6 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
// Reset the UI and data
|
||||
this.noteContextChatMessages.innerHTML = '';
|
||||
this.messages = [];
|
||||
this.chatNoteId = null;
|
||||
this.noteId = null; // Also reset the chat note ID
|
||||
this.hideSources(); // Hide any sources from previous note
|
||||
|
||||
@ -569,7 +648,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
const hasSavedData = await this.loadSavedData();
|
||||
|
||||
// Only create a new session if we don't have a session or saved data
|
||||
if (!this.chatNoteId || !this.noteId || !hasSavedData) {
|
||||
if (!this.noteId || !hasSavedData) {
|
||||
// Create a new chat session
|
||||
await this.createChatSession();
|
||||
}
|
||||
@ -580,19 +659,15 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
*/
|
||||
private async createChatSession() {
|
||||
try {
|
||||
// Create a new chat session, passing the current note ID if it exists
|
||||
const { chatNoteId, noteId } = await createChatSession(
|
||||
this.currentNoteId ? this.currentNoteId : undefined
|
||||
);
|
||||
// If we already have a noteId (for AI Chat notes), use it
|
||||
const contextNoteId = this.noteId || this.currentNoteId;
|
||||
|
||||
if (chatNoteId) {
|
||||
// If we got back an ID from the API, use it
|
||||
this.chatNoteId = chatNoteId;
|
||||
|
||||
// For new sessions, the noteId should equal the chatNoteId
|
||||
// This ensures we're using the note ID consistently
|
||||
this.noteId = noteId || chatNoteId;
|
||||
// Create a new chat session, passing the context note ID
|
||||
const noteId = await createChatSession(contextNoteId ? contextNoteId : undefined);
|
||||
|
||||
if (noteId) {
|
||||
// Set the note ID for this chat
|
||||
this.noteId = noteId;
|
||||
console.log(`Created new chat session with noteId: ${this.noteId}`);
|
||||
} else {
|
||||
throw new Error("Failed to create chat session - no ID returned");
|
||||
@ -645,7 +720,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
const showThinking = this.showThinkingCheckbox.checked;
|
||||
|
||||
// Add logging to verify parameters
|
||||
console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`);
|
||||
console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.noteId}`);
|
||||
|
||||
// Create the message parameters
|
||||
const messageParams = {
|
||||
@ -695,11 +770,11 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
await validateEmbeddingProviders(this.validationWarning);
|
||||
|
||||
// Make sure we have a valid session
|
||||
if (!this.chatNoteId) {
|
||||
if (!this.noteId) {
|
||||
// If no session ID, create a new session
|
||||
await this.createChatSession();
|
||||
|
||||
if (!this.chatNoteId) {
|
||||
if (!this.noteId) {
|
||||
// If still no session ID, show error and return
|
||||
console.error("Failed to create chat session");
|
||||
toastService.showError("Failed to create chat session");
|
||||
@ -730,7 +805,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
await this.saveCurrentData();
|
||||
|
||||
// Add logging to verify parameters
|
||||
console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`);
|
||||
console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.noteId}`);
|
||||
|
||||
// Create the message parameters
|
||||
const messageParams = {
|
||||
@ -767,12 +842,12 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
*/
|
||||
private async handleDirectResponse(messageParams: any): Promise<boolean> {
|
||||
try {
|
||||
if (!this.chatNoteId) return false;
|
||||
if (!this.noteId) return false;
|
||||
|
||||
console.log(`Getting direct response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`);
|
||||
console.log(`Getting direct response using sessionId: ${this.noteId} (noteId: ${this.noteId})`);
|
||||
|
||||
// Get a direct response from the server
|
||||
const postResponse = await getDirectResponse(this.chatNoteId, messageParams);
|
||||
const postResponse = await getDirectResponse(this.noteId, messageParams);
|
||||
|
||||
// If the POST request returned content directly, display it
|
||||
if (postResponse && postResponse.content) {
|
||||
@ -845,11 +920,11 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
* Set up streaming response via WebSocket
|
||||
*/
|
||||
private async setupStreamingResponse(messageParams: any): Promise<void> {
|
||||
if (!this.chatNoteId) {
|
||||
if (!this.noteId) {
|
||||
throw new Error("No session ID available");
|
||||
}
|
||||
|
||||
console.log(`Setting up streaming response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`);
|
||||
console.log(`Setting up streaming response using sessionId: ${this.noteId} (noteId: ${this.noteId})`);
|
||||
|
||||
// Store tool executions captured during streaming
|
||||
const toolExecutionsCache: Array<{
|
||||
@ -862,7 +937,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
}> = [];
|
||||
|
||||
return setupStreamingResponse(
|
||||
this.chatNoteId,
|
||||
this.noteId,
|
||||
messageParams,
|
||||
// Content update handler
|
||||
(content: string, isDone: boolean = false) => {
|
||||
@ -898,7 +973,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
similarity?: number;
|
||||
content?: string;
|
||||
}>;
|
||||
}>(`llm/chat/${this.chatNoteId}`)
|
||||
}>(`llm/chat/${this.noteId}`)
|
||||
.then((sessionData) => {
|
||||
console.log("Got updated session data:", sessionData);
|
||||
|
||||
@ -933,9 +1008,9 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated data to the note
|
||||
this.saveCurrentData()
|
||||
.catch(err => console.error("Failed to save data after streaming completed:", err));
|
||||
// DON'T save here - let the server handle saving the complete conversation
|
||||
// to avoid race conditions between client and server saves
|
||||
console.log("Updated metadata after streaming completion, server should save");
|
||||
})
|
||||
.catch(err => console.error("Error fetching session data after streaming:", err));
|
||||
}
|
||||
@ -973,11 +1048,9 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
|
||||
console.log(`Cached tool execution for ${toolData.tool} to be saved later`);
|
||||
|
||||
// Save immediately after receiving a tool execution
|
||||
// This ensures we don't lose tool execution data if streaming fails
|
||||
this.saveCurrentData().catch(err => {
|
||||
console.error("Failed to save tool execution data:", err);
|
||||
});
|
||||
// DON'T save immediately during streaming - let the server handle saving
|
||||
// to avoid race conditions between client and server saves
|
||||
console.log(`Tool execution cached, will be saved by server`);
|
||||
}
|
||||
},
|
||||
// Complete handler
|
||||
@ -995,23 +1068,19 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
* Update the UI with streaming content
|
||||
*/
|
||||
private updateStreamingUI(assistantResponse: string, isDone: boolean = false) {
|
||||
// Parse and handle thinking content if present
|
||||
if (!isDone) {
|
||||
const thinkingContent = this.parseThinkingContent(assistantResponse);
|
||||
if (thinkingContent) {
|
||||
this.updateThinkingText(thinkingContent);
|
||||
// Don't display the raw response with think tags in the chat
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Track if we have a streaming message in progress
|
||||
const hasStreamingMessage = !!this.noteContextChatMessages.querySelector('.assistant-message.streaming');
|
||||
|
||||
// Get the existing assistant message or create a new one
|
||||
let assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message:last-child');
|
||||
// Create a new message element or use the existing streaming one
|
||||
let assistantMessageEl: HTMLElement;
|
||||
|
||||
if (!assistantMessageEl) {
|
||||
// If no assistant message yet, create one
|
||||
if (hasStreamingMessage) {
|
||||
// Use the existing streaming message
|
||||
assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message.streaming')!;
|
||||
} else {
|
||||
// Create a new message element
|
||||
assistantMessageEl = document.createElement('div');
|
||||
assistantMessageEl.className = 'assistant-message message mb-3';
|
||||
assistantMessageEl.className = 'assistant-message message mb-3 streaming';
|
||||
this.noteContextChatMessages.appendChild(assistantMessageEl);
|
||||
|
||||
// Add assistant profile icon
|
||||
@ -1026,60 +1095,37 @@ export default class LlmChatPanel extends BasicWidget {
|
||||
assistantMessageEl.appendChild(messageContent);
|
||||
}
|
||||
|
||||
// Clean the response to remove thinking tags before displaying
|
||||
const cleanedResponse = this.removeThinkingTags(assistantResponse);
|
||||
|
||||
// Update the content
|
||||
// Update the content with the current response
|
||||
const messageContent = assistantMessageEl.querySelector('.message-content') as HTMLElement;
|
||||
messageContent.innerHTML = formatMarkdown(cleanedResponse);
|
||||
messageContent.innerHTML = formatMarkdown(assistantResponse);
|
||||
|
||||
// Apply syntax highlighting if this is the final update
|
||||
// When the response is complete
|
||||
if (isDone) {
|
||||
// Remove the streaming class to mark this message as complete
|
||||
assistantMessageEl.classList.remove('streaming');
|
||||
|
||||
// Apply syntax highlighting
|
||||
formatCodeBlocks($(assistantMessageEl as HTMLElement));
|
||||
|
||||
// Hide the thinking display when response is complete
|
||||
this.hideThinkingDisplay();
|
||||
|
||||
// Update message in the data model for storage
|
||||
// Find the last assistant message to update, or add a new one if none exists
|
||||
const assistantMessages = this.messages.filter(msg => msg.role === 'assistant');
|
||||
const lastAssistantMsgIndex = assistantMessages.length > 0 ?
|
||||
this.messages.lastIndexOf(assistantMessages[assistantMessages.length - 1]) : -1;
|
||||
|
||||
if (lastAssistantMsgIndex >= 0) {
|
||||
// Update existing message with cleaned content
|
||||
this.messages[lastAssistantMsgIndex].content = cleanedResponse;
|
||||
} else {
|
||||
// Add new message with cleaned content
|
||||
// Always add a new message to the data model
|
||||
// This ensures we preserve all distinct assistant messages
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: cleanedResponse
|
||||
content: assistantResponse,
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Hide loading indicator
|
||||
hideLoadingIndicator(this.loadingIndicator);
|
||||
|
||||
// Save the final state to the Chat Note
|
||||
this.saveCurrentData().catch(err => {
|
||||
console.error("Failed to save assistant response to note:", err);
|
||||
});
|
||||
// Save the updated message list
|
||||
this.saveCurrentData();
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove thinking tags from response content
|
||||
*/
|
||||
private removeThinkingTags(content: string): string {
|
||||
if (!content) return content;
|
||||
|
||||
// Remove <think>...</think> blocks from the content
|
||||
return content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general errors in the send message flow
|
||||
*/
|
||||
|
@ -11,7 +11,7 @@ export interface ChatResponse {
|
||||
export interface SessionResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
noteId?: string;
|
||||
noteId: string; // The ID of the chat note
|
||||
}
|
||||
|
||||
export interface ToolExecutionStep {
|
||||
@ -33,8 +33,8 @@ export interface MessageData {
|
||||
|
||||
export interface ChatData {
|
||||
messages: MessageData[];
|
||||
chatNoteId: string | null;
|
||||
noteId?: string | null;
|
||||
noteId: string; // The ID of the chat note
|
||||
chatNoteId?: string; // Deprecated - kept for backward compatibility, should equal noteId
|
||||
toolSteps: ToolExecutionStep[];
|
||||
sources?: Array<{
|
||||
noteId: string;
|
||||
|
@ -19,7 +19,7 @@ const TPL = /*html*/`
|
||||
|
||||
<div class="no-edited-notes-found">${t("edited_notes.no_edited_notes_found")}</div>
|
||||
|
||||
<div class="edited-notes-list"></div>
|
||||
<div class="edited-notes-list use-tn-links"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -94,6 +94,11 @@ export default class AiChatTypeWidget extends TypeWidget {
|
||||
this.llmChatPanel.clearNoteContextChatMessages();
|
||||
this.llmChatPanel.setMessages([]);
|
||||
|
||||
// Set the note ID for the chat panel
|
||||
if (note) {
|
||||
this.llmChatPanel.setNoteId(note.noteId);
|
||||
}
|
||||
|
||||
// This will load saved data via the getData callback
|
||||
await this.llmChatPanel.refresh();
|
||||
this.isInitialized = true;
|
||||
@ -130,7 +135,7 @@ export default class AiChatTypeWidget extends TypeWidget {
|
||||
// Reset the chat panel UI
|
||||
this.llmChatPanel.clearNoteContextChatMessages();
|
||||
this.llmChatPanel.setMessages([]);
|
||||
this.llmChatPanel.setChatNoteId(null);
|
||||
this.llmChatPanel.setNoteId(this.note.noteId);
|
||||
}
|
||||
|
||||
// Call the parent method to refresh
|
||||
@ -152,6 +157,7 @@ export default class AiChatTypeWidget extends TypeWidget {
|
||||
// Make sure the chat panel has the current note ID
|
||||
if (this.note) {
|
||||
this.llmChatPanel.setCurrentNoteId(this.note.noteId);
|
||||
this.llmChatPanel.setNoteId(this.note.noteId);
|
||||
}
|
||||
|
||||
this.initPromise = (async () => {
|
||||
@ -186,7 +192,7 @@ export default class AiChatTypeWidget extends TypeWidget {
|
||||
// Format the data properly - this is the canonical format of the data
|
||||
const formattedData = {
|
||||
messages: data.messages || [],
|
||||
chatNoteId: data.chatNoteId || this.note.noteId,
|
||||
noteId: this.note.noteId, // Always use the note's own ID
|
||||
toolSteps: data.toolSteps || [],
|
||||
sources: data.sources || [],
|
||||
metadata: {
|
||||
|
@ -189,7 +189,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) {
|
||||
{
|
||||
label: "Insert",
|
||||
icon: "plus",
|
||||
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak"]
|
||||
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||
},
|
||||
"|",
|
||||
"outdent",
|
||||
@ -244,7 +244,7 @@ export function buildFloatingToolbar() {
|
||||
{
|
||||
label: "Insert",
|
||||
icon: "plus",
|
||||
items: ["internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak"]
|
||||
items: ["bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
|
||||
},
|
||||
"|",
|
||||
"outdent",
|
||||
|
@ -8,6 +8,7 @@ import HeadingStyleOptions from "./options/text_notes/heading_style.js";
|
||||
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
|
||||
import HighlightsListOptions from "./options/text_notes/highlights_list.js";
|
||||
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
|
||||
import DateTimeFormatOptions from "./options/text_notes/date_time_format.js";
|
||||
import CodeEditorOptions from "./options/code_notes/code_editor.js";
|
||||
import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
|
||||
import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
|
||||
@ -88,7 +89,8 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", (typeof NoteContextAw
|
||||
CodeBlockOptions,
|
||||
TableOfContentsOptions,
|
||||
HighlightsListOptions,
|
||||
TextAutoReadOnlySizeOptions
|
||||
TextAutoReadOnlySizeOptions,
|
||||
DateTimeFormatOptions
|
||||
],
|
||||
_optionsCodeNotes: [
|
||||
CodeEditorOptions,
|
||||
|
@ -377,7 +377,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
|
||||
insertDateTimeToTextCommand() {
|
||||
const date = new Date();
|
||||
const dateString = utils.formatDateTime(date);
|
||||
const customDateTimeFormat = options.get("customDateTimeFormat");
|
||||
const dateString = utils.formatDateTime(date, customDateTimeFormat);
|
||||
|
||||
this.addTextToEditor(dateString);
|
||||
}
|
||||
|
@ -239,6 +239,9 @@ export default class GeoMapTypeWidget extends TypeWidget {
|
||||
wptIcons: {
|
||||
"": this.#buildIcon("bx bx-pin")
|
||||
}
|
||||
},
|
||||
polyline_options: {
|
||||
color: note.getLabelValue("color") ?? "blue"
|
||||
}
|
||||
});
|
||||
track.addTo(this.geoMapWidget.map);
|
||||
|
@ -0,0 +1,67 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
import utils from "../../../../services/utils.js";
|
||||
import keyboardActionsService from "../../../../services/keyboard_actions.js";
|
||||
import linkService from "../../../.././services/link.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("custom_date_time_format.title")}</h4>
|
||||
|
||||
<p class="description">
|
||||
${t("custom_date_time_format.description")}
|
||||
</p>
|
||||
|
||||
<div class="form-group row align-items-center">
|
||||
<div class="col-6">
|
||||
<label for="custom-date-time-format">${t("custom_date_time_format.format_string")}</label>
|
||||
<input type="text" id="custom-date-time-format" class="form-control custom-date-time-format" placeholder="YYYY-MM-DD HH:mm">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label>${t("custom_date_time_format.formatted_time")}</label>
|
||||
<div class="formatted-date"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class DateTimeFormatOptions extends OptionsWidget {
|
||||
|
||||
private $formatInput!: JQuery<HTMLInputElement>;
|
||||
private $formattedDate!: JQuery<HTMLInputElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$formatInput = this.$widget.find("input.custom-date-time-format");
|
||||
this.$formattedDate = this.$widget.find(".formatted-date");
|
||||
|
||||
this.$formatInput.on("input", () => {
|
||||
const dateString = utils.formatDateTime(new Date(), this.$formatInput.val());
|
||||
this.$formattedDate.text(dateString);
|
||||
});
|
||||
|
||||
this.$formatInput.on('blur keydown', (e) => {
|
||||
if (e.type === 'blur' || (e.type === 'keydown' && e.key === 'Enter')) {
|
||||
this.updateOption("customDateTimeFormat", this.$formatInput.val());
|
||||
}
|
||||
});
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
const shortcutKey = (await keyboardActionsService.getAction("insertDateTimeToText")).effectiveShortcuts.join(", ");
|
||||
const $link = await linkService.createLink("_hidden/_options/_optionsShortcuts", {
|
||||
"title": shortcutKey,
|
||||
"showTooltip": false
|
||||
});
|
||||
this.$widget.find(".description").find("kbd").replaceWith($link);
|
||||
|
||||
const customDateTimeFormat = options.customDateTimeFormat || "YYYY-MM-DD HH:mm";
|
||||
this.$formatInput.val(customDateTimeFormat);
|
||||
const dateString = utils.formatDateTime(new Date(), customDateTimeFormat);
|
||||
this.$formattedDate.text(dateString);
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
"@types/electron-squirrel-startup": "1.0.2",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"electron": "36.3.2",
|
||||
"electron": "36.4.0",
|
||||
"@electron-forge/cli": "7.8.1",
|
||||
"@electron-forge/maker-deb": "7.8.1",
|
||||
"@electron-forge/maker-dmg": "7.8.1",
|
||||
@ -31,7 +31,6 @@
|
||||
"config": {
|
||||
"forge": "./electron-forge/forge.config.cjs"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
||||
"scripts": {
|
||||
"start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js"
|
||||
},
|
||||
|
@ -7,8 +7,11 @@ import tray from "@triliumnext/server/src/services/tray.js";
|
||||
import options from "@triliumnext/server/src/services/options.js";
|
||||
import electronDebug from "electron-debug";
|
||||
import electronDl from "electron-dl";
|
||||
import { deferred } from "@triliumnext/server/src/services/utils.js";
|
||||
|
||||
async function main() {
|
||||
const serverInitializedPromise = deferred<void>();
|
||||
|
||||
// Prevent Trilium starting twice on first install and on uninstall for the Windows installer.
|
||||
if ((require("electron-squirrel-startup")).default) {
|
||||
process.exit(0);
|
||||
@ -37,7 +40,11 @@ async function main() {
|
||||
}
|
||||
});
|
||||
|
||||
electron.app.on("ready", onReady);
|
||||
electron.app.on("ready", async () => {
|
||||
await serverInitializedPromise;
|
||||
console.log("Starting Electron...");
|
||||
await onReady();
|
||||
});
|
||||
|
||||
electron.app.on("will-quit", () => {
|
||||
electron.globalShortcut.unregisterAll();
|
||||
@ -47,7 +54,10 @@ async function main() {
|
||||
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
|
||||
|
||||
await initializeTranslations();
|
||||
await import("@triliumnext/server/src/main.js");
|
||||
const startTriliumServer = (await import("@triliumnext/server/src/www.js")).default;
|
||||
await startTriliumServer();
|
||||
console.log("Server loaded");
|
||||
serverInitializedPromise.resolve();
|
||||
}
|
||||
|
||||
async function onReady() {
|
||||
|
@ -12,7 +12,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mime-types": "^3.0.0",
|
||||
"@types/yargs": "^17.0.33"
|
||||
},
|
||||
"nx": {
|
||||
|
14
apps/edit-docs/demo/!!!meta.json
vendored
14
apps/edit-docs/demo/!!!meta.json
vendored
@ -454,19 +454,19 @@
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "child:child:child:template",
|
||||
"value": "kr6HIBBuXRwm",
|
||||
"isInheritable": false,
|
||||
"position": 20
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"name": "iconClass",
|
||||
"value": "bx bx-calendar",
|
||||
"isInheritable": false,
|
||||
"position": 30
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "dateTemplate",
|
||||
"value": "kr6HIBBuXRwm",
|
||||
"isInheritable": false,
|
||||
"position": 20
|
||||
}
|
||||
],
|
||||
"format": "html",
|
||||
|
7
apps/edit-docs/demo/root/Trilium Demo.html
vendored
7
apps/edit-docs/demo/root/Trilium Demo.html
vendored
@ -18,22 +18,28 @@
|
||||
height="150">
|
||||
</figure>
|
||||
<p><strong>Welcome to TriliumNext Notes!</strong>
|
||||
|
||||
</p>
|
||||
<p>This is initial "demo" document provided by TriliumNext by default to
|
||||
showcase some of its features and also give you some ideas how you might
|
||||
structure your notes. You can play with it, modify note content and tree
|
||||
structure as you wish.</p>
|
||||
<p>If you need any help, visit TriliumNext website: <a href="https://github.com/TriliumNext">https://github.com/TriliumNext</a>
|
||||
|
||||
</p>
|
||||
<h3>Cleanup</h3>
|
||||
|
||||
<p>Once you're finished with experimenting and want to cleanup these pages,
|
||||
you can simply delete them all.</p>
|
||||
<h3>Formatting</h3>
|
||||
|
||||
<p>TriliumNext supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
|
||||
Of course you can add links like this one pointing to <a href="http://www.google.com">google.com</a>
|
||||
|
||||
</p>
|
||||
<p>Lists</p>
|
||||
<p><strong>Ordered:</strong>
|
||||
|
||||
</p>
|
||||
<ol>
|
||||
<li>First Item</li>
|
||||
@ -48,6 +54,7 @@
|
||||
</li>
|
||||
</ol>
|
||||
<p><strong>Unordered:</strong>
|
||||
|
||||
</p>
|
||||
<ul>
|
||||
<li>Item</li>
|
||||
|
@ -14,17 +14,22 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<h2>Main characters</h2>
|
||||
|
||||
<p>… here put main characters …</p>
|
||||
<p> </p>
|
||||
<h2>Plot</h2>
|
||||
|
||||
<p>… describe main plot lines …</p>
|
||||
<p> </p>
|
||||
<h2>Tone</h2>
|
||||
|
||||
<p> </p>
|
||||
<h2>Genre</h2>
|
||||
|
||||
<p>scifi / drama / romance</p>
|
||||
<p> </p>
|
||||
<h2>Similar books</h2>
|
||||
|
||||
<ul>
|
||||
<li>…</li>
|
||||
</ul>
|
||||
|
@ -14,11 +14,14 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<p>Checkout Kindle daily deals: <a href="https://www.amazon.com/gp/feature.html?docId=1000677541">https://www.amazon.com/gp/feature.html?docId=1000677541</a>
|
||||
|
||||
</p>
|
||||
<ul>
|
||||
<li>Cixin Liu - <a href="https://www.amazon.com/Dark-Forest-Remembrance-Earths-Past/dp/0765386690/ref=pd_bxgy_14_img_2?_encoding=UTF8&pd_rd_i=0765386690&pd_rd_r=AB0J179TM9NTEAMHE240&pd_rd_w=FAhxX&pd_rd_wg=pLGK7&psc=1&refRID=AB0J179TM9NTEAMHE240">The Dark Forest</a>
|
||||
|
||||
</li>
|
||||
<li>Ann Leckie - <a href="https://www.amazon.com/Ancillary-Sword-Imperial-Radch-Leckie/dp/0316246654/ref=pd_sim_14_1?_encoding=UTF8&pd_rd_i=0316246654&pd_rd_r=D7KDTGZFP7YM1YSYVY4G&pd_rd_w=jkn28&pd_rd_wg=JVhtw&psc=1&refRID=D7KDTGZFP7YM1YSYVY4G">Ancillary Sword</a>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -18,21 +18,25 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description">buy milk </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">do the laundry </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">watch TV </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description">eat ice cream </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -24,6 +24,7 @@
|
||||
alert("Hello world");
|
||||
|
||||
}</code></pre>
|
||||
|
||||
<p>For larger pieces of code it is better to use a code note, which uses
|
||||
a fully-fledged code editor (CodeMirror). For an example of a code note,
|
||||
see <a class="reference-link" href="../Scripting%20examples/Custom%20request%20handler.js">Custom request handler</a>.</p>
|
||||
|
@ -15,7 +15,9 @@
|
||||
<div class="ck-content">
|
||||
<p><span class="math-tex">\(% \f is defined as #1f(#2) using the macro \f\relax{x} = \int_{-\infty}^\infty \f\hat\xi\,e^{2 \pi i \xi x} \,d\xi\)</span>Some
|
||||
math examples:</p><span class="math-tex">\[\displaystyle \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }\]</span>
|
||||
|
||||
<p>Another:</p><span class="math-tex">\[\displaystyle \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)\]</span>
|
||||
|
||||
<p>Inline math is also possible: <span class="math-tex">\(c^2 = a^2 + b^2\)</span> </p>
|
||||
<p> </p>
|
||||
</div>
|
||||
|
@ -22,6 +22,7 @@
|
||||
<p>This page demonstrates two things:</p>
|
||||
<ul>
|
||||
<li>possibility to <a href="#root/_hidden/_help/_help_KSZ04uQ2D1St/_help_iPIMuisry3hd/_help_nBAXQFj20hS1">include one note into another</a>
|
||||
|
||||
</li>
|
||||
<li>PDF preview - you can read PDFs directly in Trilium!</li>
|
||||
</ul>
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<p>You can read some explanation on how this journal works here: <a href="https://github.com/zadam/trilium/wiki/Day-notes">https://github.com/zadam/trilium/wiki/Day-notes</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -17,6 +17,7 @@
|
||||
<li>XBox</li>
|
||||
<li>Candles</li>
|
||||
<li><a href="https://www.amazon.ca/Anker-SoundCore-Portable-Bluetooth-Resistance/dp/B01MTB55WH?pd_rd_wg=honW8&pd_rd_r=c9bb7c0f-0051-4da7-991f-4ca711a1b3e3&pd_rd_w=ciUpR&ref_=pd_gw_simh&pf_rd_r=K10XKX0NGPDNTYYP4BS4&pf_rd_p=5f1b460b-78c1-580e-929e-2878fe4859e8">Portable speakers</a>
|
||||
|
||||
</li>
|
||||
<li>...?</li>
|
||||
</ul>
|
||||
|
@ -14,8 +14,10 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<p>Wiki: <a href="https://en.wikipedia.org/wiki/Trusted_timestamping">https://en.wikipedia.org/wiki/Trusted_timestamping</a>
|
||||
|
||||
</p>
|
||||
<p>Bozho: <a href="https://techblog.bozho.net/using-trusted-timestamping-java/">https://techblog.bozho.net/using-trusted-timestamping-java/</a>
|
||||
|
||||
</p>
|
||||
<p><strong>Trusted timestamping</strong> is the process of <a href="https://en.wikipedia.org/wiki/Computer_security">securely</a> keeping
|
||||
track of the creation and modification time of a document. Security here
|
||||
|
@ -16,6 +16,7 @@
|
||||
<p>Miscellaneous notes done on monday ...</p>
|
||||
<p> </p>
|
||||
<p>Interesting video: <a href="https://www.youtube.com/watch?v=_eSAF_qT_FY&feature=youtu.be">https://www.youtube.com/watch?v=_eSAF_qT_FY&feature=youtu.be</a>
|
||||
|
||||
</p>
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
width="209" height="300">
|
||||
</figure>
|
||||
<p>Maybe CodeNames? <a href="https://boardgamegeek.com/boardgame/178900/codenames">https://boardgamegeek.com/boardgame/178900/codenames</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -18,6 +18,7 @@
|
||||
<li>
|
||||
<label class="todo-list__label">
|
||||
<input type="checkbox" disabled="disabled"><span class="todo-list__label__description"> </span>
|
||||
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -24,14 +24,17 @@
|
||||
<span
|
||||
class="footnote-reference" data-footnote-reference="" data-footnote-index="1"
|
||||
data-footnote-id="6qz4pm021mi" role="doc-noteref" id="fnref6qz4pm021mi"><sup><a href="#fn6qz4pm021mi">[1]</a></sup>
|
||||
|
||||
</span>
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="6qz4pm021mi" role="doc-endnote" id="fn6qz4pm021mi"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="6qz4pm021mi"><sup><strong><a href="#fnref6qz4pm021mi">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -26,13 +26,16 @@
|
||||
been brought to its knees.<span class="footnote-reference" data-footnote-reference=""
|
||||
data-footnote-index="1" data-footnote-id="o6g991vkrwj" role="doc-noteref"
|
||||
id="fnrefo6g991vkrwj"><sup><a href="#fno6g991vkrwj">[1]</a></sup></span>
|
||||
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="o6g991vkrwj" role="doc-endnote" id="fno6g991vkrwj"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="o6g991vkrwj"><sup><strong><a href="#fnrefo6g991vkrwj">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -22,13 +22,16 @@
|
||||
around 1450 in polished drystone walls.<span class="footnote-reference"
|
||||
data-footnote-reference="" data-footnote-index="1" data-footnote-id="4prjheuho88"
|
||||
role="doc-noteref" id="fnref4prjheuho88"><sup><a href="#fn4prjheuho88">[1]</a></sup></span>
|
||||
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="4prjheuho88" role="doc-endnote" id="fn4prjheuho88"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="4prjheuho88"><sup><strong><a href="#fnref4prjheuho88">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -23,13 +23,16 @@
|
||||
by earthquakes.<span class="footnote-reference" data-footnote-reference=""
|
||||
data-footnote-index="1" data-footnote-id="ej5sd0bakne" role="doc-noteref"
|
||||
id="fnrefej5sd0bakne"><sup><a href="#fnej5sd0bakne">[1]</a></sup></span>
|
||||
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="ej5sd0bakne" role="doc-endnote" id="fnej5sd0bakne"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="ej5sd0bakne"><sup><strong><a href="#fnrefej5sd0bakne">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -26,14 +26,17 @@
|
||||
<span
|
||||
class="footnote-reference" data-footnote-reference="" data-footnote-index="1"
|
||||
data-footnote-id="4kitkusvyi3" role="doc-noteref" id="fnref4kitkusvyi3"><sup><a href="#fn4kitkusvyi3">[1]</a></sup>
|
||||
|
||||
</span>
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="4kitkusvyi3" role="doc-endnote" id="fn4kitkusvyi3"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="4kitkusvyi3"><sup><strong><a href="#fnref4kitkusvyi3">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -23,14 +23,17 @@
|
||||
<span
|
||||
class="footnote-reference" data-footnote-reference="" data-footnote-index="1"
|
||||
data-footnote-id="o0o2das7ljm" role="doc-noteref" id="fnrefo0o2das7ljm"><sup><a href="#fno0o2das7ljm">[1]</a></sup>
|
||||
|
||||
</span>
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="o0o2das7ljm" role="doc-endnote" id="fno0o2das7ljm"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="o0o2das7ljm"><sup><strong><a href="#fnrefo0o2das7ljm">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -23,13 +23,16 @@
|
||||
the complex.<span class="footnote-reference" data-footnote-reference=""
|
||||
data-footnote-index="1" data-footnote-id="zzzjn52iwk" role="doc-noteref"
|
||||
id="fnrefzzzjn52iwk"><sup><a href="#fnzzzjn52iwk">[1]</a></sup></span>
|
||||
|
||||
</p>
|
||||
<ol class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
|
||||
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
|
||||
data-footnote-id="zzzjn52iwk" role="doc-endnote" id="fnzzzjn52iwk"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="zzzjn52iwk"><sup><strong><a href="#fnrefzzzjn52iwk">^</a></strong></sup></span>
|
||||
|
||||
<div
|
||||
class="footnote-content" data-footnote-content="">
|
||||
<p><a href="https://www.thecollector.com/what-are-the-seven-wonders-of-the-world/">What Are the 7 Wonders of the World? (with HD Images) | TheCollector</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -15,6 +15,7 @@
|
||||
<div class="ck-content">
|
||||
<p>This is a simple TODO/Task manager. You can see some description and explanation
|
||||
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a>
|
||||
|
||||
</p>
|
||||
<p>Please note that this is meant as scripting example only and feature/bug
|
||||
support is very limited.</p>
|
||||
|
@ -18,6 +18,7 @@
|
||||
width="209" height="300">
|
||||
</figure>
|
||||
<p>Maybe CodeNames? <a href="https://boardgamegeek.com/boardgame/178900/codenames">https://boardgamegeek.com/boardgame/178900/codenames</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<p><a href="https://en.wikipedia.org/wiki/The_Black_Swan:_The_Impact_of_the_Highly_Improbable">https://en.wikipedia.org/wiki/The_Black_Swan:_The_Impact_of_the_Highly_Improbable</a>
|
||||
|
||||
</p>
|
||||
<p><em><strong>The Black Swan: The Impact of the Highly Improbable</strong></em> is
|
||||
a 2007 book by author and former <a href="https://en.wikipedia.org/wiki/Options_trader">options trader</a>
|
||||
|
@ -25,6 +25,7 @@
|
||||
and <a href="https://en.wikipedia.org/wiki/Apple_Inc.">Apple's</a> <a href="https://en.wikipedia.org/wiki/MacOS">macOS</a> (formerly
|
||||
OS X). A version <a href="https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux">is also available for Windows 10</a>.</p>
|
||||
<p><a href="https://en.wikipedia.org/wiki/Bash_(Unix_shell)">Bash on Wikipedia</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user