Compare commits

..

No commits in common. "master" and "3.1.4" have entirely different histories.

156 changed files with 19466 additions and 11700 deletions

9
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM simon987/sist2-build
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash
RUN apt update -y; apt install -y nodejs && rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=noninteractive
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

View File

@ -0,0 +1,16 @@
{
"name": "sist2-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "sist2-dev",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools-extension-pack"
]
}
},
"remoteUser": "root",
"workspaceFolder": "/app/"
}

View File

@ -0,0 +1,8 @@
version: "3"
services:
sist2-dev:
build: .
command: sleep infinity
volumes:
- ../:/app

View File

@ -38,6 +38,4 @@ build/
__pycache__/ __pycache__/
sist2-vue/dist sist2-vue/dist
sist2-admin/frontend/dist sist2-admin/frontend/dist
*.fts *.fts
.git
third-party/libscan/third-party/ext_*/*

View File

@ -7,36 +7,11 @@ platform:
arch: amd64 arch: amd64
steps: steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: docker
image: plugins/docker
depends_on:
- submodules
settings:
username:
from_secret: DOCKER_USER
password:
from_secret: DOCKER_PASSWORD
repo: sist2app/sist2
context: ./
dockerfile: ./Dockerfile
auto_tag: true
auto_tag_suffix: x64-linux
when:
event:
- tag
- name: build - name: build
image: sist2app/sist2-build image: simon987/sist2-build
depends_on:
- submodules
commands: commands:
- ./scripts/build.sh - ./scripts/build.sh
- name: scp files - name: scp files
depends_on:
- build
image: appleboy/drone-scp image: appleboy/drone-scp
settings: settings:
host: host:
@ -47,11 +22,26 @@ steps:
from_secret: SSH_USER from_secret: SSH_USER
key: key:
from_secret: SSH_KEY from_secret: SSH_KEY
target: ~/files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/ target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
source: source:
- ./VERSION - ./VERSION
- ./sist2-x64-linux - ./sist2-x64-linux
- ./sist2-x64-linux-debug - ./sist2-x64-linux-debug
- name: docker
image: plugins/docker
settings:
username:
from_secret: DOCKER_USER
password:
from_secret: DOCKER_PASSWORD
repo: simon987/sist2
context: ./
dockerfile: ./Dockerfile
auto_tag: true
auto_tag_suffix: x64-linux
when:
event:
- tag
--- ---
kind: pipeline kind: pipeline
@ -62,36 +52,11 @@ platform:
arch: arm64 arch: arm64
steps: steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: docker
image: plugins/docker
depends_on:
- submodules
settings:
username:
from_secret: DOCKER_USER
password:
from_secret: DOCKER_PASSWORD
repo: sist2app/sist2
context: ./
dockerfile: ./Dockerfile.arm64
auto_tag: true
auto_tag_suffix: arm64-linux
when:
event:
- tag
- name: build - name: build
image: sist2app/sist2-build-arm64 image: simon987/sist2-build-arm64
depends_on:
- submodules
commands: commands:
- ./scripts/build_arm64.sh - ./scripts/build_arm64.sh
- name: scp files - name: scp files
depends_on:
- build
image: appleboy/drone-scp image: appleboy/drone-scp
settings: settings:
host: host:
@ -102,7 +67,22 @@ steps:
from_secret: SSH_USER from_secret: SSH_USER
key: key:
from_secret: SSH_KEY from_secret: SSH_KEY
target: ~/files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/ target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
source: source:
- ./sist2-arm64-linux - ./sist2-arm64-linux
- ./sist2-arm64-linux-debug - ./sist2-arm64-linux-debug
- name: docker
image: plugins/docker
settings:
username:
from_secret: DOCKER_USER
password:
from_secret: DOCKER_PASSWORD
repo: simon987/sist2
context: ./
dockerfile: ./Dockerfile.arm64
auto_tag: true
auto_tag_suffix: arm64-linux
when:
event:
- tag

1
.gitignore vendored
View File

@ -3,7 +3,6 @@ thumbs
*.cbp *.cbp
CMakeCache.txt CMakeCache.txt
CMakeFiles CMakeFiles
cmake-build-default-event-trace
cmake-build-debug cmake-build-debug
cmake_install.cmake cmake_install.cmake
Makefile Makefile

View File

@ -53,6 +53,7 @@ add_executable(
src/types.h src/types.h
src/log.c src/log.h src/log.c src/log.h
src/cli.c src/cli.h src/cli.c src/cli.h
src/parsing/sidecar.c src/parsing/sidecar.h
src/database/database.c src/database/database.h src/database/database.c src/database/database.h
src/parsing/fs_util.h src/parsing/fs_util.h
@ -62,7 +63,7 @@ add_executable(
src/database/database_schema.c src/database/database_schema.c
src/database/database_fts.c src/database/database_fts.c
src/web/web_fts.c src/web/web_fts.c
src/database/database_embeddings.c) )
set_target_properties(sist2 PROPERTIES LINKER_LANGUAGE C) set_target_properties(sist2 PROPERTIES LINKER_LANGUAGE C)
target_link_directories(sist2 PRIVATE BEFORE ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/) target_link_directories(sist2 PRIVATE BEFORE ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/)
@ -75,7 +76,6 @@ find_package(unofficial-mongoose CONFIG REQUIRED)
find_package(CURL CONFIG REQUIRED) find_package(CURL CONFIG REQUIRED)
find_library(MAGIC_LIB NAMES libmagic.a REQUIRED) find_library(MAGIC_LIB NAMES libmagic.a REQUIRED)
find_package(unofficial-sqlite3 CONFIG REQUIRED) find_package(unofficial-sqlite3 CONFIG REQUIRED)
find_package(OpenBLAS CONFIG REQUIRED)
target_include_directories( target_include_directories(
@ -147,7 +147,6 @@ add_dependencies(
target_link_libraries( target_link_libraries(
sist2 sist2
m
z z
argparse argparse
unofficial::mongoose::mongoose unofficial::mongoose::mongoose
@ -159,7 +158,6 @@ target_link_libraries(
${MAGIC_LIB} ${MAGIC_LIB}
unofficial::sqlite3::sqlite3 unofficial::sqlite3::sqlite3
OpenBLAS::OpenBLAS
) )
add_custom_target( add_custom_target(

View File

@ -1,4 +1,5 @@
FROM sist2app/sist2-build as build FROM simon987/sist2-build as build
MAINTAINER simon987 <me@simon987.net>
WORKDIR /build/ WORKDIR /build/
@ -47,6 +48,5 @@ COPY --from=build /build/build/sist2 /root/sist2
# sist2-admin # sist2-admin
WORKDIR /root/sist2-admin WORKDIR /root/sist2-admin
COPY sist2-admin/requirements.txt /root/sist2-admin/ COPY sist2-admin/requirements.txt /root/sist2-admin/
RUN ln /usr/bin/python3 /usr/bin/python RUN python3 -m pip install --no-cache -r /root/sist2-admin/requirements.txt
RUN python -m pip install --no-cache -r /root/sist2-admin/requirements.txt
COPY --from=build /build/sist2-admin/ /root/sist2-admin/ COPY --from=build /build/sist2-admin/ /root/sist2-admin/

View File

@ -1,4 +1,5 @@
FROM sist2app/sist2-build-arm64 as build FROM simon987/sist2-build-arm64 as build
MAINTAINER simon987 <me@simon987.net>
WORKDIR /build/ WORKDIR /build/

View File

@ -1,11 +1,9 @@
![GitHub](https://img.shields.io/github/license/sist2app/sist2.svg) ![GitHub](https://img.shields.io/github/license/simon987/sist2.svg)
[![CodeFactor](https://www.codefactor.io/repository/github/sist2app/sist2/badge?s=05daa325188aac4eae32c786f3d9cf4e0593f822)](https://www.codefactor.io/repository/github/sist2app/sist2) [![CodeFactor](https://www.codefactor.io/repository/github/simon987/sist2/badge?s=05daa325188aac4eae32c786f3d9cf4e0593f822)](https://www.codefactor.io/repository/github/simon987/sist2)
[![Development snapshots](https://ci.simon987.net/api/badges/simon987/sist2/status.svg)](https://files.simon987.net/.gate/sist2/simon987_sist2/) [![Development snapshots](https://ci.simon987.net/api/badges/simon987/sist2/status.svg)](https://files.simon987.net/.gate/sist2/simon987_sist2/)
**Demo**: [sist2.simon987.net](https://sist2.simon987.net/) **Demo**: [sist2.simon987.net](https://sist2.simon987.net/)
**Community URL:** [Discord](https://discord.gg/2PEjDy3Rfs)
# sist2 # sist2
sist2 (Simple incremental search tool) sist2 (Simple incremental search tool)
@ -38,32 +36,26 @@ sist2 (Simple incremental search tool)
### Using Docker Compose *(Windows/Linux/Mac)* ### Using Docker Compose *(Windows/Linux/Mac)*
```yaml ```yaml
version: "3"
services: services:
elasticsearch: elasticsearch:
image: elasticsearch:7.17.9 image: elasticsearch:7.17.9
restart: unless-stopped restart: unless-stopped
volumes:
# This directory must have 1000:1000 permissions (or update PUID & PGID below)
- /data/sist2-es-data/:/usr/share/elasticsearch/data
environment: environment:
- "discovery.type=single-node" - "discovery.type=single-node"
- "ES_JAVA_OPTS=-Xms2g -Xmx2g" - "ES_JAVA_OPTS=-Xms2g -Xmx2g"
- "PUID=1000"
- "PGID=1000"
sist2-admin: sist2-admin:
image: sist2app/sist2:x64-linux image: simon987/sist2:3.1.0-x64-linux
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- /data/sist2-admin-data/:/sist2-admin/ - ./sist2-admin-data/:/sist2-admin/
- /<path to index>/:/host - /:/host
ports: ports:
- 4090:4090 - 4090:4090 # sist2
# NOTE: Don't expose this port publicly! - 8080:8080 # sist2-admin
- 8080:8080
working_dir: /root/sist2-admin/ working_dir: /root/sist2-admin/
entrypoint: python3 entrypoint: python3 /root/sist2-admin/sist2_admin/app.py
command:
- /root/sist2-admin/sist2_admin/app.py
``` ```
Navigate to http://localhost:8080/ to configure sist2-admin. Navigate to http://localhost:8080/ to configure sist2-admin.
@ -79,7 +71,7 @@ Navigate to http://localhost:8080/ to configure sist2-admin.
``` ```
* **SQLite**: No installation required * **SQLite**: No installation required
2. Download the [latest sist2 release](https://github.com/sist2app/sist2/releases). 2. Download the [latest sist2 release](https://github.com/simon987/sist2/releases).
Select the file corresponding to your CPU architecture and mark the binary as executable with `chmod +x`. Select the file corresponding to your CPU architecture and mark the binary as executable with `chmod +x`.
3. See [usage guide](docs/USAGE.md) for command line usage. 3. See [usage guide](docs/USAGE.md) for command line usage.
@ -88,30 +80,28 @@ Example usage:
1. Scan a directory: `sist2 scan ~/Documents --output ./documents.sist2` 1. Scan a directory: `sist2 scan ~/Documents --output ./documents.sist2`
2. Prepare search index: 2. Prepare search index:
* **Elasticsearch**: `sist2 index --es-url http://localhost:9200 ./documents.sist2` * **Elasticsearch**: `sist2 index --es-url http://localhost:9200 ./documents.sist2`
* **SQLite**: `sist2 sqlite-index --search-index ./search.sist2 ./documents.sist2` * **SQLite**: `sist2 index --search-index ./search.sist2 ./documents.sist2`
3. Start web interface: 3. Start web interface: `sist2 web ./documents.sist2`
* **Elasticsearch**: `sist2 web ./documents.sist2`
* **SQLite**: `sist2 web --search-index ./search.sist2 ./documents.sist2`
## Format support ## Format support
| File type | Library | Content | Thumbnail | Metadata | | File type | Library | Content | Thumbnail | Metadata |
|:--------------------------------------------------------------------------|:-----------------------------------------------------------------------------|:---------|:------------|:---------------------------------------------------------------------------------------------------------------------------------------| |:--------------------------------------------------------------------------|:-----------------------------------------------------------------------------|:---------|:------------|:---------------------------------------------------------------------------------------------------------------------------------------|
| pdf,xps,fb2,epub | MuPDF | text+ocr | yes | author, title | | pdf,xps,fb2,epub | MuPDF | text+ocr | yes | author, title |
| cbz,cbr | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | - | yes | - | | cbz,cbr | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | - | yes | - |
| `audio/*` | ffmpeg | - | yes | ID3 tags | | `audio/*` | ffmpeg | - | yes | ID3 tags |
| `video/*` | ffmpeg | - | yes | title, comment, artist | | `video/*` | ffmpeg | - | yes | title, comment, artist |
| `image/*` | ffmpeg | ocr | yes | [Common EXIF tags](https://github.com/sist2app/sist2/blob/efdde2734eca9b14a54f84568863b7ffd59bdba3/src/parsing/media.c#L190), GPS tags | | `image/*` | ffmpeg | ocr | yes | [Common EXIF tags](https://github.com/simon987/sist2/blob/efdde2734eca9b14a54f84568863b7ffd59bdba3/src/parsing/media.c#L190), GPS tags |
| raw, rw2, dng, cr2, crw, dcr, k25, kdc, mrw, pef, xf3, arw, sr2, srf, erf | LibRaw | no | yes | Common EXIF tags, GPS tags | | raw, rw2, dng, cr2, crw, dcr, k25, kdc, mrw, pef, xf3, arw, sr2, srf, erf | LibRaw | no | yes | Common EXIF tags, GPS tags |
| ttf,ttc,cff,woff,fnt,otf | Freetype2 | - | yes, `bmp` | Name & style | | ttf,ttc,cff,woff,fnt,otf | Freetype2 | - | yes, `bmp` | Name & style |
| `text/plain` | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | yes | no | - | | `text/plain` | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | no | - |
| html, xml | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | yes | no | - | | html, xml | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | no | - |
| tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no | | tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no |
| docx, xlsx, pptx | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | yes | if embedded | creator, modified_by, title | | docx, xlsx, pptx | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | if embedded | creator, modified_by, title |
| doc (MS Word 97-2003) | antiword | yes | no | author, title | | doc (MS Word 97-2003) | antiword | yes | no | author, title |
| mobi, azw, azw3 | libmobi | yes | yes | author, title | | mobi, azw, azw3 | libmobi | yes | yes | author, title |
| wpd (WordPerfect) | libwpd | yes | no | *planned* | | wpd (WordPerfect) | libwpd | yes | no | *planned* |
| json, jsonl, ndjson | [libscan](https://github.com/sist2app/sist2/tree/master/third-party/libscan) | yes | - | - | | json, jsonl, ndjson | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | - | - |
\* *See [Archive files](#archive-files)* \* *See [Archive files](#archive-files)*
@ -135,7 +125,7 @@ You can enable OCR support for ebook (pdf,xps,fb2,epub) or image file types with
Download the language data files with your package manager (`apt install tesseract-ocr-eng`) or Download the language data files with your package manager (`apt install tesseract-ocr-eng`) or
directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files). directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files).
The `sist2app/sist2` image comes with common languages The `simon987/sist2` image comes with common languages
(hin, jpn, eng, fra, rus, spa, chi_sim, deu, pol) pre-installed. (hin, jpn, eng, fra, rus, spa, chi_sim, deu, pol) pre-installed.
You can use the `+` separator to specify multiple languages. The language You can use the `+` separator to specify multiple languages. The language
@ -156,17 +146,17 @@ sist2 v3.0.7+ supports SQLite search backend. The SQLite search backend has
fewer features and generally comparable query performance for medium-size fewer features and generally comparable query performance for medium-size
indices, but it uses much less memory and is easier to set up. indices, but it uses much less memory and is easier to set up.
| | SQLite | Elasticsearch | | | SQLite | Elasticsearch |
|----------------------------------------------|:---------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------:| |----------------------------------------------|:----------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------:|
| Requires separate search engine installation | | ✓ | | Requires separate search engine installation | | ✓ |
| Memory footprint | ~20MB | >500MB | | Memory footprint | ~20MB | >500MB |
| Query syntax | [fts5](https://www.sqlite.org/fts5.html) | [query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax) | | Query syntax | [fts5](https://www.sqlite.org/fts5.html) | [query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax) |
| Fuzzy search | | ✓ | | Fuzzy search | | ✓ |
| Media Types tree real-time updating | | ✓ | | Media Types tree real-time updating | | ✓ |
| Manual tagging | ✓ | ✓ | | Search in file `path` | | ✓ |
| User scripts | | ✓ | | Manual tagging | ✓ | ✓ |
| Media Type breakdown for search results | | ✓ | | User scripts | | ✓ |
| Embeddings search | ✓ *O(n)* | ✓ *O(logn)* | | Media Type breakdown for search results | | ✓ |
### NER ### NER
@ -175,13 +165,13 @@ sist2 v3.0.4+ supports named-entity recognition (NER). Simply add a supported re
to enable it. to enable it.
The text processing is done in your browser, no data is sent to any third-party services. The text processing is done in your browser, no data is sent to any third-party services.
See [sist2app/sist2-ner-models](https://github.com/sist2app/sist2-ner-models) for more details. See [simon987/sist2-ner-models](https://github.com/simon987/sist2-ner-models) for more details.
#### List of available repositories: #### List of available repositories:
| URL | Maintainer | Purpose | | URL | Maintainer | Purpose |
|---------------------------------------------------------------------------------------------------------|-----------------------------------------|---------| |---------------------------------------------------------------------------------------------------------|-----------------------------------------|---------|
| [sist2app/sist2-ner-models](https://raw.githubusercontent.com/sist2app/sist2-ner-models/main/repo.json) | [sist2app](https://github.com/sist2app) | General | | [simon987/sist2-ner-models](https://raw.githubusercontent.com/simon987/sist2-ner-models/main/repo.json) | [simon987](https://github.com/simon987) | General |
<details> <details>
<summary>Screenshot</summary> <summary>Screenshot</summary>
@ -197,7 +187,7 @@ You can compile **sist2** by yourself if you don't want to use the pre-compiled
### Using docker ### Using docker
```bash ```bash
git clone --recursive https://github.com/sist2app/sist2/ git clone --recursive https://github.com/simon987/sist2/
cd sist2 cd sist2
docker build . -t my-sist2-image docker build . -t my-sist2-image
# Copy sist2 executable from docker image # Copy sist2 executable from docker image
@ -212,16 +202,16 @@ docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
apt install gcc g++ python3 yasm ragel automake autotools-dev wget libtool libssl-dev curl zip unzip tar xorg-dev libglu1-mesa-dev libxcursor-dev libxml2-dev libxinerama-dev gettext nasm git nodejs apt install gcc g++ python3 yasm ragel automake autotools-dev wget libtool libssl-dev curl zip unzip tar xorg-dev libglu1-mesa-dev libxcursor-dev libxml2-dev libxinerama-dev gettext nasm git nodejs
``` ```
2. Install vcpkg using my fork: https://github.com/sist2app/vcpkg 2. Install vcpkg using my fork: https://github.com/simon987/vcpkg
3. Install vcpkg dependencies 3. Install vcpkg dependencies
```bash ```bash
vcpkg install openblas curl[core,openssl] sqlite3[core,fts5,json1] cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf[ocr] gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample,webp,opus,mp3lame,vpx,zlib] vcpkg install curl[core,openssl] sqlite3[core,fts5] cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample,webp,opus,mp3lame,vpx,ffprobe,zlib]
``` ```
4. Build 4. Build
```bash ```bash
git clone --recursive https://github.com/sist2app/sist2/ git clone --recursive https://github.com/simon987/sist2/
(cd sist2-vue; npm install; npm run build) (cd sist2-vue; npm install; npm run build)
(cd sist2-admin/frontend; npm install; npm run build) (cd sist2-admin/frontend; npm install; npm run build)
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake . cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake .

View File

@ -4,21 +4,16 @@ services:
elasticsearch: elasticsearch:
image: elasticsearch:7.17.9 image: elasticsearch:7.17.9
container_name: sist2-es container_name: sist2-es
volumes:
# This directory must have 1000:1000 permissions (or update PUID & PGID below)
- /data/sist2-es-data/:/usr/share/elasticsearch/data
environment: environment:
- "discovery.type=single-node" - "discovery.type=single-node"
- "ES_JAVA_OPTS=-Xms2g -Xmx2g" - "ES_JAVA_OPTS=-Xms2g -Xmx2g"
- "PUID=1000"
- "PGID=1000"
sist2-admin: sist2-admin:
build: build:
context: . context: .
container_name: sist2-admin container_name: sist2-admin
volumes: volumes:
- /data/sist2-admin-data/:/sist2-admin/ - /mnt/array/sist2-admin-data/:/sist2-admin/
- /<path to index>/:/host - /:/host
ports: ports:
- 4090:4090 - 4090:4090
# NOTE: Don't export this port publicly! # NOTE: Don't export this port publicly!

View File

@ -5,6 +5,7 @@ Usage: sist2 scan [OPTION]... PATH
or: sist2 index [OPTION]... INDEX or: sist2 index [OPTION]... INDEX
or: sist2 sqlite-index [OPTION]... INDEX or: sist2 sqlite-index [OPTION]... INDEX
or: sist2 web [OPTION]... INDEX... or: sist2 web [OPTION]... INDEX...
or: sist2 exec-script [OPTION]... INDEX
Lightning-fast file system indexer and search tool. Lightning-fast file system indexer and search tool.
@ -16,9 +17,9 @@ Lightning-fast file system indexer and search tool.
Scan options Scan options
-t, --threads=<int> Number of threads. DEFAULT: 1 -t, --threads=<int> Number of threads. DEFAULT: 1
-q, --thumbnail_count-quality=<int> Thumbnail quality, on a scale of 0 to 100, 100 being the best. DEFAULT: 50 -q, --thumbnail-quality=<int> Thumbnail quality, on a scale of 0 to 100, 100 being the best. DEFAULT: 50
--thumbnail_count-size=<int> Thumbnail size, in pixels. DEFAULT: 552 --thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT: 552
--thumbnail_count-count=<int> Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails. DEFAULT: 1 --thumbnail-count=<int> Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails. DEFAULT: 1
--content-size=<int> Number of bytes to be extracted from text documents. Set to 0 to disable. DEFAULT: 32768 --content-size=<int> Number of bytes to be extracted from text documents. Set to 0 to disable. DEFAULT: 32768
-o, --output=<str> Output index file path. DEFAULT: index.sist2 -o, --output=<str> Output index file path. DEFAULT: index.sist2
--incremental If the output file path exists, only scan new or modified files. --incremental If the output file path exists, only scan new or modified files.
@ -73,14 +74,21 @@ Web options
--dev Serve html & js files from disk (for development) --dev Serve html & js files from disk (for development)
--lang=<str> Default UI language. Can be changed by the user --lang=<str> Default UI language. Can be changed by the user
Exec-script options
--es-url=<str> Elasticsearch url. DEFAULT: http://localhost:9200
--es-insecure-ssl Do not verify SSL connections to Elasticsearch.
--es-index=<str> Elasticsearch index name. DEFAULT: sist2
--script-file=<str> Path to user script.
--async-script Execute user script asynchronously.
Made by simon987 <me@simon987.net>. Released under GPL-3.0 Made by simon987 <me@simon987.net>. Released under GPL-3.0
``` ```
#### Thumbnail database size estimation #### Thumbnail database size estimation
See chart below for rough estimate of thumbnail_count size vs. thumbnail_count size & quality arguments: See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments:
For example, `--thumbnail_count-size=500`, `--thumbnail_count-quality=50` for a directory with 8 million images will create a thumbnail_count database For example, `--thumbnail-size=500`, `--thumbnail-quality=50` for a directory with 8 million images will create a thumbnail database
that is about `8000000 * 11.8kB = 94.4GB`. that is about `8000000 * 11.8kB = 94.4GB`.
![thumbnail_size](thumbnail_size.png) ![thumbnail_size](thumbnail_size.png)
@ -92,7 +100,7 @@ Simple scan
sist2 scan ~/Documents sist2 scan ~/Documents
sist2 scan \ sist2 scan \
--threads 4 --content-size 16000000 --thumbnail_count-quality 2 --archive shallow \ --threads 4 --content-size 16000000 --thumbnail-quality 2 --archive shallow \
--name "My Documents" --rewrite-url "http://nas.domain.local/My Documents/" \ --name "My Documents" --rewrite-url "http://nas.domain.local/My Documents/" \
~/Documents -o ./documents.sist2 ~/Documents -o ./documents.sist2
``` ```
@ -172,37 +180,12 @@ Using a version >=7.14.0 is recommended to enable the following features:
- Bug fix for large documents (See #198) - Bug fix for large documents (See #198)
Using a version >=8.0.0 is recommended to enable the following features:
- Approximate KNN search for Embeddings search (faster queries).
When using a legacy version of ES, a notice will be displayed next to the sist2 version in the web UI. When using a legacy version of ES, a notice will be displayed next to the sist2 version in the web UI.
If you don't care about the features above, you can ignore it or disable it in the configuration page. If you don't care about the features above, you can ignore it or disable it in the configuration page.
# Embeddings search ## exec-script
Since v3.2.0, User scripts can be used to generate _embeddings_ (vector of float32 numbers) which are stored in the .sist2 index file The `exec-script` command is used to execute a user script for an index that has already been imported to Elasticsearch with the `index` command. Note that the documents will not be reset to their default state before each execution as the `index` command does: if you make undesired changes to the documents by accident, you will need to run `index` again to revert to the original state.
(see [scripting](scripting.md)). Embeddings can be used for:
* Nearest-neighbor queries (e.g. "return the documents most similar to this one")
* Semantic searches (e.g. "return the documents that are most closely related to the given topic")
In theory, embeddings can be created for any type of documents (image, text, audio etc.).
For example, the [clip](https://github.com/sist2app/sist2-script-clip) User Script, generates 512-d embeddings of images
(videos are also supported using the thumbnails generated by sist2). When the user enters a query in the "Embeddings Search"
textbox, the query's embedding is generated in their browser, leveraging the ONNX web runtime.
<details>
<summary>Screenshots</summary>
![embeddings-1](embeddings-1.png)
![embeddings-2](embeddings-2.png)
1. Embeddings search bar. You can select the model using the dropdown on the left.
2. This icon appears for indices with embeddings search enabled.
3. Documents with this icon have embeddings. Click on the icon to perform KNN search.
</details>
# Tagging # Tagging
@ -230,3 +213,42 @@ See [Automatic tagging](#automatic-tagging) for information about tag
### Automatic tagging ### Automatic tagging
See [scripting](scripting.md) documentation. See [scripting](scripting.md) documentation.
# Sidecar files
When scanning, sist2 will read metadata from `.s2meta` JSON files and overwrite the
original document's indexed metadata (does not modify the actual file). Sidecar metadata files will also work inside archives.
Sidecar files themselves are not saved in the index.
This feature is useful to leverage third-party applications such as speech-to-text or
OCR to add additional metadata to a file.
**Example**
```
~/Documents/
├── Video.mp4
└── Video.mp4.s2meta
```
The sidecar file must have exactly the same file path and the `.s2meta` suffix.
`Video.mp4.s2meta`:
```json
{
"content": "This sidecar file will overwrite some metadata fields of Video.mp4",
"author": "Some author",
"duration": 12345,
"bitrate": 67890,
"some_arbitrary_field": [1,2,3]
}
```
```
sist2 scan ~/Documents -o ./docs.sist2
sist2 index ./docs.sist2
```
*NOTE*: It is technically possible to overwrite the `tag` value using sidecar files, however,
it is not currently possible to restore both manual tags and sidecar tags without user scripts
while reindexing.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

View File

@ -1,47 +1,18 @@
## User scripts ## User scripts
User scripts are used to augment your sist2 index with additional metadata, neural network embeddings, tags etc. *This document is under construction, more in-depth guide coming soon*
Since version 3.2.0, user scripts are written in Python, and are ran against the sist2 index file. User scripts do not
need a connection to the search backend.
You can create a user script based on a template from the sist2-admin interface:
![sist2-admin-scripts](sist2-admin-scripts.png)
User scripts leverage the [sist2-python](https://github.com/simon987/sist2-python) library to interface with the
index file*. You can find sist2-python documentation and examples
here: [sist2-python.readthedocs.io](https://sist2-python.readthedocs.io/).
If you are not using the sist2-admin interface, you can run user scripts manually from the command line:
```
pip install git+https://github.com/simon987/sist2-python.git
python my_script.py /path/to/my_index.sist2
```
\* It is possible to manually update the index using raw SQL queries, but the database schema is not stable and
can change at any time; it is recommended to use the more stable sist2-python wrapper instead.
<hr>
<details>
<summary>Legacy user scripts (sist2 version < 3.2.0)</summary>
During the `index` step, you can use the `--script-file <script>` option to During the `index` step, you can use the `--script-file <script>` option to
modify documents or add user tags. This option is mainly used to modify documents or add user tags. This option is mainly used to
implement automatic tagging based on file attributes. implement automatic tagging based on file attributes.
The scripting language used The scripting language used
([Painless Scripting Language](https://www.elastic.co/guide/en/elasticsearch/painless/7.4/index.html)) ([Painless Scripting Language](https://www.elastic.co/guide/en/elasticsearch/painless/7.4/index.html))
is very similar to Java, but you should be able to create user scripts is very similar to Java, but you should be able to create user scripts
without programming experience at all if you're somewhat familiar with without programming experience at all if you're somewhat familiar with
regex. regex.
This is the base structure of the documents we're working with: This is the base structure of the documents we're working with:
```json ```json
{ {
"_id": "e171405c-fdb5-4feb-bb32-82637bc32084", "_id": "e171405c-fdb5-4feb-bb32-82637bc32084",
@ -63,8 +34,7 @@ This is the base structure of the documents we're working with:
**Example script** **Example script**
This script checks if the `genre` attribute exists, if it does This script checks if the `genre` attribute exists, if it does
it adds the `genre.<genre>` tag. it adds the `genre.<genre>` tag.
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@ -77,23 +47,21 @@ You can use `.` to create a hierarchical tag tree:
![scripting/genre_example](genre_example.png) ![scripting/genre_example](genre_example.png)
To use regular expressions, you need to add this line in `/etc/elasticsearch/elasticsearch.yml`
To use regular expressions, you need to add this line in `/etc/elasticsearch/elasticsearch.yml`
```yaml ```yaml
script.painless.regex.enabled: true script.painless.regex.enabled: true
``` ```
Or, if you're using docker add `-e "script.painless.regex.enabled=true"` Or, if you're using docker add `-e "script.painless.regex.enabled=true"`
**Tag color** **Tag color**
You can specify the color for an individual tag by appending an You can specify the color for an individual tag by appending an
hexadecimal color code (`#RRGGBBAA`) to the tag name. hexadecimal color code (`#RRGGBBAA`) to the tag name.
### Examples ### Examples
If `(20XX)` is in the file name, add the `year.<year>` tag: If `(20XX)` is in the file name, add the `year.<year>` tag:
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@ -104,7 +72,6 @@ if (m.find()) {
``` ```
Use default *Calibre* folder structure to infer author. Use default *Calibre* folder structure to infer author.
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@ -117,9 +84,8 @@ if (ctx._source.name.contains("-") && ctx._source.extension == "pdf") {
} }
``` ```
If the file matches a specific pattern `AAAA-000 fName1 lName1, <fName2 lName2>...`, add the `actress.<actress>` and If the file matches a specific pattern `AAAA-000 fName1 lName1, <fName2 lName2>...`, add the `actress.<actress>` and
`studio.<studio>` tag: `studio.<studio>` tag:
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
@ -136,18 +102,16 @@ if (m.find()) {
``` ```
Set the name of the last folder (`/path/to/<studio>/file.mp4`) to `studio.<studio>` tag Set the name of the last folder (`/path/to/<studio>/file.mp4`) to `studio.<studio>` tag
```Java ```Java
ArrayList tags = ctx._source.tag = new ArrayList(); ArrayList tags = ctx._source.tag = new ArrayList();
if (ctx._source.path != "") { if (ctx._source.path != "") {
String[] names = ctx._source.path.splitOnToken('/'); String[] names = ctx._source.path.splitOnToken('/');
tags.add("studio." + names[names.length-1]); tags.add("studio." + names[names.length-1]);
} }
``` ```
Parse `EXIF:F Number` tag Parse `EXIF:F Number` tag
```Java ```Java
if (ctx._source?.exif_fnumber != null) { if (ctx._source?.exif_fnumber != null) {
String[] values = ctx._source.exif_fnumber.splitOnToken(' '); String[] values = ctx._source.exif_fnumber.splitOnToken(' ');
@ -160,7 +124,6 @@ if (ctx._source?.exif_fnumber != null) {
``` ```
Display year and months from `EXIF:DateTime` tag Display year and months from `EXIF:DateTime` tag
```Java ```Java
if (ctx._source?.exif_datetime != null) { if (ctx._source?.exif_datetime != null) {
SimpleDateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); SimpleDateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
@ -177,6 +140,3 @@ if (ctx._source?.exif_datetime != null) {
} }
``` ```
</details>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@ -202,46 +202,6 @@
}, },
"modified_by": { "modified_by": {
"type": "text" "type": "text"
},
"emb.384.*": {
"type": "dense_vector",
"dims": 384
},
"emb.idx_384.*": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
},
"emb.idx_512.clip": {
"type": "dense_vector",
"dims": 512,
"index": true,
"similarity": "cosine"
},
"emb.512.*": {
"type": "dense_vector",
"dims": 512
},
"emb.idx_768.*": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"emb.768.*": {
"type": "dense_vector",
"dims": 768
},
"emb.idx_1024.*": {
"type": "dense_vector",
"dims": 1024,
"index": true,
"similarity": "cosine"
},
"emb.1024.*": {
"type": "dense_vector",
"dims": 1024
} }
} }
} }

View File

@ -2,6 +2,8 @@
VCPKG_ROOT="/vcpkg" VCPKG_ROOT="/vcpkg"
git submodule update --init --recursive
( (
cd sist2-vue/ cd sist2-vue/
npm install npm install

View File

@ -1,16 +1,8 @@
MAGIC_PATHS = [ try:
"/vcpkg/installed/x64-linux/share/libmagic/misc/magic.mgc", with open("/usr/lib/file/magic.mgc", "rb") as f:
"/work/vcpkg/installed/x64-linux/share/libmagic/misc/magic.mgc", data = f.read()
"/usr/lib/file/magic.mgc" except:
] data = bytes([])
for path in MAGIC_PATHS:
try:
with open(path, "rb") as f:
data = f.read()
break
except:
continue
print("char magic_database_buffer[%d] = {%s};" % (len(data), ",".join(str(int(b)) for b in data))) print("char magic_database_buffer[%d] = {%s};" % (len(data), ",".join(str(int(b)) for b in data)))

View File

@ -449,4 +449,5 @@ image/x-sigma-x3f, xf3
image/x-sony-arw, arw image/x-sony-arw, arw
image/x-sony-sr2, sr2 image/x-sony-sr2, sr2
image/x-sony-srf, srf image/x-sony-srf, srf
image/x-epson-erf, erf image/x-epson-erf, erf
sist2/sidecar, s2meta
1 application/x-matlab-data mat
449 image/x-sony-arw arw
450 image/x-sony-sr2 sr2
451 image/x-sony-srf srf
452 image/x-epson-erf erf
453 sist2/sidecar s2meta

View File

@ -3,7 +3,6 @@ import zlib
mimes = {} mimes = {}
noparse = set() noparse = set()
ext_in_hash = set() ext_in_hash = set()
mime_ids = {}
major_mime = { major_mime = {
"sist2": 0, "sist2": 0,
@ -103,9 +102,6 @@ cnt = 1
def mime_id(mime): def mime_id(mime):
if mime in mime_ids:
return mime_ids[mime]
global cnt global cnt
major = mime.split("/")[0] major = mime.split("/")[0]
mime_id = str((major_mime[major] << 16) + cnt) mime_id = str((major_mime[major] << 16) + cnt)
@ -131,7 +127,9 @@ def mime_id(mime):
elif mime == "application/x-empty": elif mime == "application/x-empty":
cnt -= 1 cnt -= 1
return "1" return "1"
mime_ids[mime] = mime_id elif mime == "sist2/sidecar":
cnt -= 1
return "2"
return mime_id return mime_id
@ -199,12 +197,4 @@ with open("scripts/mime.csv") as f:
print(f"case {crc(mime)}: return {clean(mime)};") print(f"case {crc(mime)}: return {clean(mime)};")
print("default: return 0;}}") print("default: return 0;}}")
# mime list
mime_list = ",".join(mime_id(x) for x in mimes.keys()) + ",0"
print(f"unsigned int mime_ids[] = {{{mime_list}}};")
print("unsigned int* get_mime_ids() { return mime_ids; }")
print("#endif") print("#endif")

View File

@ -1,3 +1,3 @@
docker run --rm -it --name "sist2-dev-es3"\ docker run --rm -it --name "sist2-dev-es"\
-p 9200:9200 -e "discovery.type=single-node" \ -p 9200:9200 -e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.17.9 -e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.17.9

View File

@ -1,3 +1,3 @@
docker run --rm -it --name "sist2-dev-es3"\ docker run --rm -it --name "sist2-dev-es"\
-p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" \ -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:8.7.0 -e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:8.7.0

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
"watch": "vue-cli-service build --watch" "watch": "vue-cli-service build --watch"
}, },
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^0.27.2",
"bootstrap-vue": "^2.21.2", "bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"moment": "^2.29.3", "moment": "^2.29.3",

View File

@ -4,10 +4,10 @@
<b-container class="pt-4"> <b-container class="pt-4">
<b-alert show dismissible variant="info"> <b-alert show dismissible variant="info">
This is a beta version of sist2-admin. Please submit bug reports, usability issues and feature requests This is a beta version of sist2-admin. Please submit bug reports, usability issues and feature requests
to the <a href="https://github.com/sist2app/sist2/issues/new/choose" target="_blank">issue tracker on to the <a href="https://github.com/simon987/sist2/issues/new/choose" target="_blank">issue tracker on
Github</a>. Thank you! Github</a>. Thank you!
</b-alert> </b-alert>
<router-view v-if="$store.state.sist2AdminInfo"/> <router-view/>
</b-container> </b-container>
</div> </div>
</template> </template>
@ -71,12 +71,10 @@ html, body {
.info-icon { .info-icon {
width: 1rem; width: 1rem;
min-width: 1rem;
margin-right: 0.2rem; margin-right: 0.2rem;
cursor: pointer; cursor: pointer;
line-height: 1rem; line-height: 1rem;
height: 1rem; height: 1rem;
min-height: 1rem;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==); background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
filter: brightness(45%); filter: brightness(45%);
display: block; display: block;

View File

@ -89,12 +89,9 @@ class Sist2AdminApi {
/** /**
* @param {string} name * @param {string} name
* @param {bool} full
*/ */
runJob(name, full) { runJob(name) {
return axios.get(`${this.baseUrl}/api/job/${name}/run`, { return axios.get(`${this.baseUrl}/api/job/${name}/run`);
params: {full}
});
} }
/** /**
@ -142,38 +139,6 @@ class Sist2AdminApi {
deleteTaskLogs(taskId) { deleteTaskLogs(taskId) {
return axios.post(`${this.baseUrl}/api/task/${taskId}/delete_logs`); return axios.post(`${this.baseUrl}/api/task/${taskId}/delete_logs`);
} }
getUserScripts() {
return axios.get(`${this.baseUrl}/api/user_script`);
}
getUserScript(name) {
return axios.get(`${this.baseUrl}/api/user_script/${name}`);
}
createUserScript(name, template) {
return axios.post(`${this.baseUrl}/api/user_script/${name}`, null, {
params: {
template: template
}
});
}
updateUserScript(name, data) {
return axios.put(`${this.baseUrl}/api/user_script/${name}`, data);
}
deleteUserScript(name) {
return axios.delete(`${this.baseUrl}/api/user_script/${name}`);
}
testUserScript(name, job) {
return axios.get(`${this.baseUrl}/api/user_script/${name}/run`, {
params: {
job: job
}
});
}
} }
export default new Sist2AdminApi() export default new Sist2AdminApi()

View File

@ -1,54 +1,42 @@
<template> <template>
<div> <div>
<h5>{{ $t("selectJobs") }}</h5> <h5>{{ $t("selectJobs") }}</h5>
<b-progress v-if="loading" striped animated value="100"></b-progress> <b-progress v-if="loading" striped animated value="100"></b-progress>
<b-form-group v-else> <b-form-group v-else>
<b-form-checkbox-group <b-form-checkbox-group
v-if="jobs.length > 0" v-if="jobs.length > 0"
:checked="frontend.jobs" :checked="frontend.jobs"
@input="frontend.jobs = $event; $emit('input')" @input="frontend.jobs = $event; $emit('input')"
> >
<div v-for="job in jobs" :key="job.name"> <div v-for="job in jobs" :key="job.name">
<b-form-checkbox :disabled="job.status !== 'indexed'" <b-form-checkbox :disabled="job.status !== 'indexed'" :value="job.name">[{{ job.name }}]</b-form-checkbox>
:value="job.name"> <br/>
<template #default><span </div>
:title="job.status !== 'indexed' ? $t('jobOptions.notIndexed') : ''" </b-form-checkbox-group>
>[{{ job.name }}]</span></template> <div v-else>
</b-form-checkbox> <span class="text-muted">{{ $t('jobOptions.noJobAvailable') }}</span>
<br/> &nbsp;<router-link to="/">{{$t("create")}}</router-link>
</div> </div>
</b-form-checkbox-group> </b-form-group>
<div v-else> </div>
<span class="text-muted">{{ $t('jobOptions.noJobAvailable') }}</span>
<router-link to="/">{{ $t("create") }}</router-link>
</div>
</b-form-group>
</div>
</template> </template>
<script> <script>
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "JobCheckboxGroup", name: "JobCheckboxGroup",
props: ["frontend"], props: ["frontend"],
mounted() { mounted() {
Sist2AdminApi.getJobs().then(resp => { Sist2AdminApi.getJobs().then(resp => {
this._jobs = resp.data; this.jobs = resp.data;
this.loading = false; this.loading = false;
}); });
}, },
computed: { data() {
jobs() { return {
return this._jobs loading: true,
.filter(job => job.index_options.search_backend === this.frontend.web_options.search_backend)
}
},
data() {
return {
loading: true,
_jobs: null
}
} }
}
} }
</script> </script>

View File

@ -1,34 +0,0 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<span v-else-if="jobs.length === 0"></span>
<b-form-select v-else :options="jobs" text-field="name" value-field="name"
@change="$emit('change', $event)" :value="$t('selectJob')"></b-form-select>
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "JobSelect",
mounted() {
Sist2AdminApi.getJobs().then(resp => {
this._jobs = resp.data;
this.loading = false;
});
},
computed: {
jobs() {
return [
{name: this.$t("selectJob"), disabled: true},
...this._jobs.filter(job => job.index_path)
]
}
},
data() {
return {
loading: true,
_jobs: null
}
}
}
</script>

View File

@ -95,7 +95,6 @@ export default {
methods: { methods: {
onOcrLangChange() { onOcrLangChange() {
this.options.ocr_lang = this.selectedOcrLangs.join("+"); this.options.ocr_lang = this.selectedOcrLangs.join("+");
this.update();
}, },
update() { update() {
this.disableOcrLang = this.options.ocr_images === false && this.options.ocr_ebooks === false; this.disableOcrLang = this.options.ocr_images === false && this.options.ocr_ebooks === false;

View File

@ -1,18 +0,0 @@
<template>
<b-list-group-item action :to="`/userScript/${script.name}`">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
{{ script.name }}
</h5>
</div>
</b-list-group-item>
</template>
<script>
export default {
name: "UserScriptListItem",
props: ["script"],
}
</script>

View File

@ -1,88 +0,0 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-row v-else>
<b-col cols="6">
<h5>Selected scripts</h5>
<b-list-group>
<b-list-group-item v-for="script in selectedScripts" :key="script"
button
@click="onRemoveScript(script)"
class="d-flex justify-content-between align-items-center">
{{ script }}
<b-button-group>
<b-button variant="light" @click.stop="moveUpScript(script)"></b-button>
<b-button variant="light" @click.stop="moveDownScript(script)"></b-button>
</b-button-group>
</b-list-group-item>
</b-list-group>
</b-col>
<b-col cols="6">
<h5>Available scripts</h5>
<b-list-group>
<b-list-group-item v-for="script in availableScripts" :key="script" button
@click="onSelectScript(script)">
{{ script }}
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
<!-- <b-checkbox-group v-else :options="scripts" stacked :checked="selectedScripts"-->
<!-- @input="$emit('change', $event)"></b-checkbox-group>-->
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "UserScriptPicker",
props: ["selectedScripts"],
data() {
return {
loading: true,
scripts: []
}
},
computed: {
availableScripts() {
return this.scripts.filter(script => !this.selectedScripts.includes(script))
}
},
mounted() {
Sist2AdminApi.getUserScripts().then(resp => {
this.scripts = resp.data.map(script => script.name);
this.loading = false;
});
},
methods: {
onSelectScript(name) {
this.selectedScripts.push(name);
this.$emit("change", this.selectedScripts)
},
onRemoveScript(name) {
this.selectedScripts.splice(this.selectedScripts.indexOf(name), 1);
this.$emit("change", this.selectedScripts);
},
moveUpScript(name) {
const index = this.selectedScripts.indexOf(name);
if (index > 0) {
this.selectedScripts.splice(index, 1);
this.selectedScripts.splice(index - 1, 0, name);
}
this.$emit("change", this.selectedScripts);
},
moveDownScript(name) {
const index = this.selectedScripts.indexOf(name);
if (index < this.selectedScripts.length - 1) {
this.selectedScripts.splice(index, 1);
this.selectedScripts.splice(index + 1, 0, name);
}
this.$emit("change", this.selectedScripts);
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,70 +1,60 @@
<template> <template>
<div> <div>
<h4>{{ $t("webOptions.title") }}</h4>
<b-card>
<label>{{ $t("webOptions.lang") }}</label>
<b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN', 'pl', 'de']"
@change="update()"></b-form-select>
<label>{{ $t("webOptions.bind") }}</label> <label>{{ $t("webOptions.lang") }}</label>
<b-form-input v-model="options.bind" @change="update()"></b-form-input> <b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN']" @change="update()"></b-form-select>
<label>{{ $t("webOptions.tagline") }}</label> <label>{{ $t("webOptions.bind") }}</label>
<b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea> <b-form-input v-model="options.bind" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth") }}</label> <label>{{ $t("webOptions.tagline") }}</label>
<b-form-input v-model="options.auth" @change="update()"></b-form-input> <b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea>
<label>{{ $t("webOptions.tagAuth") }}</label> <label>{{ $t("webOptions.auth") }}</label>
<b-form-input v-model="options.tag_auth" @change="update()" :disabled="Boolean(options.auth)"></b-form-input> <b-form-input v-model="options.auth" @change="update()"></b-form-input>
<b-form-checkbox v-model="options.verbose" @change="update()"> <label>{{ $t("webOptions.tagAuth") }}</label>
{{$t("webOptions.verbose")}} <b-form-input v-model="options.tag_auth" @change="update()"></b-form-input>
</b-form-checkbox>
</b-card>
<br> <br>
<h4>Auth0 options</h4> <h5>Auth0 options</h5>
<b-card> <label>{{ $t("webOptions.auth0Audience") }}</label>
<label>{{ $t("webOptions.auth0Audience") }}</label> <b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
<b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth0Domain") }}</label> <label>{{ $t("webOptions.auth0Domain") }}</label>
<b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input> <b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth0ClientId") }}</label> <label>{{ $t("webOptions.auth0ClientId") }}</label>
<b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input> <b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
<label>{{ $t("webOptions.auth0PublicKey") }}</label> <label>{{ $t("webOptions.auth0PublicKey") }}</label>
<b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea> <b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
</b-card> </div>
</div>
</template> </template>
<script> <script>
import sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "WebOptions", name: "WebOptions",
props: ["options", "frontendName"], props: ["options", "frontendName"],
data() { data() {
return { return {
showEsTestAlert: false, showEsTestAlert: false,
esTestOk: false, esTestOk: false,
esTestMessage: "" esTestMessage: "",
} }
},
methods: {
update() {
console.log(this.options)
if (this.options.auth && this.options.tag_auth) {
// If both are set, remove tagAuth
this.options.tag_auth = "";
}
this.$emit("change", this.options);
}, },
} methods: {
update() {
if (!this.options.es_url.startsWith("https")) {
this.options.es_insecure_ssl = false;
}
this.$emit("change", this.options);
},
}
} }
</script> </script>

View File

@ -8,7 +8,6 @@ export default {
view: "View", view: "View",
delete: "Delete", delete: "Delete",
runNow: "Index now", runNow: "Index now",
runNowFull: "Full re-index",
create: "Create", create: "Create",
cancel: "Cancel", cancel: "Cancel",
test: "Test", test: "Test",
@ -53,23 +52,7 @@ export default {
searchBackendTitle: "search backend configuration", searchBackendTitle: "search backend configuration",
newBackendName: "New search backend name", newBackendName: "New search backend name",
frontendTab: "Frontend", selectJobs: "Select jobs",
backendTab: "Backend",
scripts: "User Scripts",
script: "User Script",
testScript: "Test/debug User Script",
newScriptName: "New script name",
scriptType: "Script type",
scriptCode: "Script code (Python)",
scriptOptions: "User scripts",
gitRepository: "Git repository URL",
extraArgs: "Extra command line arguments",
couldNotStartFrontend: "Could not start frontend",
couldNotStartFrontendBody: "Unable to start the frontend, check server logs for more details.",
selectJobs: "Available jobs",
selectJob: "Select a job",
webOptions: { webOptions: {
title: "Web options", title: "Web options",
lang: "UI Language", lang: "UI Language",
@ -81,7 +64,6 @@ export default {
auth0Domain: "Auth0 domain", auth0Domain: "Auth0 domain",
auth0ClientId: "Auth0 client ID", auth0ClientId: "Auth0 client ID",
auth0PublicKey: "Auth0 public key", auth0PublicKey: "Auth0 public key",
verbose: "Verbose logs"
}, },
backendOptions: { backendOptions: {
title: "Search backend options", title: "Search backend options",
@ -127,13 +109,12 @@ export default {
keepNLogs: "Keep last N log files. Set to -1 to keep all logs.", keepNLogs: "Keep last N log files. Set to -1 to keep all logs.",
deleteNow: "Delete now", deleteNow: "Delete now",
scheduleEnabled: "Enable scheduled re-scan", scheduleEnabled: "Enable scheduled re-scan",
noJobAvailable: "No jobs available for this search backend.", noJobAvailable: "No jobs available.",
notIndexed: "Has not been indexed yet",
noBackendError: "You must select a search backend to run this job", noBackendError: "You must select a search backend to run this job",
desktopNotifications: "Desktop notifications" desktopNotifications: "Desktop notifications"
}, },
frontendOptions: { frontendOptions: {
title: "Advanced options", title: "Frontend options",
noJobSelectedWarning: "You must select at least one job to start this frontend" noJobSelectedWarning: "You must select at least one job to start this frontend"
}, },
notifications: { notifications: {

View File

@ -6,18 +6,12 @@ import Tasks from "@/views/Tasks";
import Frontend from "@/views/Frontend"; import Frontend from "@/views/Frontend";
import Tail from "@/views/Tail"; import Tail from "@/views/Tail";
import SearchBackend from "@/views/SearchBackend.vue"; import SearchBackend from "@/views/SearchBackend.vue";
import UserScript from "@/views/UserScript.vue";
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
{ {
path: "/task", path: "/",
name: "Tasks",
component: Tasks
},
{
path: "/:tab?",
name: "Home", name: "Home",
component: Home component: Home
}, },
@ -26,6 +20,11 @@ const routes = [
name: "Job", name: "Job",
component: Job component: Job
}, },
{
path: "/task/",
name: "Tasks",
component: Tasks
},
{ {
path: "/frontend/:name", path: "/frontend/:name",
name: "Frontend", name: "Frontend",
@ -36,11 +35,6 @@ const routes = [
name: "SearchBackend", name: "SearchBackend",
component: SearchBackend component: SearchBackend
}, },
{
path: "/userScript/:name",
name: "UserScript",
component: UserScript
},
{ {
path: "/log/:taskId", path: "/log/:taskId",
name: "Tail", name: "Tail",

View File

@ -1,63 +1,70 @@
<template> <template>
<b-card> <b-card>
<b-card-title> <b-card-title>
{{ name }} {{ name }}
<small style="vertical-align: top"> <small style="vertical-align: top">
<b-badge v-if="!loading && frontend.running" variant="success">{{ $t("online") }}</b-badge> <b-badge v-if="!loading && frontend.running" variant="success">{{ $t("online") }}</b-badge>
<b-badge v-else-if="!loading" variant="secondary">{{ $t("offline") }}</b-badge> <b-badge v-else-if="!loading" variant="secondary">{{ $t("offline") }}</b-badge>
</small> </small>
</b-card-title> </b-card-title>
<!-- Action buttons--> <div class="mb-3" v-if="!loading">
<div class="mb-3" v-if="!loading"> <b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{
<b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{ $t("start")
$t("start") }}
}} </b-button>
</b-button> <b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{
<b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{ $t("stop")
$t("stop") }}
}} </b-button>
</b-button> <b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank">
<b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank"> {{ $t("go") }}
{{ $t("go") }} </b-button>
</b-button> <b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button>
<b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button> </div>
</div>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else>
<h4>{{ $t("backendOptions.title") }}</h4> <b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card> <b-card-body v-else>
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
<SearchBackendSelect :value="frontend.web_options.search_backend" <h4>{{ $t("frontendOptions.title") }}</h4>
@change="onBackendSelect($event)"></SearchBackendSelect> <b-card>
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
{{ $t("autoStart") }}
</b-form-checkbox>
<br> <label>{{ $t("extraQueryArgs") }}</label>
<JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup> <b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input>
</b-card>
<br/> <label>{{ $t("customUrl") }}</label>
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
<WebOptions :options="frontend.web_options" :frontend-name="$route.params.name" <br/>
@change="update()"></WebOptions>
<br/>
<h4>{{ $t("frontendOptions.title") }}</h4> <b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
<b-card>
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
{{ $t("autoStart") }}
</b-form-checkbox>
<label>{{ $t("extraQueryArgs") }}</label> <JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup>
<b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input> </b-card>
<label>{{ $t("customUrl") }}</label> <br/>
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
</b-card> <h4>{{ $t("webOptions.title") }}</h4>
</b-card-body> <b-card>
</b-card> <WebOptions :options="frontend.web_options" :frontend-name="$route.params.name"
@change="update()"></WebOptions>
</b-card>
<br>
<h4>{{ $t("backendOptions.title") }}</h4>
<b-card>
<SearchBackendSelect :value="frontend.web_options.search_backend"
@change="onBackendSelect($event)"></SearchBackendSelect>
</b-card>
</b-card-body>
</b-card>
</template> </template>
<script> <script>
@ -68,78 +75,70 @@ import WebOptions from "@/components/WebOptions";
import SearchBackendSelect from "@/components/SearchBackendSelect.vue"; import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
export default { export default {
name: 'Frontend', name: 'Frontend',
components: {SearchBackendSelect, JobCheckboxGroup, WebOptions}, components: {SearchBackendSelect, JobCheckboxGroup, WebOptions},
data() { data() {
return { return {
loading: true, loading: true,
frontend: null, frontend: null,
} }
},
computed: {
valid() {
return !this.loading && this.frontend.jobs.length > 0;
}, },
frontendUrl() { computed: {
if (this.frontend.custom_url) { valid() {
return this.frontend.custom_url + this.args; return !this.loading && this.frontend.jobs.length > 0;
} },
frontendUrl() {
if (this.frontend.custom_url) {
return this.frontend.custom_url + this.args;
}
if (this.frontend.web_options.bind.startsWith("0.0.0.0")) { if (this.frontend.web_options.bind.startsWith("0.0.0.0")) {
return window.location.protocol + "//" + window.location.hostname + ":" + this.port + this.args; return window.location.protocol + "//" + window.location.hostname + ":" + this.port + this.args;
} }
return window.location.protocol + "//" + this.frontend.web_options.bind + this.args; return window.location.protocol + "//" + this.frontend.web_options.bind + this.args;
},
name() {
return this.$route.params.name;
},
port() {
return this.frontend.web_options.bind.split(":")[1]
},
args() {
const args = this.frontend.extra_query_args;
if (args !== "") {
return "#" + (args.startsWith("?") ? (args) : ("?" + args));
}
return "";
}
}, },
name() { mounted() {
return this.$route.params.name; Sist2AdminApi.getFrontend(this.name).then(resp => {
}, this.frontend = resp.data;
port() { this.loading = false;
return this.frontend.web_options.bind.split(":")[1]
},
args() {
const args = this.frontend.extra_query_args;
if (args !== "") {
return "#" + (args.startsWith("?") ? (args) : ("?" + args));
}
return "";
}
},
mounted() {
Sist2AdminApi.getFrontend(this.name).then(resp => {
this.frontend = resp.data;
this.loading = false;
});
},
methods: {
start() {
Sist2AdminApi.startFrontend(this.name).then(() => {
this.frontend.running = true;
}).catch(() => {
this.$bvToast.toast(this.$t("couldNotStartFrontendBody"), {
title: this.$t("couldNotStartFrontend"),
variant: "danger",
toaster: "b-toaster-bottom-right"
}); });
});
}, },
stop() { methods: {
this.frontend.running = false; start() {
Sist2AdminApi.stopFrontend(this.name) this.frontend.running = true;
}, Sist2AdminApi.startFrontend(this.name)
deleteFrontend() { },
Sist2AdminApi.deleteFrontend(this.name).then(() => { stop() {
this.$router.push("/"); this.frontend.running = false;
}); Sist2AdminApi.stopFrontend(this.name)
}, },
update() { deleteFrontend() {
Sist2AdminApi.updateFrontend(this.name, this.frontend); Sist2AdminApi.deleteFrontend(this.name).then(() => {
}, this.$router.push("/");
onBackendSelect(backend) { });
this.frontend.web_options.search_backend = backend; },
this.frontend.jobs = []; update() {
this.update(); Sist2AdminApi.updateFrontend(this.name, this.frontend);
},
onBackendSelect(backend) {
this.frontend.web_options.search_backend = backend;
this.update();
}
} }
}
} }
</script> </script>

View File

@ -1,122 +1,88 @@
<template> <template>
<div> <div>
<b-tabs content-class="mt-3" v-model="tab" @input="onTabChange($event)"> <b-card>
<b-tab :title="$t('backendTab')"> <b-card-title>{{ $t("jobs") }}</b-card-title>
<b-row>
<b-col>
<b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input>
<b-popover
:show.sync="showHelp"
target="new-job"
placement="top"
triggers="manual"
variant="primary"
:content="$t('newJobHelp')"
></b-popover>
</b-col>
<b-col>
<b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<b-card> <hr/>
<b-card-title>{{ $t("searchBackends") }}</b-card-title>
<b-row> <b-progress v-if="jobsLoading" striped animated value="100"></b-progress>
<b-col> <b-list-group v-else>
<b-input v-model="newBackendName" :placeholder="$t('newBackendName')"></b-input> <JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem>
</b-col> </b-list-group>
<b-col> </b-card>
<b-button variant="primary" @click="createBackend()"
:disabled="!backendNameValid(newBackendName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/> <br/>
<b-progress v-if="backendsLoading" striped animated value="100"></b-progress> <b-card>
<b-list-group v-else>
<SearchBackendListItem v-for="backend in backends"
:key="backend.name" :backend="backend"></SearchBackendListItem>
</b-list-group>
</b-card>
<br/> <b-card-title>{{ $t("frontends") }}</b-card-title>
<b-card> <b-row>
<b-card-title>{{ $t("jobs") }}</b-card-title> <b-col>
<b-row> <b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input>
<b-col> </b-col>
<b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input> <b-col>
<b-popover <b-button variant="primary" @click="createFrontend()"
:show.sync="showHelp" :disabled="!frontendNameValid(newFrontendName)">
target="new-job" {{ $t("create") }}
placement="top" </b-button>
triggers="manual" </b-col>
variant="primary" </b-row>
:content="$t('newJobHelp')"
></b-popover>
</b-col>
<b-col>
<b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/> <hr/>
<b-progress v-if="jobsLoading" striped animated value="100"></b-progress> <b-progress v-if="frontendsLoading" striped animated value="100"></b-progress>
<b-list-group v-else> <b-list-group v-else>
<JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem> <FrontendListItem v-for="frontend in frontends"
</b-list-group> :key="frontend.name" :frontend="frontend"></FrontendListItem>
</b-card> </b-list-group>
</b-tab>
<b-tab :title="$t('scripts')">
<b-progress v-if="scriptsLoading" striped animated value="100"></b-progress> </b-card>
<b-card v-else>
<b-card-title>{{ $t("scripts") }}</b-card-title>
<label>Select template</label> <br/>
<b-form-radio-group stacked :options="scriptTemplates" v-model="scriptTemplate"></b-form-radio-group>
<br>
<b-row> <b-card>
<b-col> <b-card-title>{{ $t("searchBackends") }}</b-card-title>
<b-form-input v-model="newScriptName" :disabled="!scriptTemplate" :placeholder="$t('newScriptName')"></b-form-input>
</b-col>
<b-col>
<b-button variant="primary" @click="createScript()"
:disabled="!scriptNameValid(newScriptName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/> <b-row>
<b-col>
<b-input v-model="newBackendName" :placeholder="$t('newBackendName')"></b-input>
</b-col>
<b-col>
<b-button variant="primary" @click="createBackend()"
:disabled="!backendNameValid(newBackendName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<b-list-group> <hr/>
<UserScriptListItem v-for="script in scripts"
:key="script.name" :script="script"></UserScriptListItem>
</b-list-group>
</b-card> <b-progress v-if="backendsLoading" striped animated value="100"></b-progress>
</b-tab> <b-list-group v-else>
<b-tab :title="$t('frontendTab')"> <SearchBackendListItem v-for="backend in backends"
<b-card> :key="backend.name" :backend="backend"></SearchBackendListItem>
</b-list-group>
<b-card-title>{{ $t("frontends") }}</b-card-title> </b-card>
<b-row>
<b-col>
<b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input>
</b-col>
<b-col>
<b-button variant="primary" @click="createFrontend()"
:disabled="!frontendNameValid(newFrontendName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/>
<b-progress v-if="frontendsLoading" striped animated value="100"></b-progress>
<b-list-group v-else>
<FrontendListItem v-for="frontend in frontends"
:key="frontend.name" :frontend="frontend"></FrontendListItem>
</b-list-group>
</b-card>
</b-tab>
</b-tabs>
</div> </div>
</template> </template>
@ -126,11 +92,10 @@ import {formatBindAddress} from "@/util";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import FrontendListItem from "@/components/FrontendListItem"; import FrontendListItem from "@/components/FrontendListItem";
import SearchBackendListItem from "@/components/SearchBackendListItem.vue"; import SearchBackendListItem from "@/components/SearchBackendListItem.vue";
import UserScriptListItem from "@/components/UserScriptListItem.vue";
export default { export default {
name: "Jobs", name: "Jobs",
components: {UserScriptListItem, SearchBackendListItem, JobListItem, FrontendListItem}, components: {SearchBackendListItem, JobListItem, FrontendListItem},
data() { data() {
return { return {
jobsLoading: true, jobsLoading: true,
@ -146,24 +111,11 @@ export default {
backendsLoading: true, backendsLoading: true,
newBackendName: "", newBackendName: "",
scripts: [], showHelp: false
scriptTemplates: [],
newScriptName: "",
scriptTemplate: null,
scriptsLoading: true,
showHelp: false,
tab: 0
} }
}, },
mounted() { mounted() {
this.loading = true; this.loading = true;
if (this.$route.params.tab) {
console.log("mounted " + this.$route.params.tab)
window.setTimeout(() => {
this.tab = Math.round(Number(this.$route.params.tab));
}, 1)
}
this.reload(); this.reload();
}, },
methods: { methods: {
@ -188,20 +140,11 @@ export default {
return /^[a-zA-Z0-9-_,.; ]+$/.test(name); return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
}, },
scriptNameValid(name) {
if (this.scripts.some(script => script.name === name)) {
return false;
}
if (name.length > 16) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
reload() { reload() {
Sist2AdminApi.getJobs().then(resp => { Sist2AdminApi.getJobs().then(resp => {
this.jobs = resp.data; this.jobs = resp.data;
this.jobsLoading = false; this.jobsLoading = false;
this.showHelp = this.jobs.length === 0; this.showHelp = this.jobs.length === 0;
}); });
Sist2AdminApi.getFrontends().then(resp => { Sist2AdminApi.getFrontends().then(resp => {
@ -212,11 +155,6 @@ export default {
this.backends = resp.data; this.backends = resp.data;
this.backendsLoading = false; this.backendsLoading = false;
}) })
Sist2AdminApi.getUserScripts().then(resp => {
this.scripts = resp.data;
this.scriptTemplates = this.$store.state.sist2AdminInfo.user_script_templates;
this.scriptsLoading = false;
})
}, },
createJob() { createJob() {
Sist2AdminApi.createJob(this.newJobName).then(this.reload); Sist2AdminApi.createJob(this.newJobName).then(this.reload);
@ -226,14 +164,6 @@ export default {
}, },
createBackend() { createBackend() {
Sist2AdminApi.createBackend(this.newBackendName).then(this.reload); Sist2AdminApi.createBackend(this.newBackendName).then(this.reload);
},
createScript() {
Sist2AdminApi.createUserScript(this.newScriptName, this.scriptTemplate).then(this.reload)
},
onTabChange(tab) {
if (this.$route.params.tab != tab) {
this.$router.push({params: {tab: tab}})
}
} }
} }
} }

View File

@ -6,19 +6,7 @@
</b-card-title> </b-card-title>
<div class="mb-3"> <div class="mb-3">
<b-button class="mr-1" variant="primary" @click="runJob()" :disabled="!valid">{{ $t("runNow") }}</b-button>
<b-dropdown
split
split-variant="primary"
variant="primary"
:text="$t('runNow')"
class="mr-1"
:disabled="!valid"
@click="runJob()"
>
<b-dropdown-item href="#" @click="runJob(true)">{{ $t("runNowFull") }}</b-dropdown-item>
</b-dropdown>
<b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button> <b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button>
</div> </div>
@ -36,26 +24,19 @@
<br/> <br/>
<h4>{{ $t("scanOptions.title") }}</h4>
<b-card>
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
</b-card>
<br/>
<h4>{{ $t("backendOptions.title") }}</h4> <h4>{{ $t("backendOptions.title") }}</h4>
<b-card> <b-card>
<b-alert v-if="!valid" variant="warning" show>{{ $t("jobOptions.noBackendError") }}</b-alert> <b-alert v-if="!valid" variant="warning" show>{{ $t("jobOptions.noBackendError") }}</b-alert>
<SearchBackendSelect :value="job.index_options.search_backend" <SearchBackendSelect :value="job.index_options.search_backend"
@change="onBackendSelect($event)"></SearchBackendSelect> @change="onBackendSelect($event)"></SearchBackendSelect>
</b-card> </b-card>
<br/>
<h4>{{ $t("scriptOptions") }}</h4>
<b-card>
<UserScriptPicker :selected-scripts="job.user_scripts"
@change="onScriptChange($event)"></UserScriptPicker>
</b-card>
<br/>
<h4>{{ $t("scanOptions.title") }}</h4>
<b-card>
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
</b-card>
</b-card-body> </b-card-body>
@ -67,12 +48,10 @@ import ScanOptions from "@/components/ScanOptions";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import JobOptions from "@/components/JobOptions"; import JobOptions from "@/components/JobOptions";
import SearchBackendSelect from "@/components/SearchBackendSelect.vue"; import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
import UserScriptPicker from "@/components/UserScriptPicker.vue";
export default { export default {
name: "Job", name: "Job",
components: { components: {
UserScriptPicker,
SearchBackendSelect, SearchBackendSelect,
ScanOptions, ScanOptions,
JobOptions JobOptions
@ -81,7 +60,6 @@ export default {
return { return {
loading: true, loading: true,
job: null, job: null,
console: console
} }
}, },
methods: { methods: {
@ -91,8 +69,8 @@ export default {
update() { update() {
Sist2AdminApi.updateJob(this.getName(), this.job); Sist2AdminApi.updateJob(this.getName(), this.job);
}, },
runJob(full = false) { runJob() {
Sist2AdminApi.runJob(this.getName(), full).then(() => { Sist2AdminApi.runJob(this.getName()).then(() => {
this.$bvToast.toast(this.$t("runJobConfirmation"), { this.$bvToast.toast(this.$t("runJobConfirmation"), {
title: this.$t("runJobConfirmationTitle"), title: this.$t("runJobConfirmationTitle"),
variant: "success", variant: "success",
@ -117,10 +95,6 @@ export default {
onBackendSelect(backend) { onBackendSelect(backend) {
this.job.index_options.search_backend = backend; this.job.index_options.search_backend = backend;
this.update(); this.update();
},
onScriptChange(scripts) {
this.job.user_scripts = scripts;
this.update();
} }
}, },
mounted() { mounted() {

View File

@ -44,6 +44,9 @@
<label>{{ $t("backendOptions.batchSize") }}</label> <label>{{ $t("backendOptions.batchSize") }}</label>
<b-form-input v-model="backend.batch_size" type="number" min="1" @change="update()"></b-form-input> <b-form-input v-model="backend.batch_size" type="number" min="1" @change="update()"></b-form-input>
<label>{{ $t("backendOptions.script") }}</label>
<b-form-textarea v-model="backend.script" rows="6" @change="update()"></b-form-textarea>
</template> </template>
<template v-else> <template v-else>
<label>{{ $t("backendOptions.searchIndex") }}</label> <label>{{ $t("backendOptions.searchIndex") }}</label>

View File

@ -92,9 +92,6 @@ export default {
if ("stderr" in message) { if ("stderr" in message) {
message.level = "ERROR"; message.level = "ERROR";
message.message = message["stderr"]; message.message = message["stderr"];
} else if ("stdout" in message) {
message.level = "INFO";
message.message = message["stdout"];
} else { } else {
message.level = "ADMIN"; message.level = "ADMIN";
message.message = message["sist2-admin"]; message.message = message["sist2-admin"];
@ -170,6 +167,6 @@ span.ADMIN {
margin: 3px; margin: 3px;
white-space: pre; white-space: pre;
color: #000; color: #000;
overflow-y: hidden; overflow: hidden;
} }
</style> </style>

View File

@ -81,7 +81,7 @@ function humanDuration(sec_num) {
return `${seconds}s`; return `${seconds}s`;
} }
return "<1s"; return "<0s";
} }
export default { export default {
@ -134,7 +134,7 @@ export default {
duration: this.taskDuration(row), duration: this.taskDuration(row),
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"), time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
logs: null, logs: null,
status: row.return_code === 0 ? "ok" : "failed", status: [0,1].includes(row.return_code) ? "ok" : "failed",
_row: row _row: row
})); }));
}); });

View File

@ -1,117 +0,0 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card v-else>
<b-card-title>
{{ $route.params.name }}
{{ $t("script") }}
</b-card-title>
<div class="mb-3">
<b-button variant="danger" @click="deleteScript()">{{ $t("delete") }}</b-button>
</div>
<b-card>
<h5>{{ $t("testScript") }}</h5>
<b-row>
<b-col cols="11">
<JobSelect @change="onJobSelect($event)"></JobSelect>
</b-col>
<b-col cols="1">
<b-button :disabled="!selectedTestJob" variant="primary" @click="testScript()">{{ $t("test") }}
</b-button>
</b-col>
</b-row>
</b-card>
<br/>
<label>{{ $t("scriptType") }}</label>
<b-form-select :options="['git', 'simple']" v-model="script.type" @change="update()"></b-form-select>
<template v-if="script.type === 'git'">
<label>{{ $t("gitRepository") }}</label>
<b-form-input v-model="script.git_repository" placeholder="https://github.com/example/example.git"
@change="update()"></b-form-input>
<label>{{ $t("extraArgs") }}</label>
<b-form-input v-model="script.extra_args" @change="update()" class="text-monospace"></b-form-input>
</template>
<template v-if="script.type === 'simple'">
<label>{{ $t("scriptCode") }}</label>
<p>Find sist2-python documentation <a href="https://sist2-python.readthedocs.io/" target="_blank">here</a></p>
<b-textarea rows="15" class="text-monospace" v-model="script.script" @change="update()" spellcheck="false"></b-textarea>
</template>
<template v-if="script.type === 'local'">
<!-- TODO-->
</template>
</b-card>
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
import JobOptions from "@/components/JobOptions.vue";
import JobCheckboxGroup from "@/components/JobCheckboxGroup.vue";
import JobSelect from "@/components/JobSelect.vue";
export default {
name: "UserScript",
components: {JobSelect, JobCheckboxGroup, JobOptions},
data() {
return {
loading: true,
script: null,
selectedTestJob: null
}
},
methods: {
update() {
Sist2AdminApi.updateUserScript(this.name, this.script);
},
onJobSelect(job) {
this.selectedTestJob = job;
},
deleteScript() {
Sist2AdminApi.deleteUserScript(this.name)
.then(() => {
this.$router.push("/");
})
.catch(err => {
this.$bvToast.toast("Cannot delete user script " +
"because it is referenced by a job", {
title: "Error",
variant: "danger",
toaster: "b-toaster-bottom-right"
});
})
},
testScript() {
Sist2AdminApi.testUserScript(this.name, this.selectedTestJob)
.then(() => {
this.$bvToast.toast(this.$t("runJobConfirmation"), {
title: this.$t("runJobConfirmationTitle"),
variant: "success",
toaster: "b-toaster-bottom-right"
});
})
}
},
mounted() {
Sist2AdminApi.getUserScript(this.name).then(resp => {
this.script = resp.data;
this.loading = false;
});
},
computed: {
name() {
return this.$route.params.name;
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,4 @@ fastapi
git+https://github.com/simon987/hexlib.git git+https://github.com/simon987/hexlib.git
uvicorn uvicorn
websockets websockets
pycron pycron
GitPython
git+https://github.com/sist2app/sist2-python.git@2.1

View File

@ -2,7 +2,6 @@ import asyncio
import os import os
import signal import signal
from datetime import datetime from datetime import datetime
from time import sleep
from urllib.parse import urlparse from urllib.parse import urlparse
import requests import requests
@ -19,14 +18,12 @@ from websockets.exceptions import ConnectionClosed
import cron import cron
from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY
from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus, Sist2UserScriptTask from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus
from notifications import Subscribe, Notifications from notifications import Subscribe, Notifications
from sist2 import Sist2, Sist2SearchBackend from sist2 import Sist2, Sist2SearchBackend
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION, migrate_v3_to_v4, \ from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION, migrate_v3_to_v4, \
get_log_files_to_remove, delete_log_file, create_default_search_backends get_log_files_to_remove, delete_log_file, create_default_search_backends
from web import Sist2Frontend from web import Sist2Frontend
from script import UserScript, SCRIPT_TEMPLATES
from util import tail_sync, pid_is_running
sist2 = Sist2(SIST2_BINARY, DATA_FOLDER) sist2 = Sist2(SIST2_BINARY, DATA_FOLDER)
db = PersistentState(dbfile=os.path.join(DATA_FOLDER, "state.db")) db = PersistentState(dbfile=os.path.join(DATA_FOLDER, "state.db"))
@ -55,8 +52,7 @@ async def home():
async def api(): async def api():
return { return {
"tesseract_langs": TESSERACT_LANGS, "tesseract_langs": TESSERACT_LANGS,
"logs_folder": LOG_FOLDER, "logs_folder": LOG_FOLDER
"user_script_templates": list(SCRIPT_TEMPLATES.keys())
} }
@ -78,7 +74,7 @@ async def get_frontend(name: str):
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@app.get("/api/job") @app.get("/api/job/")
async def get_jobs(): async def get_jobs():
return list(db["jobs"]) return list(db["jobs"])
@ -117,10 +113,12 @@ async def update_job(name: str, new_job: Sist2Job):
async def update_frontend(name: str, frontend: Sist2Frontend): async def update_frontend(name: str, frontend: Sist2Frontend):
db["frontends"][name] = frontend db["frontends"][name] = frontend
# TODO: Check etag
return "ok" return "ok"
@app.get("/api/task") @app.get("/api/task/")
async def get_tasks(): async def get_tasks():
return list(map(lambda t: t.json(), task_queue.tasks())) return list(map(lambda t: t.json(), task_queue.tasks()))
@ -152,54 +150,23 @@ def _run_job(job: Sist2Job):
db["jobs"][job.name] = job db["jobs"][job.name] = job
scan_task = Sist2ScanTask(job, f"Scan [{job.name}]") scan_task = Sist2ScanTask(job, f"Scan [{job.name}]")
index_task = Sist2IndexTask(job, f"Index [{job.name}]", depends_on=scan_task)
index_depends_on = scan_task
script_tasks = []
for script_name in job.user_scripts:
script = db["user_scripts"][script_name]
task = Sist2UserScriptTask(script, job, f"Script <{script_name}> [{job.name}]", depends_on=scan_task)
script_tasks.append(task)
index_depends_on = task
index_task = Sist2IndexTask(job, f"Index [{job.name}]", depends_on=index_depends_on)
task_queue.submit(scan_task) task_queue.submit(scan_task)
for task in script_tasks:
task_queue.submit(task)
task_queue.submit(index_task) task_queue.submit(index_task)
@app.get("/api/job/{name:str}/run") @app.get("/api/job/{name:str}/run")
async def run_job(name: str, full: bool = False): async def run_job(name: str):
job: Sist2Job = db["jobs"][name] job = db["jobs"][name]
if not job: if not job:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
if full:
job.do_full_scan = True
_run_job(job) _run_job(job)
return "ok" return "ok"
@app.get("/api/user_script/{name:str}/run")
def run_user_script(name: str, job: str):
script = db["user_scripts"][name]
if not script:
raise HTTPException(status_code=404)
job = db["jobs"][job]
if not job:
raise HTTPException(status_code=404)
script_task = Sist2UserScriptTask(script, job, f"Script <{name}> [{job.name}]")
task_queue.submit(script_task)
return "ok"
@app.get("/api/job/{name:str}/logs_to_delete") @app.get("/api/job/{name:str}/logs_to_delete")
async def task_history(n: int, name: str): async def task_history(n: int, name: str):
return get_log_files_to_remove(db, name, n) return get_log_files_to_remove(db, name, n)
@ -227,10 +194,7 @@ async def delete_job(name: str):
@app.delete("/api/frontend/{name:str}") @app.delete("/api/frontend/{name:str}")
async def delete_frontend(name: str): async def delete_frontend(name: str):
if name in RUNNING_FRONTENDS: if name in RUNNING_FRONTENDS:
try: os.kill(RUNNING_FRONTENDS[name], signal.SIGTERM)
os.kill(RUNNING_FRONTENDS[name], signal.SIGTERM)
except ProcessLookupError:
pass
del RUNNING_FRONTENDS[name] del RUNNING_FRONTENDS[name]
frontend = db["frontends"][name] frontend = db["frontends"][name]
@ -275,7 +239,7 @@ def check_es_version(es_url: str, insecure: bool):
es_url = f"{url.scheme}://{url.hostname}:{url.port}" es_url = f"{url.scheme}://{url.hostname}:{url.port}"
else: else:
auth = None auth = None
r = requests.get(es_url, verify=not insecure, auth=auth) r = requests.get(es_url, verify=insecure, auth=auth)
except SSLError: except SSLError:
return { return {
"ok": False, "ok": False,
@ -326,18 +290,7 @@ def start_frontend_(frontend: Sist2Frontend):
logger.debug(f"Fetched search backend options for {backend_name}") logger.debug(f"Fetched search backend options for {backend_name}")
pid = sist2.web(frontend.web_options, search_backend, frontend.name) pid = sist2.web(frontend.web_options, search_backend, frontend.name)
sleep(0.2)
if not pid_is_running(pid):
frontend_log = frontend.get_log_path(LOG_FOLDER)
logger.error(f"Frontend exited too quickly, check {frontend_log} for more details:")
for line in tail_sync(frontend.get_log_path(LOG_FOLDER), 3):
logger.error(line.strip())
return False
RUNNING_FRONTENDS[frontend.name] = pid RUNNING_FRONTENDS[frontend.name] = pid
return True
@app.post("/api/frontend/{name:str}/start") @app.post("/api/frontend/{name:str}/start")
@ -346,12 +299,7 @@ async def start_frontend(name: str):
if not frontend: if not frontend:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
ok = start_frontend_(frontend) start_frontend_(frontend)
if not ok:
raise HTTPException(status_code=500)
return "ok"
@app.post("/api/frontend/{name:str}/stop") @app.post("/api/frontend/{name:str}/stop")
@ -361,7 +309,7 @@ async def stop_frontend(name: str):
del RUNNING_FRONTENDS[name] del RUNNING_FRONTENDS[name]
@app.get("/api/frontend") @app.get("/api/frontend/")
async def get_frontends(): async def get_frontends():
res = [] res = []
for frontend in db["frontends"]: for frontend in db["frontends"]:
@ -371,7 +319,7 @@ async def get_frontends():
return res return res
@app.get("/api/search_backend") @app.get("/api/search_backend/")
async def get_search_backends(): async def get_search_backends():
return list(db["search_backends"]) return list(db["search_backends"])
@ -427,59 +375,6 @@ def create_search_backend(name: str):
return backend return backend
@app.delete("/api/user_script/{name:str}")
def delete_user_script(name: str):
if db["user_scripts"][name] is None:
return HTTPException(status_code=404)
if any(name in job.user_scripts for job in db["jobs"]):
raise HTTPException(status_code=400, detail="in use (job)")
script: UserScript = db["user_scripts"][name]
script.delete_dir()
del db["user_scripts"][name]
return "ok"
@app.post("/api/user_script/{name:str}")
def create_user_script(name: str, template: str):
if db["user_scripts"][name] is not None:
return HTTPException(status_code=400, detail="already exists")
script = SCRIPT_TEMPLATES[template](name)
db["user_scripts"][name] = script
return script
@app.get("/api/user_script")
async def get_user_scripts():
return list(db["user_scripts"])
@app.get("/api/user_script/{name:str}")
async def get_user_script(name: str):
backend = db["user_scripts"][name]
if not backend:
raise HTTPException(status_code=404)
return backend
@app.put("/api/user_script/{name:str}")
async def update_user_script(name: str, script: UserScript):
previous_version: UserScript = db["user_scripts"][name]
if previous_version and previous_version.git_repository != script.git_repository:
script.force_clone = True
db["user_scripts"][name] = script
return "ok"
def tail(filepath: str, n: int): def tail(filepath: str, n: int):
with open(filepath) as file: with open(filepath) as file:
@ -584,8 +479,7 @@ if __name__ == '__main__':
migrate_v3_to_v4(db) migrate_v3_to_v4(db)
if db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION: if db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION:
raise Exception(f"Incompatible database {db.dbfile}. " raise Exception(f"Incompatible database version for {db.dbfile}")
f"Automatic migration is not available, please delete the database file to continue.")
start_frontends() start_frontends()
cron.initialize(db, _run_job) cron.initialize(db, _run_job)

View File

@ -9,11 +9,9 @@ MAX_LOG_SIZE = 1 * 1024 * 1024
SIST2_BINARY = os.environ.get("SIST2_BINARY", "/root/sist2") SIST2_BINARY = os.environ.get("SIST2_BINARY", "/root/sist2")
DATA_FOLDER = os.environ.get("DATA_FOLDER", "/sist2-admin/") DATA_FOLDER = os.environ.get("DATA_FOLDER", "/sist2-admin/")
LOG_FOLDER = os.path.join(DATA_FOLDER, "logs") LOG_FOLDER = os.path.join(DATA_FOLDER, "logs")
SCRIPT_FOLDER = os.path.join(DATA_FOLDER, "scripts")
WEBSERVER_PORT = 8080 WEBSERVER_PORT = 8080
os.makedirs(LOG_FOLDER, exist_ok=True) os.makedirs(LOG_FOLDER, exist_ok=True)
os.makedirs(SCRIPT_FOLDER, exist_ok=True)
os.makedirs(DATA_FOLDER, exist_ok=True) os.makedirs(DATA_FOLDER, exist_ok=True)
logger = logging.Logger("sist2-admin") logger = logging.Logger("sist2-admin")

View File

@ -1,18 +1,13 @@
import json import json
import logging import logging
import os.path import os.path
import shlex
import signal import signal
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from io import TextIOWrapper
from logging import FileHandler from logging import FileHandler
from subprocess import Popen
import subprocess
from threading import Lock, Thread from threading import Lock, Thread
from time import sleep from time import sleep
from typing import List
from uuid import uuid4, UUID from uuid import uuid4, UUID
from hexlib.db import PersistentState from hexlib.db import PersistentState
@ -23,7 +18,6 @@ from notifications import Notifications
from sist2 import ScanOptions, IndexOptions, Sist2 from sist2 import ScanOptions, IndexOptions, Sist2
from state import RUNNING_FRONTENDS, get_log_files_to_remove, delete_log_file from state import RUNNING_FRONTENDS, get_log_files_to_remove, delete_log_file
from web import Sist2Frontend from web import Sist2Frontend
from script import UserScript
class JobStatus(Enum): class JobStatus(Enum):
@ -38,8 +32,6 @@ class Sist2Job(BaseModel):
scan_options: ScanOptions scan_options: ScanOptions
index_options: IndexOptions index_options: IndexOptions
user_scripts: List[str] = []
cron_expression: str cron_expression: str
schedule_enabled: bool = False schedule_enabled: bool = False
@ -120,10 +112,6 @@ class Sist2Task:
logger.info(f"Started task {self.display_name}") logger.info(f"Started task {self.display_name}")
def set_pid(self, pid):
self.pid = pid
class Sist2ScanTask(Sist2Task): class Sist2ScanTask(Sist2Task):
@ -137,10 +125,13 @@ class Sist2ScanTask(Sist2Task):
else: else:
self.job.scan_options.output = None self.job.scan_options.output = None
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=self.set_pid) def set_pid(pid):
self.pid = pid
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid)
self.ended = datetime.utcnow() self.ended = datetime.utcnow()
is_ok = (return_code in (0, 1)) if "debug" in sist2.bin_path else (return_code == 0) is_ok = return_code in (0, 1)
if not is_ok: if not is_ok:
self._logger.error(json.dumps({"sist2-admin": f"Process returned non-zero exit code ({return_code})"})) self._logger.error(json.dumps({"sist2-admin": f"Process returned non-zero exit code ({return_code})"}))
@ -166,9 +157,6 @@ class Sist2ScanTask(Sist2Task):
self.job.previous_index_path = self.job.index_path self.job.previous_index_path = self.job.index_path
db["jobs"][self.job.name] = self.job db["jobs"][self.job.name] = self.job
if is_ok:
return 0
return return_code return return_code
@ -189,12 +177,12 @@ class Sist2IndexTask(Sist2Task):
logger.debug(f"Fetched search backend options for {self.job.index_options.search_backend}") logger.debug(f"Fetched search backend options for {self.job.index_options.search_backend}")
return_code = sist2.index(self.job.index_options, search_backend, logs_cb=self.log_callback, set_pid_cb=self.set_pid) return_code = sist2.index(self.job.index_options, search_backend, logs_cb=self.log_callback)
self.ended = datetime.utcnow() self.ended = datetime.utcnow()
duration = self.ended - self.started duration = self.ended - self.started
ok = return_code in (0, 1) ok = return_code == 0
if ok: if ok:
self.restart_running_frontends(db, sist2) self.restart_running_frontends(db, sist2)
@ -204,7 +192,7 @@ class Sist2IndexTask(Sist2Task):
self.job.previous_index_path = self.job.index_path self.job.previous_index_path = self.job.index_path
db["jobs"][self.job.name] = self.job db["jobs"][self.job.name] = self.job
self._logger.info(json.dumps({"sist2-admin": f"Sist2Scan task finished {return_code=}, {duration=}, {ok=}"})) self._logger.info(json.dumps({"sist2-admin": f"Sist2Scan task finished {return_code=}, {duration=}"}))
logger.info(f"Completed {self.display_name} ({return_code=})") logger.info(f"Completed {self.display_name} ({return_code=})")
@ -243,65 +231,6 @@ class Sist2IndexTask(Sist2Task):
self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"})) self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"}))
class Sist2UserScriptTask(Sist2Task):
def __init__(self, user_script: UserScript, job: Sist2Job, display_name: str, depends_on: Sist2Task = None):
super().__init__(job, display_name, depends_on=depends_on.id if depends_on else None)
self.user_script = user_script
def run(self, sist2: Sist2, db: PersistentState):
super().run(sist2, db)
try:
self.user_script.setup(self.log_callback, self.set_pid)
except Exception as e:
logger.error(f"Setup for {self.user_script.name} failed: ")
logger.exception(e)
self.log_callback({"sist2-admin": f"Setup for {self.user_script.name} failed: {e}"})
return -1
executable = self.user_script.get_executable()
index_path = os.path.join(DATA_FOLDER, self.job.index_path)
extra_args = self.user_script.extra_args
args = [
executable,
index_path,
*shlex.split(extra_args)
]
self.log_callback({"sist2-admin": f"Starting user script with {executable=}, {index_path=}, {extra_args=}"})
proc = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.user_script.script_dir())
self.set_pid(proc.pid)
t_stderr = Thread(target=self._consume_logs, args=(self.log_callback, proc, "stderr", False))
t_stderr.start()
self._consume_logs(self.log_callback, proc, "stdout", True)
self.ended = datetime.utcnow()
return 0
@staticmethod
def _consume_logs(logs_cb, proc, stream, wait):
pipe_wrapper = TextIOWrapper(getattr(proc, stream), encoding="utf8", errors="ignore")
try:
for line in pipe_wrapper:
if line.strip() == "":
continue
if line.startswith("$PROGRESS"):
progress = json.loads(line[len("$PROGRESS "):])
logs_cb({"progress": progress})
continue
logs_cb({stream: line})
finally:
if wait:
proc.wait()
pipe_wrapper.close()
class TaskQueue: class TaskQueue:
def __init__(self, sist2: Sist2, db: PersistentState, notifications: Notifications): def __init__(self, sist2: Sist2, db: PersistentState, notifications: Notifications):
self._lock = Lock() self._lock = Lock()
@ -320,7 +249,7 @@ class TaskQueue:
def _tasks_failed(self): def _tasks_failed(self):
done = set() done = set()
for row in self._db["task_done"].sql("WHERE return_code != 0"): for row in self._db["task_done"].sql("WHERE return_code NOT IN (0,1)"):
done.add(uuid.UUID(row["id"])) done.add(uuid.UUID(row["id"]))
return done return done

View File

@ -1,130 +0,0 @@
import os
import shutil
import stat
import subprocess
from enum import Enum
from git import Repo
from pydantic import BaseModel
from config import SCRIPT_FOLDER
class ScriptType(Enum):
LOCAL = "local"
SIMPLE = "simple"
GIT = "git"
def set_executable(file):
os.chmod(file, os.stat(file).st_mode | stat.S_IEXEC)
def _initialize_git_repository(url, path, log_cb, force_clone, set_pid_cb):
log_cb({"sist2-admin": f"Cloning {url}"})
if force_clone or not os.path.exists(os.path.join(path, ".git")):
if force_clone:
shutil.rmtree(path, ignore_errors=True)
Repo.clone_from(url, path)
else:
repo = Repo(path)
repo.remote("origin").pull()
setup_script = os.path.join(path, "setup.sh")
if setup_script:
log_cb({"sist2-admin": f"Executing setup script {setup_script}"})
set_executable(setup_script)
proc = subprocess.Popen([setup_script], cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
set_pid_cb(proc.pid)
proc.wait()
stdout = proc.stdout.read()
for line in stdout.split(b"\n"):
if line:
log_cb({"stdout": line.decode()})
log_cb({"stdout": f"Executed setup script {setup_script}, return code = {proc.returncode}"})
if proc.returncode != 0:
raise Exception("Error when running setup script!")
log_cb({"sist2-admin": f"Initialized git repository in {path}"})
class UserScript(BaseModel):
name: str
type: ScriptType
git_repository: str = None
force_clone: bool = False
script: str = None
extra_args: str = ""
def script_dir(self):
return os.path.join(SCRIPT_FOLDER, self.name)
def setup(self, log_cb, set_pid_cb):
os.makedirs(self.script_dir(), exist_ok=True)
if self.type == ScriptType.GIT:
_initialize_git_repository(self.git_repository, self.script_dir(), log_cb, self.force_clone, set_pid_cb)
self.force_clone = False
elif self.type == ScriptType.SIMPLE:
self._setup_simple()
set_executable(self.get_executable())
def _setup_simple(self):
with open(self.get_executable(), "w") as f:
f.write(
"#!/bin/bash\n"
"python run.py \"$@\""
)
with open(os.path.join(self.script_dir(), "run.py"), "w") as f:
f.write(self.script)
def get_executable(self):
return os.path.join(self.script_dir(), "run.sh")
def delete_dir(self):
shutil.rmtree(self.script_dir(), ignore_errors=True)
SCRIPT_TEMPLATES = {
"CLIP - Generate embeddings to predict the most relevant image based on the text prompt": lambda name: UserScript(
name=name,
type=ScriptType.GIT,
git_repository="https://github.com/sist2app/sist2-script-clip",
extra_args="--num-tags=1 --tags-file=general.txt --color=#dcd7ff"
),
"Whisper - Speech to text with OpenAI Whisper": lambda name: UserScript(
name=name,
type=ScriptType.GIT,
git_repository="https://github.com/simon987/sist2-script-whisper",
extra_args="--model=base --num-threads=4 --color=#51da4c --tag"
),
"Hamburger - Simple script example": lambda name: UserScript(
name=name,
type=ScriptType.SIMPLE,
script=
'from sist2 import Sist2Index\n'
'import sys\n'
'\n'
'index = Sist2Index(sys.argv[1])\n'
'for doc in index.document_iter():\n'
' doc.json_data["tag"] = ["hamburger.#00FF00"]\n'
' index.update_document(doc)\n'
'\n'
'index.sync_tag_table()\n'
'index.commit()\n'
'\n'
'print("Done!")\n'
),
"(Blank)": lambda name: UserScript(
name=name,
type=ScriptType.SIMPLE,
script=""
)
}

View File

@ -2,11 +2,10 @@ import datetime
import json import json
import logging import logging
import os.path import os.path
import sys
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from io import TextIOWrapper from io import TextIOWrapper
from logging import FileHandler, StreamHandler from logging import FileHandler
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from threading import Thread from threading import Thread
@ -42,6 +41,8 @@ class Sist2SearchBackend(BaseModel):
es_insecure_ssl: bool = False es_insecure_ssl: bool = False
es_index: str = "sist2" es_index: str = "sist2"
threads: int = 1 threads: int = 1
script: str = ""
script_file: str = None
batch_size: int = 70 batch_size: int = 70
@staticmethod @staticmethod
@ -73,6 +74,8 @@ class IndexOptions(BaseModel):
f"--es-index={search_backend.es_index}", f"--es-index={search_backend.es_index}",
f"--batch-size={search_backend.batch_size}"] f"--batch-size={search_backend.batch_size}"]
if search_backend.script_file:
args.append(f"--script-file={search_backend.script_file}")
if search_backend.es_insecure_ssl: if search_backend.es_insecure_ssl:
args.append(f"--es-insecure-ssl") args.append(f"--es-insecure-ssl")
if self.incremental_index: if self.incremental_index:
@ -201,7 +204,6 @@ class WebOptions(BaseModel):
auth0_client_id: str = None auth0_client_id: str = None
auth0_public_key: str = None auth0_public_key: str = None
auth0_public_key_file: str = None auth0_public_key_file: str = None
verbose: bool = False
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -233,8 +235,6 @@ class WebOptions(BaseModel):
args.append(f"--tag-auth={self.tag_auth}") args.append(f"--tag-auth={self.tag_auth}")
if self.dev: if self.dev:
args.append(f"--dev") args.append(f"--dev")
if self.verbose:
args.append(f"--very-verbose")
args.extend(self.indices) args.extend(self.indices)
@ -247,7 +247,14 @@ class Sist2:
self.bin_path = bin_path self.bin_path = bin_path
self._data_dir = data_directory self._data_dir = data_directory
def index(self, options: IndexOptions, search_backend: Sist2SearchBackend, logs_cb, set_pid_cb): def index(self, options: IndexOptions, search_backend: Sist2SearchBackend, logs_cb):
if search_backend.script and search_backend.backend_type == SearchBackendType("elasticsearch"):
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".painless", delete=False) as f:
f.write(search_backend.script)
search_backend.script_file = f.name
else:
search_backend.script_file = None
args = [ args = [
self.bin_path, self.bin_path,
@ -259,9 +266,7 @@ class Sist2:
logs_cb({"sist2-admin": f"Starting sist2 command with args {args}"}) logs_cb({"sist2-admin": f"Starting sist2 command with args {args}"})
proc = Popen(args, stdout=PIPE, stderr=PIPE) proc = Popen(args, stdout=PIPE, stderr=PIPE)
set_pid_cb(proc.pid) t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, None, proc))
t_stderr.start() t_stderr.start()
self._consume_logs_stdout(logs_cb, proc) self._consume_logs_stdout(logs_cb, proc)
@ -288,7 +293,7 @@ class Sist2:
set_pid_cb(proc.pid) set_pid_cb(proc.pid)
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, None, proc)) t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
t_stderr.start() t_stderr.start()
self._consume_logs_stdout(logs_cb, proc) self._consume_logs_stdout(logs_cb, proc)
@ -298,7 +303,7 @@ class Sist2:
return proc.returncode return proc.returncode
@staticmethod @staticmethod
def _consume_logs_stderr(logs_cb, exit_cb, proc): def _consume_logs_stderr(logs_cb, proc):
pipe_wrapper = TextIOWrapper(proc.stderr, encoding="utf8", errors="ignore") pipe_wrapper = TextIOWrapper(proc.stderr, encoding="utf8", errors="ignore")
try: try:
for line in pipe_wrapper: for line in pipe_wrapper:
@ -306,9 +311,7 @@ class Sist2:
continue continue
logs_cb({"stderr": line}) logs_cb({"stderr": line})
finally: finally:
return_code = proc.wait() proc.wait()
if exit_cb:
exit_cb(return_code)
pipe_wrapper.close() pipe_wrapper.close()
@staticmethod @staticmethod
@ -342,19 +345,15 @@ class Sist2:
web_logger = logging.Logger(name=f"sist2-frontend-{name}") web_logger = logging.Logger(name=f"sist2-frontend-{name}")
web_logger.addHandler(FileHandler(os.path.join(LOG_FOLDER, f"frontend-{name}.log"))) web_logger.addHandler(FileHandler(os.path.join(LOG_FOLDER, f"frontend-{name}.log")))
web_logger.addHandler(StreamHandler())
def logs_cb(message): def logs_cb(message):
web_logger.info(json.dumps(message)) web_logger.info(json.dumps(message))
def exit_cb(return_code):
logger.info(f"Web frontend exited with return code {return_code}")
logger.info(f"Starting frontend {' '.join(args)}") logger.info(f"Starting frontend {' '.join(args)}")
proc = Popen(args, stdout=PIPE, stderr=PIPE) proc = Popen(args, stdout=PIPE, stderr=PIPE)
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, exit_cb, proc)) t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
t_stderr.start() t_stderr.start()
t_stdout = Thread(target=self._consume_logs_stdout, args=(logs_cb, proc)) t_stdout = Thread(target=self._consume_logs_stdout, args=(logs_cb, proc))

View File

@ -14,7 +14,7 @@ RUNNING_FRONTENDS: Dict[str, int] = {}
TESSERACT_LANGS = get_tesseract_langs() TESSERACT_LANGS = get_tesseract_langs()
DB_SCHEMA_VERSION = "5" DB_SCHEMA_VERSION = "4"
from pydantic import BaseModel from pydantic import BaseModel

View File

@ -1,41 +0,0 @@
from glob import glob
import os
from config import DATA_FOLDER
def get_old_index_files(name):
files = glob(os.path.join(DATA_FOLDER, f"scan-{name.replace('/', '_')}-*.sist2"))
files = list(sorted(files, key=lambda f: os.stat(f).st_mtime))
files = files[-1:]
return files
def tail_sync(filename, lines=1, _buffer=4098):
with open(filename) as f:
lines_found = []
block_counter = -1
while len(lines_found) < lines:
try:
f.seek(block_counter * _buffer, os.SEEK_END)
except IOError:
f.seek(0)
lines_found = f.readlines()
break
lines_found = f.readlines()
block_counter -= 1
return lines_found[-lines:]
def pid_is_running(pid):
try:
os.kill(pid, 0)
except OSError:
return False
return True

Binary file not shown.

15841
sist2-vue/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "sist2", "name": "sist2",
"version": "1.0.0", "version": "2.11.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -9,7 +9,8 @@
"dependencies": { "dependencies": {
"@auth0/auth0-spa-js": "^2.0.2", "@auth0/auth0-spa-js": "^2.0.2",
"@egjs/vue-infinitegrid": "3.3.0", "@egjs/vue-infinitegrid": "3.3.0",
"axios": "^1.6.0", "@tensorflow/tfjs": "^4.4.0",
"axios": "^0.25.0",
"bootstrap-vue": "^2.21.2", "bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"d3": "^5.6.1", "d3": "^5.6.1",
@ -17,7 +18,6 @@
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"fslightbox-vue": "fslightbox-vue.tgz", "fslightbox-vue": "fslightbox-vue.tgz",
"nouislider": "^15.2.0", "nouislider": "^15.2.0",
"onnxruntime-web": "1.15.1",
"underscore": "^1.13.1", "underscore": "^1.13.1",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-color": "^2.8.1", "vue-color": "^2.8.1",
@ -29,9 +29,9 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@types/underscore": "^1.11.6",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8", "@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8", "@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "^5.0.8", "@vue/cli-service": "^5.0.8",
"@vue/test-utils": "^1.0.3", "@vue/test-utils": "^1.0.3",
@ -43,7 +43,8 @@
"portal-vue": "^2.1.7", "portal-vue": "^2.1.7",
"sass": "^1.26.11", "sass": "^1.26.11",
"sass-loader": "^10.0.2", "sass-loader": "^10.0.2",
"vue-cli-plugin-bootstrap-vue": "~0.8.2", "typescript": "~4.1.5",
"vue-cli-plugin-bootstrap-vue": "~0.7.0",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
}, },
"browserslist": [ "browserslist": [

View File

@ -308,21 +308,15 @@ html, body {
.info-icon { .info-icon {
width: 1rem; width: 1rem;
min-width: 1rem;
margin-right: 0.2rem; margin-right: 0.2rem;
cursor: pointer; cursor: pointer;
line-height: 1rem; line-height: 1rem;
height: 1rem; height: 1rem;
min-height: 1rem;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==); background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
filter: brightness(45%); filter: brightness(45%);
display: block; display: block;
} }
.theme-black .info-icon {
filter: brightness(80%);
}
.tabs { .tabs {
margin-top: 10px; margin-top: 10px;
} }

View File

@ -1,20 +1,115 @@
import axios from "axios"; import axios from "axios";
import {strUnescape, lum, sid} from "./util"; import {ext, strUnescape, lum} from "./util";
import Sist2Query from "@/Sist2ElasticsearchQuery"; import Sist2Query from "@/Sist2ElasticsearchQuery";
import store from "@/store"; import store from "@/store";
export interface EsTag {
id: string
count: number
color: string | undefined
isLeaf: boolean
}
export interface Tag {
style: string
text: string
rawText: string
fg: string
bg: string
userTag: boolean
}
export interface Index {
name: string
version: string
id: string
idPrefix: string
timestamp: number
}
export interface EsHit {
_index: string
_id: string
_score: number
_type: string
_tags: Tag[]
_seq: number
_source: {
path: string
size: number
mime: string
name: string
extension: string
index: string
_depth: number
mtime: number
videoc: string
audioc: string
parent: string
width: number
height: number
duration: number
tag: string[]
checksum: string
thumbnail: string
}
_props: {
isSubDocument: boolean
isImage: boolean
isGif: boolean
isVideo: boolean
isPlayableVideo: boolean
isPlayableImage: boolean
isAudio: boolean
hasThumbnail: boolean
hasVidPreview: boolean
imageAspectRatio: number
/** Number of thumbnails available */
tnNum: number
}
highlight: {
name: string[] | undefined,
content: string[] | undefined,
}
}
function getIdPrefix(indices: Index[], id: string): string {
for (let i = 4; i < 32; i++) {
const prefix = id.slice(0, i);
if (indices.filter(idx => idx.id.slice(0, i) == prefix).length == 1) {
return prefix;
}
}
return id;
}
export interface EsResult {
took: number
hits: {
// TODO: ES 6.X ?
total: {
value: number
}
hits: EsHit[]
}
aggregations: any
}
class Sist2Api { class Sist2Api {
baseUrl; private readonly baseUrl: string
sist2Info; private sist2Info: any
queryfunc; private queryfunc: () => EsResult;
constructor(baseUrl) { constructor(baseUrl: string) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
} }
init(queryFunc) { init(queryFunc: () => EsResult) {
this.queryfunc = queryFunc; this.queryfunc = queryFunc;
} }
@ -22,25 +117,28 @@ class Sist2Api {
return this.sist2Info.searchBackend; return this.sist2Info.searchBackend;
} }
models() { getSist2Info(): Promise<any> {
const allModels = this.sist2Info.indices
.map(idx => idx.models)
.flat();
return allModels
.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i)
}
getSist2Info() {
return axios.get(`${this.baseUrl}i`).then(resp => { return axios.get(`${this.baseUrl}i`).then(resp => {
const indices = resp.data.indices as Index[];
resp.data.indices = indices.map(idx => {
return {
id: idx.id,
name: idx.name,
timestamp: idx.timestamp,
version: idx.version,
idPrefix: getIdPrefix(indices, idx.id)
} as Index;
});
this.sist2Info = resp.data; this.sist2Info = resp.data;
return resp.data; return resp.data;
}) })
} }
setHitProps(hit) { setHitProps(hit: EsHit): void {
hit["_props"] = {}; hit["_props"] = {} as any;
const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0]; const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0];
@ -48,7 +146,7 @@ class Sist2Api {
hit._props.isSubDocument = true; hit._props.isSubDocument = true;
} }
if ("thumbnail" in hit._source && hit._source.thumbnail > 0) { if ("thumbnail" in hit._source) {
hit._props.hasThumbnail = true; hit._props.hasThumbnail = true;
if (Number.isNaN(Number(hit._source.thumbnail))) { if (Number.isNaN(Number(hit._source.thumbnail))) {
@ -104,8 +202,8 @@ class Sist2Api {
} }
} }
setHitTags(hit) { setHitTags(hit: EsHit): void {
const tags = []; const tags = [] as Tag[];
// User tags // User tags
if ("tag" in hit._source) { if ("tag" in hit._source) {
@ -117,10 +215,10 @@ class Sist2Api {
hit._tags = tags; hit._tags = tags;
} }
createUserTag(tag) { createUserTag(tag: string): Tag {
const tokens = tag.split("."); const tokens = tag.split(".");
const colorToken = tokens.pop(); const colorToken = tokens.pop() as string;
const bg = colorToken; const bg = colorToken;
const fg = lum(colorToken) > 50 ? "#000" : "#fff"; const fg = lum(colorToken) > 50 ? "#000" : "#fff";
@ -132,30 +230,25 @@ class Sist2Api {
text: tokens.join("."), text: tokens.join("."),
rawText: tag, rawText: tag,
userTag: true, userTag: true,
}; } as Tag;
} }
search() { search(): Promise<EsResult> {
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
return this.ftsQuery(this.queryfunc()) return this.ftsQuery(this.queryfunc())
} else { } else {
return this.esQuery(this.queryfunc()); return this.esQuery(this.queryfunc());
} }
} }
_getIndexRoot(indexId) { esQuery(query: any): Promise<EsResult> {
return this.sist2Info.indices.find(idx => idx.id === indexId).root;
}
esQuery(query) {
return axios.post(`${this.baseUrl}es`, query).then(resp => { return axios.post(`${this.baseUrl}es`, query).then(resp => {
const res = resp.data; const res = resp.data as EsResult;
if (res.hits?.hits) { if (res.hits?.hits) {
res.hits.hits.forEach((hit) => { res.hits.hits.forEach((hit: EsHit) => {
hit["_source"]["name"] = strUnescape(hit["_source"]["name"]); hit["_source"]["name"] = strUnescape(hit["_source"]["name"]);
hit["_source"]["path"] = strUnescape(hit["_source"]["path"]); hit["_source"]["path"] = strUnescape(hit["_source"]["path"]);
hit["_source"]["indexRoot"] = this._getIndexRoot(hit["_source"]["index"]);
this.setHitProps(hit); this.setHitProps(hit);
this.setHitTags(hit); this.setHitTags(hit);
@ -166,9 +259,9 @@ class Sist2Api {
}); });
} }
ftsQuery(query) { ftsQuery(query: any): Promise<EsResult> {
return axios.post(`${this.baseUrl}fts/search`, query).then(resp => { return axios.post(`${this.baseUrl}fts/search`, query).then(resp => {
const res = resp.data; const res = resp.data as any;
if (res.hits.hits) { if (res.hits.hits) {
res.hits.hits.forEach(hit => { res.hits.hits.forEach(hit => {
@ -189,7 +282,7 @@ class Sist2Api {
}); });
} }
getMimeTypesEs(query) { private getMimeTypesEs(query) {
const AGGS = { const AGGS = {
mimeTypes: { mimeTypes: {
terms: { terms: {
@ -218,7 +311,7 @@ class Sist2Api {
}); });
} }
getMimeTypesSqlite() { private getMimeTypesSqlite(): Promise<[{ mime: string, count: number }]> {
return axios.get(`${this.baseUrl}fts/mimetypes`) return axios.get(`${this.baseUrl}fts/mimetypes`)
.then(resp => { .then(resp => {
return resp.data; return resp.data;
@ -228,15 +321,15 @@ class Sist2Api {
async getMimeTypes(query = undefined) { async getMimeTypes(query = undefined) {
let buckets; let buckets;
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
buckets = await this.getMimeTypesSqlite(); buckets = await this.getMimeTypesSqlite();
} else { } else {
buckets = await this.getMimeTypesEs(query); buckets = await this.getMimeTypesEs(query);
} }
const mimeMap = []; const mimeMap: any[] = [];
buckets.sort((a, b) => a.mime > b.mime).forEach((bucket) => { buckets.sort((a: any, b: any) => a.mime > b.mime).forEach((bucket: any) => {
const tmp = bucket.mime.split("/"); const tmp = bucket.mime.split("/");
const category = tmp[0]; const category = tmp[0];
const mime = tmp[1]; const mime = tmp[1];
@ -270,7 +363,7 @@ class Sist2Api {
return {buckets, mimeMap}; return {buckets, mimeMap};
} }
_createEsTag(tag, count) { _createEsTag(tag: string, count: number): EsTag {
const tokens = tag.split("."); const tokens = tag.split(".");
if (/.*\.#[0-9a-fA-F]{6}/.test(tag)) { if (/.*\.#[0-9a-fA-F]{6}/.test(tag)) {
@ -290,7 +383,7 @@ class Sist2Api {
}; };
} }
getTagsEs() { private getTagsEs() {
return this.esQuery({ return this.esQuery({
aggs: { aggs: {
tags: { tags: {
@ -303,21 +396,21 @@ class Sist2Api {
size: 0, size: 0,
}).then(resp => { }).then(resp => {
return resp["aggregations"]["tags"]["buckets"] return resp["aggregations"]["tags"]["buckets"]
.sort((a, b) => a["key"].localeCompare(b["key"])) .sort((a: any, b: any) => a["key"].localeCompare(b["key"]))
.map((bucket) => this._createEsTag(bucket["key"], bucket["doc_count"])); .map((bucket: any) => this._createEsTag(bucket["key"], bucket["doc_count"]));
}); });
} }
getTagsSqlite() { private getTagsSqlite() {
return axios.get(`${this.baseUrl}fts/tags`) return axios.get(`${this.baseUrl}/fts/tags`)
.then(resp => { .then(resp => {
return resp.data.map(tag => this._createEsTag(tag.tag, tag.count)) return resp.data.map(tag => this._createEsTag(tag.tag, tag.count))
}); });
} }
async getTags() { async getTags(): Promise<EsTag[]> {
let tags; let tags;
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
tags = await this.getTagsSqlite(); tags = await this.getTagsSqlite();
} else { } else {
tags = await this.getTagsEs(); tags = await this.getTagsEs();
@ -326,7 +419,7 @@ class Sist2Api {
// Remove duplicates (same tag with different color) // Remove duplicates (same tag with different color)
const seen = new Set(); const seen = new Set();
return tags.filter((t) => { return tags.filter((t: EsTag) => {
if (seen.has(t.id)) { if (seen.has(t.id)) {
return false; return false;
} }
@ -335,29 +428,31 @@ class Sist2Api {
}); });
} }
saveTag(tag, hit) { saveTag(tag: string, hit: EsHit) {
return axios.post(`${this.baseUrl}tag/${sid(hit)}`, { return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], {
delete: false, delete: false,
name: tag, name: tag,
doc_id: hit["_id"]
}); });
} }
deleteTag(tag, hit) { deleteTag(tag: string, hit: EsHit) {
return axios.post(`${this.baseUrl}tag/${sid(hit)}`, { return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], {
delete: true, delete: true,
name: tag, name: tag,
doc_id: hit["_id"]
}); });
} }
searchPaths(indexId, minDepth, maxDepth, prefix = null) { searchPaths(indexId, minDepth, maxDepth, prefix = null) {
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
return this.searchPathsSqlite(indexId, minDepth, minDepth, prefix); return this.searchPathsSqlite(indexId, minDepth, minDepth, prefix);
} else { } else {
return this.searchPathsEs(indexId, minDepth, maxDepth, prefix); return this.searchPathsEs(indexId, minDepth, maxDepth, prefix);
} }
} }
searchPathsSqlite(indexId, minDepth, maxDepth, prefix) { private searchPathsSqlite(indexId, minDepth, maxDepth, prefix) {
return axios.post(`${this.baseUrl}fts/paths`, { return axios.post(`${this.baseUrl}fts/paths`, {
indexId, minDepth, maxDepth, prefix indexId, minDepth, maxDepth, prefix
}).then(resp => { }).then(resp => {
@ -365,7 +460,7 @@ class Sist2Api {
}); });
} }
searchPathsEs(indexId, minDepth, maxDepth, prefix) { private searchPathsEs(indexId, minDepth, maxDepth, prefix): Promise<[{ path: string, count: number }]> {
const query = { const query = {
query: { query: {
@ -410,25 +505,23 @@ class Sist2Api {
}); });
} }
getDateRangeSqlite() { private getDateRangeSqlite() {
return axios.get(`${this.baseUrl}fts/dateRange`) return axios.get(`${this.baseUrl}fts/dateRange`)
.then(resp => ({ .then(resp => ({
min: resp.data.dateMin, min: resp.data.dateMin,
max: (resp.data.dateMax === resp.data.dateMin) max: resp.data.dateMax,
? resp.data.dateMax + 1
: resp.data.dateMax,
})); }));
} }
getDateRange() { getDateRange(): Promise<{ min: number, max: number }> {
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
return this.getDateRangeSqlite(); return this.getDateRangeSqlite();
} else { } else {
return this.getDateRangeEs(); return this.getDateRangeEs();
} }
} }
getDateRangeEs() { private getDateRangeEs() {
return this.esQuery({ return this.esQuery({
// TODO: filter current selected indices // TODO: filter current selected indices
aggs: { aggs: {
@ -445,7 +538,7 @@ class Sist2Api {
if (range.min == null) { if (range.min == null) {
range.min = 0; range.min = 0;
range.max = 1; range.max = 1;
} else if (range.min === range.max) { } else if (range.min == range.max) {
range.max += 1; range.max += 1;
} }
@ -453,7 +546,7 @@ class Sist2Api {
}); });
} }
getPathSuggestionsSqlite(text) { private getPathSuggestionsSqlite(text: string) {
return axios.post(`${this.baseUrl}fts/paths`, { return axios.post(`${this.baseUrl}fts/paths`, {
prefix: text, prefix: text,
minDepth: 1, minDepth: 1,
@ -463,7 +556,7 @@ class Sist2Api {
}) })
} }
getPathSuggestionsEs(text) { private getPathSuggestionsEs(text) {
return this.esQuery({ return this.esQuery({
suggest: { suggest: {
path: { path: {
@ -481,31 +574,31 @@ class Sist2Api {
}); });
} }
getPathSuggestions(text) { getPathSuggestions(text: string): Promise<string[]> {
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
return this.getPathSuggestionsSqlite(text); return this.getPathSuggestionsSqlite(text);
} else { } else {
return this.getPathSuggestionsEs(text) return this.getPathSuggestionsEs(text)
} }
} }
getTreemapStat(indexId) { getTreemapStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/TMAP`; return `${this.baseUrl}s/${indexId}/TMAP`;
} }
getMimeStat(indexId) { getMimeStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/MAGG`; return `${this.baseUrl}s/${indexId}/MAGG`;
} }
getSizeStat(indexId) { getSizeStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/SAGG`; return `${this.baseUrl}s/${indexId}/SAGG`;
} }
getDateStat(indexId) { getDateStat(indexId: string) {
return `${this.baseUrl}s/${indexId}/DAGG`; return `${this.baseUrl}s/${indexId}/DAGG`;
} }
getDocumentEs(sid, highlight, fuzzy) { private getDocumentEs(docId: string, highlight: boolean, fuzzy: boolean) {
const query = Sist2Query.searchQuery(); const query = Sist2Query.searchQuery();
if (highlight) { if (highlight) {
@ -525,15 +618,6 @@ class Sist2Api {
} }
} }
if ("knn" in query) {
query.query = {
bool: {
must: []
}
};
delete query.knn;
}
if ("function_score" in query.query) { if ("function_score" in query.query) {
query.query = query.query.function_score.query; query.query = query.query.function_score.query;
} }
@ -544,7 +628,7 @@ class Sist2Api {
query.query.bool.must = [query.query.bool.must]; query.query.bool.must = [query.query.bool.must];
} }
query.query.bool.must.push({match: {_id: sid}}); query.query.bool.must.push({match: {_id: docId}});
delete query["sort"]; delete query["sort"];
delete query["aggs"]; delete query["aggs"];
@ -565,35 +649,35 @@ class Sist2Api {
}); });
} }
getDocumentSqlite(sid) { private getDocumentSqlite(docId: string): Promise<EsHit> {
return axios.get(`${this.baseUrl}fts/d/${sid}`) return axios.get(`${this.baseUrl}/fts/d/${docId}`)
.then(resp => ({ .then(resp => ({
_source: resp.data _source: resp.data
})); } as EsHit));
} }
getDocument(sid, highlight, fuzzy) { getDocument(docId: string, highlight: boolean, fuzzy: boolean): Promise<EsHit | null> {
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
return this.getDocumentSqlite(sid); return this.getDocumentSqlite(docId);
} else { } else {
return this.getDocumentEs(sid, highlight, fuzzy); return this.getDocumentEs(docId, highlight, fuzzy);
} }
} }
getTagSuggestions(prefix) { getTagSuggestions(prefix: string): Promise<string[]> {
if (this.backend() === "sqlite") { if (this.backend() == "sqlite") {
return this.getTagSuggestionsSqlite(prefix); return this.getTagSuggestionsSqlite(prefix);
} else { } else {
return this.getTagSuggestionsEs(prefix); return this.getTagSuggestionsEs(prefix);
} }
} }
getTagSuggestionsSqlite(prefix) { private getTagSuggestionsSqlite(prefix): Promise<string[]> {
return axios.post(`${this.baseUrl}fts/suggestTags`, prefix) return axios.post(`${this.baseUrl}/fts/suggestTags`, prefix)
.then(resp => (resp.data)); .then(resp => (resp.data));
} }
getTagSuggestionsEs(prefix) { private getTagSuggestionsEs(prefix): Promise<string[]> {
return this.esQuery({ return this.esQuery({
suggest: { suggest: {
tag: { tag: {
@ -618,11 +702,6 @@ class Sist2Api {
return result; return result;
}); });
} }
getEmbeddings(sid, modelId) {
return axios.post(`${this.baseUrl}e/${sid}/${modelId.toString().padStart(3, '0')}`)
.then(resp => (resp.data));
}
} }
export default new Sist2Api(""); export default new Sist2Api("");

View File

@ -1,5 +1,5 @@
import store from "@/store"; import store from "./store";
import Sist2Api from "@/Sist2Api"; import {EsHit, Index} from "@/Sist2Api";
const SORT_MODES = { const SORT_MODES = {
score: { score: {
@ -7,62 +7,62 @@ const SORT_MODES = {
{_score: {order: "desc"}}, {_score: {order: "desc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._score key: (hit: EsHit) => hit._score
}, },
random: { random: {
mode: [ mode: [
{_score: {order: "desc"}}, {_score: {order: "desc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._score key: (hit: EsHit) => hit._score
}, },
dateAsc: { dateAsc: {
mode: [ mode: [
{mtime: {order: "asc"}}, {mtime: {order: "asc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._source.mtime key: (hit: EsHit) => hit._source.mtime
}, },
dateDesc: { dateDesc: {
mode: [ mode: [
{mtime: {order: "desc"}}, {mtime: {order: "desc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._source.mtime key: (hit: EsHit) => hit._source.mtime
}, },
sizeAsc: { sizeAsc: {
mode: [ mode: [
{size: {order: "asc"}}, {size: {order: "asc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._source.size key: (hit: EsHit) => hit._source.size
}, },
sizeDesc: { sizeDesc: {
mode: [ mode: [
{size: {order: "desc"}}, {size: {order: "desc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._source.size key: (hit: EsHit) => hit._source.size
}, },
nameAsc: { nameAsc: {
mode: [ mode: [
{name: {order: "asc"}}, {name: {order: "asc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._source.name key: (hit: EsHit) => hit._source.name
}, },
nameDesc: { nameDesc: {
mode: [ mode: [
{name: {order: "desc"}}, {name: {order: "desc"}},
{_tie: {order: "asc"}} {_tie: {order: "asc"}}
], ],
key: (hit) => hit._source.name key: (hit: EsHit) => hit._source.name
} }
}; } as any;
class Sist2ElasticsearchQuery { class Sist2ElasticsearchQuery {
searchQuery(blankSearch = false) { searchQuery(blankSearch: boolean = false): any {
const getters = store.getters; const getters = store.getters;
@ -76,17 +76,15 @@ class Sist2ElasticsearchQuery {
const fuzzy = getters.fuzzy; const fuzzy = getters.fuzzy;
const size = getters.size; const size = getters.size;
const after = getters.lastDoc; const after = getters.lastDoc;
const selectedIndexIds = getters.selectedIndices.map((idx) => idx.id) const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id)
const selectedMimeTypes = getters.selectedMimeTypes; const selectedMimeTypes = getters.selectedMimeTypes;
const selectedTags = getters.selectedTags; const selectedTags = getters.selectedTags;
const sortMode = getters.embedding ? "score" : getters.sortMode;
const legacyES = store.state.sist2Info.esVersionLegacy; const legacyES = store.state.sist2Info.esVersionLegacy;
const hasKnn = store.state.sist2Info.esVersionHasKnn;
const filters = [ const filters = [
{terms: {index: selectedIndexIds}} {terms: {index: selectedIndexIds}}
]; ] as any[];
const fields = [ const fields = [
"name^8", "name^8",
@ -117,11 +115,11 @@ class Sist2ElasticsearchQuery {
} }
if (dateMin && dateMax) { if (dateMin && dateMax) {
filters.push({range: {mtime: {gte: dateMin, lte: dateMax, format: "epoch_second"}}}) filters.push({range: {mtime: {gte: dateMin, lte: dateMax}}})
} else if (dateMin) { } else if (dateMin) {
filters.push({range: {mtime: {gte: dateMin, format: "epoch_second"}}}) filters.push({range: {mtime: {gte: dateMin}}})
} else if (dateMax) { } else if (dateMax) {
filters.push({range: {mtime: {lte: dateMax, format: "epoch_second"}}}) filters.push({range: {mtime: {lte: dateMax}}})
} }
const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
@ -138,7 +136,7 @@ class Sist2ElasticsearchQuery {
if (getters.optTagOrOperator) { if (getters.optTagOrOperator) {
filters.push({terms: {"tag": selectedTags}}); filters.push({terms: {"tag": selectedTags}});
} else { } else {
selectedTags.forEach((tag) => filters.push({term: {"tag": tag}})); selectedTags.forEach((tag: string) => filters.push({term: {"tag": tag}}));
} }
} }
} }
@ -164,16 +162,16 @@ class Sist2ElasticsearchQuery {
const q = { const q = {
_source: { _source: {
excludes: ["content", "_tie", "emb.*"] excludes: ["content", "_tie"]
}, },
query: { query: {
bool: { bool: {
filter: filters, filter: filters,
} }
}, },
sort: SORT_MODES[sortMode].mode, sort: SORT_MODES[getters.sortMode].mode,
size: size, size: size,
}; } as any;
if (!after) { if (!after) {
q.aggs = { q.aggs = {
@ -183,57 +181,14 @@ class Sist2ElasticsearchQuery {
} }
if (!empty && !blankSearch) { if (!empty && !blankSearch) {
if (getters.embedding) { q.query.bool.must = query;
filters.push(query)
} else {
q.query.bool.must = query;
}
}
if (getters.embedding) {
delete q.query;
const field = "emb." + Sist2Api.models().find(m => m.id === getters.embeddingsModel).path;
if (hasKnn) {
// Use knn (8.8+)
q.knn = {
field: field,
query_vector: getters.embedding,
k: 600,
num_candidates: 600,
filter: filters
}
} else {
// Use brute-force as a fallback
filters.push({exists: {field: field}});
q.query = {
function_score: {
query: {
bool: {
must: filters,
}
},
script_score: {
script: {
source: `cosineSimilarity(params.query_vector, "${field}") + 1.0`,
params: {query_vector: getters.embedding}
}
}
}
}
}
} }
if (after) { if (after) {
q.search_after = [SORT_MODES[sortMode].key(after), after["_id"]]; q.search_after = [SORT_MODES[getters.sortMode].key(after), after["_id"]];
} }
if (getters.optHighlight && !getters.embedding) { if (getters.optHighlight) {
q.highlight = { q.highlight = {
pre_tags: ["<mark>"], pre_tags: ["<mark>"],
post_tags: ["</mark>"], post_tags: ["</mark>"],
@ -259,7 +214,7 @@ class Sist2ElasticsearchQuery {
} }
} }
if (sortMode === "random") { if (getters.sortMode === "random") {
q.query = { q.query = {
function_score: { function_score: {
query: { query: {

View File

@ -1,4 +1,5 @@
import store from "@/store"; import store from "./store";
import {EsHit, Index} from "@/Sist2Api";
const SORT_MODES = { const SORT_MODES = {
score: { score: {
@ -28,12 +29,18 @@ const SORT_MODES = {
"sort": "name", "sort": "name",
"sortAsc": false "sortAsc": false
} }
}; } as any;
interface SortMode {
text: string
mode: any[]
key: (hit: EsHit) => any
}
class Sist2ElasticsearchQuery { class Sist2ElasticsearchQuery {
searchQuery() { searchQuery(): any {
const getters = store.getters; const getters = store.getters;
@ -45,7 +52,7 @@ class Sist2ElasticsearchQuery {
const dateMax = getters.dateMax; const dateMax = getters.dateMax;
const size = getters.size; const size = getters.size;
const after = getters.lastDoc; const after = getters.lastDoc;
const selectedIndexIds = getters.selectedIndices.map((idx) => idx.id) const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id)
const selectedMimeTypes = getters.selectedMimeTypes; const selectedMimeTypes = getters.selectedMimeTypes;
const selectedTags = getters.selectedTags; const selectedTags = getters.selectedTags;
@ -88,7 +95,7 @@ class Sist2ElasticsearchQuery {
if (selectedTags.length > 0) { if (selectedTags.length > 0) {
q["tags"] = selectedTags q["tags"] = selectedTags
} }
if (getters.sortMode === "random") { if (getters.sortMode == "random") {
q["seed"] = getters.seed; q["seed"] = getters.seed;
} }
if (getters.optHighlight) { if (getters.optHighlight) {
@ -96,18 +103,6 @@ class Sist2ElasticsearchQuery {
q["highlightContextSize"] = Number(getters.optFragmentSize); q["highlightContextSize"] = Number(getters.optFragmentSize);
} }
if (getters.embedding) {
q["model"] = getters.embeddingsModel;
q["embedding"] = getters.embedding;
q["sort"] = "embedding";
q["sortAsc"] = false;
} else if (getters.sortMode === "embedding") {
q["sort"] = "sort"
q["sortAsc"] = true;
}
q["searchInPath"] = getters.optSearchInPath;
return q; return q;
} }
} }

View File

@ -12,7 +12,7 @@ export default {
props: ["span", "text"], props: ["span", "text"],
methods: { methods: {
getStyle() { getStyle() {
return ModelsRepo.data[this.$store.getters.nerModel.name].labelStyles[this.span.label]; return ModelsRepo.data[this.$store.getters.mlModel.name].labelStyles[this.span.label];
} }
} }
} }

View File

@ -22,7 +22,7 @@ export default {
props: ["spans", "text"], props: ["spans", "text"],
computed: { computed: {
legend() { legend() {
return Object.entries(ModelsRepo.data[this.$store.state.nerModel.name].legend) return Object.entries(ModelsRepo.data[this.$store.state.mlModel.name].legend)
.map(([label, name]) => ({ .map(([label, name]) => ({
text: name, text: name,
id: label, id: label,

View File

@ -9,7 +9,8 @@
<script> <script>
import * as d3 from "d3"; import * as d3 from "d3";
import {humanFileSize, burrow} from "@/util"; import {burrow} from "@/util-js"
import {humanFileSize} from "@/util";
import Sist2Api from "@/Sist2Api"; import Sist2Api from "@/Sist2Api";
import domtoimage from "dom-to-image"; import domtoimage from "dom-to-image";

View File

@ -31,7 +31,8 @@
<script> <script>
import noUiSlider from 'nouislider'; import noUiSlider from 'nouislider';
import 'nouislider/dist/nouislider.css'; import 'nouislider/dist/nouislider.css';
import {humanDate, mergeTooltips} from "@/util"; import {humanDate} from "@/util";
import {mergeTooltips} from "@/util-js";
export default { export default {
name: "DateSlider", name: "DateSlider",

View File

@ -45,8 +45,7 @@ export default {
items.push( items.push(
{key: "esVersion", value: this.$store.state.sist2Info.esVersion}, {key: "esVersion", value: this.$store.state.sist2Info.esVersion},
{key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported}, {key: "esVersionSupported", value: this.$store.state.sist2Info.esVersionSupported},
{key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy}, {key: "esVersionLegacy", value: this.$store.state.sist2Info.esVersionLegacy}
{key: "esVersionHasKnn", value: this.$store.state.sist2Info.esVersionHasKnn},
); );
} }

View File

@ -16,7 +16,7 @@
<!-- Audio player--> <!-- Audio player-->
<audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls <audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls
:type="doc._source.mime" :type="doc._source.mime"
:src="`f/${sid(doc)}`" :src="`f/${doc._source.index}/${doc._id}`"
@play="onAudioPlay()"></audio> @play="onAudioPlay()"></audio>
<b-card-body class="padding-03"> <b-card-body class="padding-03">
@ -24,7 +24,6 @@
<!-- Title line --> <!-- Title line -->
<div style="display: flex"> <div style="display: flex">
<span class="info-icon" @click="onInfoClick()"></span> <span class="info-icon" @click="onInfoClick()"></span>
<MLIcon v-if="doc._source.embedding" clickable @click="onEmbeddingClick()"></MLIcon>
<DocFileTitle :doc="doc"></DocFileTitle> <DocFileTitle :doc="doc"></DocFileTitle>
</div> </div>
@ -43,19 +42,17 @@
</template> </template>
<script> <script>
import {ext, humanFileSize, humanTime, sid} from "@/util"; import {ext, humanFileSize, humanTime} from "@/util";
import TagContainer from "@/components/TagContainer.vue"; import TagContainer from "@/components/TagContainer.vue";
import DocFileTitle from "@/components/DocFileTitle.vue"; import DocFileTitle from "@/components/DocFileTitle.vue";
import DocInfoModal from "@/components/DocInfoModal.vue"; import DocInfoModal from "@/components/DocInfoModal.vue";
import ContentDiv from "@/components/ContentDiv.vue"; import ContentDiv from "@/components/ContentDiv.vue";
import FullThumbnail from "@/components/FullThumbnail"; import FullThumbnail from "@/components/FullThumbnail";
import FeaturedFieldsLine from "@/components/FeaturedFieldsLine"; import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
import MLIcon from "@/components/icons/MlIcon.vue";
import Sist2Api from "@/Sist2Api";
export default { export default {
components: {MLIcon, FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer}, components: {FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
props: ["doc", "width"], props: ["doc", "width"],
data() { data() {
return { return {
@ -69,19 +66,11 @@ export default {
} }
}, },
methods: { methods: {
sid: sid,
humanFileSize: humanFileSize, humanFileSize: humanFileSize,
humanTime: humanTime, humanTime: humanTime,
onInfoClick() { onInfoClick() {
this.showInfo = true; this.showInfo = true;
}, },
onEmbeddingClick() {
Sist2Api.getEmbeddings(sid(this.doc), this.$store.state.embeddingsModel).then(embeddings => {
this.$store.commit("setEmbeddingText", "");
this.$store.commit("setEmbedding", embeddings);
this.$store.commit("setEmbeddingDoc", this.doc);
})
},
async onThumbnailClick() { async onThumbnailClick() {
this.$store.commit("setUiLightboxSlide", this.doc._seq); this.$store.commit("setUiLightboxSlide", this.doc._seq);
await this.$store.dispatch("showLightbox"); await this.$store.dispatch("showLightbox");

View File

@ -1,16 +1,15 @@
<template> <template>
<GridLayout <GridLayout
ref="grid-layout" ref="grid-layout"
:options="gridOptions" :options="gridOptions"
@append="append" @append="append"
@layout-complete="$emit('layout-complete')" @layout-complete="$emit('layout-complete')"
> >
<DocCard v-for="doc in docs" :key="sid(doc)" :doc="doc" :width="width"></DocCard> <DocCard v-for="doc in docs" :key="doc._id" :doc="doc" :width="width"></DocCard>
</GridLayout> </GridLayout>
</template> </template>
<script> <script>
import {sid} from "@/util";
import Vue from "vue"; import Vue from "vue";
import DocCard from "@/components/DocCard"; import DocCard from "@/components/DocCard";
@ -19,53 +18,50 @@ import VueInfiniteGrid from "@egjs/vue-infinitegrid";
Vue.use(VueInfiniteGrid); Vue.use(VueInfiniteGrid);
export default Vue.extend({ export default Vue.extend({
components: { components: {
DocCard, DocCard,
}, },
props: ["docs", "append"], props: ["docs", "append"],
data() { data() {
return { return {
width: 0, width: 0,
gridOptions: { gridOptions: {
align: "center", align: "center",
margin: 0, margin: 0,
transitionDuration: 0, transitionDuration: 0,
isOverflowScroll: false, isOverflowScroll: false,
isConstantSize: false, isConstantSize: false,
useFit: false, useFit: false,
// Indicates whether keep the number of DOMs is maintained. If the useRecycle value is 'true', keep the number // Indicates whether keep the number of DOMs is maintained. If the useRecycle value is 'true', keep the number
// of DOMs is maintained. If the useRecycle value is 'false', the number of DOMs will increase as card elements // of DOMs is maintained. If the useRecycle value is 'false', the number of DOMs will increase as card elements
// are added. // are added.
useRecycle: false useRecycle: false
} }
} }
}, },
methods: { computed: {
sid, colCount() {
}, const columns = this.$store.getters["optColumns"];
computed: {
colCount() {
const columns = this.$store.getters["optColumns"];
if (columns === "auto") { if (columns === "auto") {
return Math.round(this.$refs["grid-layout"].$el.scrollWidth / 300) return Math.round(this.$refs["grid-layout"].$el.scrollWidth / 300)
} }
return columns; return columns;
},
}, },
mounted() { },
this.width = this.$refs["grid-layout"].$el.scrollWidth / this.colCount; mounted() {
this.width = this.$refs["grid-layout"].$el.scrollWidth / this.colCount;
if (this.colCount === 1) { if (this.colCount === 1) {
this.$refs["grid-layout"].$el.classList.add("grid-single-column"); this.$refs["grid-layout"].$el.classList.add("grid-single-column");
} }
this.$store.subscribe((mutation) => { this.$store.subscribe((mutation) => {
if (mutation.type === "busUpdateWallItems" && this.$refs["grid-layout"]) { if (mutation.type === "busUpdateWallItems" && this.$refs["grid-layout"]) {
this.$refs["grid-layout"].updateItems(); this.$refs["grid-layout"].updateItems();
} }
}); });
}, },
}); });
</script> </script>

View File

@ -1,71 +1,63 @@
<template> <template>
<a :href="`f/${sid(doc)}`" <a :href="`f/${doc._source.index}/${doc._id}`" class="file-title-anchor" target="_blank">
:class="doc._source.embedding ? 'file-title-anchor-with-embedding' : 'file-title-anchor'" target="_blank"> <div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)"
<div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)" v-html="fileName() + ext(doc)"></div>
v-html="fileName() + ext(doc)"></div> </a>
</a>
</template> </template>
<script> <script>
import {ext, sid} from "@/util"; import {ext} from "@/util";
export default { export default {
name: "DocFileTitle", name: "DocFileTitle",
props: ["doc"], props: ["doc"],
methods: { methods: {
sid: sid, ext: ext,
ext: ext, fileName() {
fileName() { if (!this.doc.highlight) {
if (!this.doc.highlight) { return this.doc._source.name;
return this.doc._source.name; }
} if (this.doc.highlight["name.nGram"]) {
if (this.doc.highlight["name.nGram"]) { return this.doc.highlight["name.nGram"];
return this.doc.highlight["name.nGram"]; }
} if (this.doc.highlight.name) {
if (this.doc.highlight.name) { return this.doc.highlight.name;
return this.doc.highlight.name; }
} return this.doc._source.name;
return this.doc._source.name;
}
} }
}
} }
</script> </script>
<style scoped> <style scoped>
.file-title-anchor { .file-title-anchor {
max-width: calc(100% - 1.2rem); max-width: calc(100% - 1.2rem);
}
.file-title-anchor-with-embedding {
max-width: calc(100% - 2.2rem);
} }
.file-title { .file-title {
width: 100%; width: 100%;
max-width: 100%; line-height: 1rem;
line-height: 1rem; height: 1.1rem;
height: 1.1rem; white-space: nowrap;
white-space: nowrap; text-overflow: ellipsis;
text-overflow: ellipsis; overflow: hidden;
overflow: hidden; font-size: 16px;
font-size: 16px; font-family: "Source Sans Pro", sans-serif;
font-family: "Source Sans Pro", sans-serif; font-weight: bold;
font-weight: bold;
} }
.theme-black .file-title { .theme-black .file-title {
color: #ddd; color: #ddd;
} }
.theme-black .file-title:hover { .theme-black .file-title:hover {
color: #fff; color: #fff;
} }
.theme-light .file-title { .theme-light .file-title {
color: black; color: black;
} }
.doc-card .file-title { .doc-card .file-title {
font-size: 12px; font-size: 12px;
} }
</style> </style>

View File

@ -1,34 +1,33 @@
<template> <template>
<b-modal :visible="show" size="lg" :hide-footer="true" static lazy @close="$emit('close')" @hide="$emit('close')" <b-modal :visible="show" size="lg" :hide-footer="true" static lazy @close="$emit('close')" @hide="$emit('close')"
> >
<template #modal-title> <template #modal-title>
<h5 class="modal-title" :title="doc._source.name + ext(doc)"> <h5 class="modal-title" :title="doc._source.name + ext(doc)">
{{ doc._source.name + ext(doc) }} {{ doc._source.name + ext(doc) }}
<router-link :to="`/file?byId=${doc._id}`">#</router-link> <router-link :to="`/file?byId=${doc._id}`">#</router-link>
</h5> </h5>
</template> </template>
<img v-if="doc._props.hasThumbnail" :src="`t/${sid(doc)}`" alt="" class="fit card-img-top"> <img v-if="doc._props.hasThumbnail" :src="`t/${doc._source.index}/${doc._id}`" alt="" class="fit card-img-top">
<InfoTable :doc="doc"></InfoTable> <InfoTable :doc="doc"></InfoTable>
<LazyContentDiv :sid="sid(doc)"></LazyContentDiv> <LazyContentDiv :doc-id="doc._id"></LazyContentDiv>
</b-modal> </b-modal>
</template> </template>
<script> <script>
import {ext, sid} from "@/util"; import {ext} from "@/util";
import InfoTable from "@/components/InfoTable"; import InfoTable from "@/components/InfoTable";
import LazyContentDiv from "@/components/LazyContentDiv"; import LazyContentDiv from "@/components/LazyContentDiv";
export default { export default {
name: "DocInfoModal", name: "DocInfoModal",
components: {LazyContentDiv, InfoTable}, components: {LazyContentDiv, InfoTable},
props: ["doc", "show"], props: ["doc", "show"],
methods: { methods: {
ext: ext, ext: ext,
sid: sid }
}
} }
</script> </script>

View File

@ -1,49 +1,45 @@
<template> <template>
<b-list-group class="mt-3"> <b-list-group class="mt-3">
<DocListItem v-for="doc in docs" :key="sid(doc)" :doc="doc"></DocListItem> <DocListItem v-for="doc in docs" :key="doc._id" :doc="doc"></DocListItem>
</b-list-group> </b-list-group>
</template> </template>
<script> <script lang="ts">
import {sid} from "@/util";
import DocListItem from "@/components/DocListItem.vue"; import DocListItem from "@/components/DocListItem.vue";
import Vue from "vue"; import Vue from "vue";
export default Vue.extend({ export default Vue.extend({
name: "DocList", name: "DocList",
components: {DocListItem}, components: {DocListItem},
props: ["docs", "append"], props: ["docs", "append"],
mounted() { mounted() {
window.addEventListener("scroll", () => { window.addEventListener("scroll", () => {
const threshold = 400; const threshold = 400;
const app = document.getElementById("app"); const app = document.getElementById("app");
if ((window.innerHeight + window.scrollY) >= app.offsetHeight - threshold) { if ((window.innerHeight + window.scrollY) >= app.offsetHeight - threshold) {
this.append(); this.append();
} }
}); });
}, }
methods: {
sid: sid
}
}); });
</script> </script>
<style> <style>
.theme-black .list-group-item { .theme-black .list-group-item {
background: #212121; background: #212121;
color: #e0e0e0; color: #e0e0e0;
border-bottom: none; border-bottom: none;
border-left: none; border-left: none;
border-right: none; border-right: none;
border-radius: 0; border-radius: 0;
padding: .25rem 0.5rem; padding: .25rem 0.5rem;
} }
.theme-black .list-group-item:first-child { .theme-black .list-group-item:first-child {
border-top: none; border-top: none;
} }
</style> </style>

View File

@ -1,64 +1,63 @@
<template> <template>
<b-list-group-item class="flex-column align-items-start mb-2" :class="{'sub-document': doc._props.isSubDocument}" <b-list-group-item class="flex-column align-items-start mb-2" :class="{'sub-document': doc._props.isSubDocument}"
@mouseenter="onTnEnter()" @mouseleave="onTnLeave()"> @mouseenter="onTnEnter()" @mouseleave="onTnLeave()">
<!-- Info modal--> <!-- Info modal-->
<DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal> <DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal>
<div class="media ml-2"> <div class="media ml-2">
<!-- Thumbnail--> <!-- Thumbnail-->
<div v-if="doc._props.hasThumbnail" class="align-self-start mr-2 wrapper-sm"> <div v-if="doc._props.hasThumbnail" class="align-self-start mr-2 wrapper-sm">
<div class="img-wrapper"> <div class="img-wrapper">
<div v-if="doc._props.isPlayableVideo" class="play"> <div v-if="doc._props.isPlayableVideo" class="play">
<svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg">
<path d="m35.353 0 424.236 247.471-424.236 247.471z"/> <path d="m35.353 0 424.236 247.471-424.236 247.471z"/>
</svg> </svg>
</div> </div>
<img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo" <img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
:src="(doc._props.isGif && hover) ? `f/${sid(doc)}` : `t/${sid(doc)}`" :src="(doc._props.isGif && hover) ? `f/${doc._source.index}/${doc._id}` : `t/${doc._source.index}/${doc._id}`"
alt="" alt=""
class="pointer fit-sm" @click="onThumbnailClick()"> class="pointer fit-sm" @click="onThumbnailClick()">
<img v-else :src="`t/${sid(doc)}`" alt="" <img v-else :src="`t/${doc._source.index}/${doc._id}`" alt=""
class="fit-sm"> class="fit-sm">
</div> </div>
</div> </div>
<div v-else class="file-icon-wrapper" style=""> <div v-else class="file-icon-wrapper" style="">
<FileIcon></FileIcon> <FileIcon></FileIcon>
</div> </div>
<!-- Doc line--> <!-- Doc line-->
<div class="doc-line ml-3"> <div class="doc-line ml-3">
<div style="display: flex"> <div style="display: flex">
<span class="info-icon" @click="showInfo = true"></span> <span class="info-icon" @click="showInfo = true"></span>
<MLIcon v-if="doc._source.embedding" clickable @click="onEmbeddingClick()"></MLIcon> <DocFileTitle :doc="doc"></DocFileTitle>
<DocFileTitle :doc="doc"></DocFileTitle> </div>
</div>
<!-- Content highlight --> <!-- Content highlight -->
<ContentDiv :doc="doc"></ContentDiv> <ContentDiv :doc="doc"></ContentDiv>
<div class="path-row"> <div class="path-row">
<div class="path-line" v-html="path()"></div> <div class="path-line" v-html="path()"></div>
<TagContainer :hit="doc"></TagContainer> <TagContainer :hit="doc"></TagContainer>
</div> </div>
<div v-if="doc._source.pages || doc._source.author" class="path-row text-muted"> <div v-if="doc._source.pages || doc._source.author" class="path-row text-muted">
<span v-if="doc._source.pages">{{ doc._source.pages }} {{ <span v-if="doc._source.pages">{{ doc._source.pages }} {{
doc._source.pages > 1 ? $t("pages") : $t("page") doc._source.pages > 1 ? $t("pages") : $t("page")
}}</span> }}</span>
<span v-if="doc._source.author && doc._source.pages" class="mx-1">-</span> <span v-if="doc._source.author && doc._source.pages" class="mx-1">-</span>
<span v-if="doc._source.author">{{ doc._source.author }}</span> <span v-if="doc._source.author">{{ doc._source.author }}</span>
</div>
<!-- Featured line -->
<div style="display: flex">
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
</div>
</div>
</div> </div>
</b-list-group-item>
<!-- Featured line -->
<div style="display: flex">
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
</div>
</div>
</div>
</b-list-group-item>
</template> </template>
<script> <script>
@ -68,142 +67,131 @@ import DocInfoModal from "@/components/DocInfoModal";
import ContentDiv from "@/components/ContentDiv"; import ContentDiv from "@/components/ContentDiv";
import FileIcon from "@/components/icons/FileIcon"; import FileIcon from "@/components/icons/FileIcon";
import FeaturedFieldsLine from "@/components/FeaturedFieldsLine"; import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
import MLIcon from "@/components/icons/MlIcon.vue";
import Sist2Api from "@/Sist2Api";
import {sid} from "@/util";
export default { export default {
name: "DocListItem", name: "DocListItem",
components: {MLIcon, FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine}, components: {FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine},
props: ["doc"], props: ["doc"],
data() { data() {
return { return {
hover: false, hover: false,
showInfo: false showInfo: false
}
},
methods: {
sid: sid,
async onThumbnailClick() {
this.$store.commit("setUiLightboxSlide", this.doc._seq);
await this.$store.dispatch("showLightbox");
},
onEmbeddingClick() {
Sist2Api.getEmbeddings(sid(this.doc), this.$store.state.embeddingsModel).then(embeddings => {
this.$store.commit("setEmbeddingText", "");
this.$store.commit("setEmbedding", embeddings);
this.$store.commit("setEmbeddingDoc", this.doc);
})
},
path() {
if (!this.doc.highlight) {
return this.doc._source.path + "/"
}
if (this.doc.highlight["path.text"]) {
return this.doc.highlight["path.text"] + "/"
}
if (this.doc.highlight["path.nGram"]) {
return this.doc.highlight["path.nGram"] + "/"
}
return this.doc._source.path + "/"
},
onTnEnter() {
this.hover = true;
},
onTnLeave() {
this.hover = false;
},
} }
},
methods: {
async onThumbnailClick() {
this.$store.commit("setUiLightboxSlide", this.doc._seq);
await this.$store.dispatch("showLightbox");
},
path() {
if (!this.doc.highlight) {
return this.doc._source.path + "/"
}
if (this.doc.highlight["path.text"]) {
return this.doc.highlight["path.text"] + "/"
}
if (this.doc.highlight["path.nGram"]) {
return this.doc.highlight["path.nGram"] + "/"
}
return this.doc._source.path + "/"
},
onTnEnter() {
this.hover = true;
},
onTnLeave() {
this.hover = false;
},
}
} }
</script> </script>
<style scoped> <style scoped>
.sub-document { .sub-document {
background: #AB47BC1F !important; background: #AB47BC1F !important;
} }
.theme-black .sub-document { .theme-black .sub-document {
background: #37474F !important; background: #37474F !important;
} }
.list-group { .list-group {
margin-top: 1em; margin-top: 1em;
} }
.list-group-item { .list-group-item {
padding: .25rem 0.5rem; padding: .25rem 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important; box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important;
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
.path-row { .path-row {
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-ms-flex-align: start; -ms-flex-align: start;
align-items: flex-start; align-items: flex-start;
} }
.path-line { .path-line {
color: #808080; color: #808080;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
margin-right: 0.3em; margin-right: 0.3em;
} }
.theme-black .path-line { .theme-black .path-line {
color: #bbb; color: #bbb;
} }
.play { .play {
position: absolute; position: absolute;
width: 18px; width: 18px;
height: 18px; height: 18px;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
pointer-events: none; pointer-events: none;
} }
.play svg { .play svg {
fill: rgba(0, 0, 0, 0.7); fill: rgba(0, 0, 0, 0.7);
} }
.list-group-item .img-wrapper { .list-group-item .img-wrapper {
width: 88px; width: 88px;
height: 88px; height: 88px;
position: relative; position: relative;
} }
.fit-sm { .fit-sm {
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
width: auto; width: auto;
height: auto; height: auto;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
margin: auto; margin: auto;
/*box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.12);*/ /*box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.12);*/
} }
.doc-line { .doc-line {
max-width: calc(100% - 88px - 1.5rem); max-width: calc(100% - 88px - 1.5rem);
flex: 1; flex: 1;
vertical-align: middle; vertical-align: middle;
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
} }
.file-icon-wrapper { .file-icon-wrapper {
width: calc(88px + .5rem); width: calc(88px + .5rem);
height: 88px; height: 88px;
position: relative; position: relative;
} }
</style> </style>

View File

@ -1,155 +0,0 @@
<template>
<div>
<b-progress v-if="modelLoading && [0, 1].includes(modelLoadingProgress)" max="1" class="mb-1" variant="primary"
striped animated :value="1">
</b-progress>
<b-progress v-else-if="modelLoading" :value="modelLoadingProgress" max="1" class="mb-1" variant="warning"
show-progress>
</b-progress>
<div style="display: flex">
<b-select :options="modelOptions()" class="mr-2 input-prepend" :value="modelName"
@change="onModelChange($event)"></b-select>
<b-input-group>
<b-form-input :value="embeddingText"
:placeholder="$store.state.embeddingDoc ? ' ' : $t('embeddingsSearchPlaceholder')"
@input="onInput($event)"
:disabled="modelLoading"
:style="{'pointer-events': $store.state.embeddingDoc ? 'none' : undefined}"
></b-form-input>
<b-badge v-if="$store.state.embeddingDoc" pill variant="primary" class="overlay-badge" href="#"
@click="onBadgeClick()">{{ docName }}
</b-badge>
<template #prepend>
</template>
<template #append>
<b-input-group-text>
<MLIcon class="ml-append" big></MLIcon>
</b-input-group-text>
</template>
</b-input-group>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations} from "vuex";
import {CLIPTransformerModel} from "@/ml/CLIPTransformerModel"
import _debounce from "lodash/debounce";
import MLIcon from "@/components/icons/MlIcon.vue";
import Sist2AdminApi from "@/Sist2Api";
export default {
components: {MLIcon},
data() {
return {
modelLoading: false,
modelLoadingProgress: 0,
modelLoaded: false,
model: null,
modelName: null
}
},
computed: {
...mapGetters({
optQueryMode: "optQueryMode",
embeddingText: "embeddingText",
fuzzy: "fuzzy",
}),
docName() {
const ext = this.$store.state.embeddingDoc._source.extension;
return this.$store.state.embeddingDoc._source.name +
(ext ? "." + ext : "")
}
},
mounted() {
// Set default model
this.modelName = Sist2AdminApi.models()[0].name;
this.onModelChange(this.modelName);
this.onInput = _debounce(this._onInput, 450, {leading: false});
},
methods: {
...mapMutations({
setEmbeddingText: "setEmbeddingText",
setEmbedding: "setEmbedding",
setEmbeddingModel: "setEmbeddingsModel",
}),
async loadModel() {
this.modelLoading = true;
await this.model.init(async progress => {
this.modelLoadingProgress = progress;
});
this.modelLoading = false;
this.modelLoaded = true;
},
async _onInput(text) {
try {
if (!this.modelLoaded) {
await this.loadModel();
}
if (text.length === 0) {
this.setEmbeddingText("");
this.setEmbedding(null);
return;
}
const embeddings = await this.model.predict(text);
this.setEmbeddingText(text);
this.setEmbedding(embeddings);
} catch (e) {
alert(e)
}
},
modelOptions() {
return Sist2AdminApi.models().map(model => model.name);
},
onModelChange(name) {
this.modelLoaded = false;
this.modelLoadingProgress = 0;
const modelInfo = Sist2AdminApi.models().find(m => m.name === name);
if (modelInfo.name === "CLIP") {
const tokenizerUrl = new URL("./tokenizer.json", modelInfo.url).href;
this.model = new CLIPTransformerModel(modelInfo.url, tokenizerUrl)
this.setEmbeddingModel(modelInfo.id);
} else {
throw new Error("Unknown model: " + name);
}
},
onBadgeClick() {
this.$store.commit("setEmbedding", null);
this.$store.commit("setEmbeddingDoc", null);
}
}
}
</script>
<style>
.overlay-badge {
position: absolute;
z-index: 1;
left: 0.375rem;
top: 8px;
line-height: 1.1rem;
overflow: hidden;
max-width: 200px;
text-overflow: ellipsis;
}
.input-prepend {
max-width: 100px;
}
.theme-black .ml-append {
filter: brightness(0.95) !important;
}
</style>

View File

@ -1,189 +1,188 @@
<template> <template>
<div v-if="doc._props.hasThumbnail" class="img-wrapper" @mouseenter="onTnEnter()" @mouseleave="onTnLeave()" <div v-if="doc._props.hasThumbnail" class="img-wrapper" @mouseenter="onTnEnter()" @mouseleave="onTnLeave()"
@touchstart="onTouchStart()"> @touchstart="onTouchStart()">
<div v-if="doc._props.isAudio" class="card-img-overlay" :class="{'small-badge': smallBadge}"> <div v-if="doc._props.isAudio" class="card-img-overlay" :class="{'small-badge': smallBadge}">
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span> <span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
</div>
<div
v-if="doc._props.isImage && doc._props.imageAspectRatio < 5"
class="card-img-overlay"
:class="{'small-badge': smallBadge}">
<span class="badge badge-resolution">{{ `${doc._source.width}x${doc._source.height}` }}</span>
</div>
<div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0"
class="card-img-overlay"
:class="{'small-badge': smallBadge}">
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
</div>
<div v-if="doc._props.isPlayableVideo" class="play">
<svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg">
<path d="m35.353 0 424.236 247.471-424.236 247.471z"/>
</svg>
</div>
<img ref="tn"
v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
:src="tnSrc"
alt=""
:style="{height: (doc._props.isGif && hover) ? `${tnHeight()}px` : undefined}"
class="pointer fit card-img-top" @click="onThumbnailClick()">
<img v-else :src="tnSrc" alt=""
class="fit card-img-top">
<ThumbnailProgressBar v-if="hover && doc._props.hasVidPreview"
:progress="(currentThumbnailNum + 1) / (doc._props.tnNum)"
></ThumbnailProgressBar>
</div> </div>
<div
v-if="doc._props.isImage && doc._props.imageAspectRatio < 5"
class="card-img-overlay"
:class="{'small-badge': smallBadge}">
<span class="badge badge-resolution">{{ `${doc._source.width}x${doc._source.height}` }}</span>
</div>
<div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0"
class="card-img-overlay"
:class="{'small-badge': smallBadge}">
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
</div>
<div v-if="doc._props.isPlayableVideo" class="play">
<svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg">
<path d="m35.353 0 424.236 247.471-424.236 247.471z"/>
</svg>
</div>
<img ref="tn"
v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo"
:src="tnSrc"
alt=""
:style="{height: (doc._props.isGif && hover) ? `${tnHeight()}px` : undefined}"
class="pointer fit card-img-top" @click="onThumbnailClick()">
<img v-else :src="tnSrc" alt=""
class="fit card-img-top">
<ThumbnailProgressBar v-if="hover && doc._props.hasVidPreview"
:progress="(currentThumbnailNum + 1) / (doc._props.tnNum)"
></ThumbnailProgressBar>
</div>
</template> </template>
<script> <script>
import {humanTime, sid} from "@/util"; import {humanTime} from "@/util";
import ThumbnailProgressBar from "@/components/ThumbnailProgressBar"; import ThumbnailProgressBar from "@/components/ThumbnailProgressBar";
export default { export default {
name: "FullThumbnail", name: "FullThumbnail",
props: ["doc", "smallBadge"], props: ["doc", "smallBadge"],
components: {ThumbnailProgressBar}, components: {ThumbnailProgressBar},
data() { data() {
return { return {
hover: false, hover: false,
currentThumbnailNum: 0, currentThumbnailNum: 0,
timeoutId: null timeoutId: null
}
},
created() {
this.$store.subscribe((mutation) => {
if (mutation.type === "busTnTouchStart" && mutation.payload !== this.doc._id) {
this.onTnLeave();
}
});
},
computed: {
tnSrc() {
return this.getThumbnailSrc(this.currentThumbnailNum);
},
},
methods: {
sid: sid,
getThumbnailSrc(thumbnailNum) {
const doc = this.doc;
const props = doc._props;
if (props.isGif && this.hover) {
return `f/${sid(doc)}`;
}
return (this.currentThumbnailNum === 0)
? `t/${sid(doc)}`
: `t/${sid(doc)}/${String(thumbnailNum).padStart(4, "0")}`;
},
humanTime: humanTime,
onThumbnailClick() {
this.$emit("onThumbnailClick");
},
tnHeight() {
return this.$refs.tn.height;
},
tnWidth() {
return this.$refs.tn.width;
},
onTnEnter() {
this.hover = true;
const start = Date.now()
if (this.doc._props.hasVidPreview) {
let img = new Image();
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
img.onload = () => {
this.currentThumbnailNum += 1;
this.scheduleNextTnNum(Date.now() - start);
}
}
},
onTnLeave() {
this.currentThumbnailNum = 0;
this.hover = false;
if (this.timeoutId !== null) {
window.clearTimeout(this.timeoutId);
this.timeoutId = null;
}
},
scheduleNextTnNum(offset = 0) {
const INTERVAL = (this.$store.state.optVidPreviewInterval ?? 700) - offset;
this.timeoutId = window.setTimeout(() => {
const start = Date.now();
if (!this.hover) {
return;
}
if (this.currentThumbnailNum === this.doc._props.tnNum - 1) {
this.currentThumbnailNum = 0;
this.scheduleNextTnNum();
} else {
let img = new Image();
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
img.onload = () => {
this.currentThumbnailNum += 1;
this.scheduleNextTnNum(Date.now() - start);
}
}
}, INTERVAL);
},
onTouchStart() {
this.$store.commit("busTnTouchStart", this.doc._id);
if (!this.hover) {
this.onTnEnter()
}
},
} }
},
created() {
this.$store.subscribe((mutation) => {
if (mutation.type === "busTnTouchStart" && mutation.payload !== this.doc._id) {
this.onTnLeave();
}
});
},
computed: {
tnSrc() {
return this.getThumbnailSrc(this.currentThumbnailNum);
},
},
methods: {
getThumbnailSrc(thumbnailNum) {
const doc = this.doc;
const props = doc._props;
if (props.isGif && this.hover) {
return `f/${doc._source.index}/${doc._id}`;
}
return (this.currentThumbnailNum === 0)
? `t/${doc._source.index}/${doc._id}`
: `t/${doc._source.index}/${doc._id}/${String(thumbnailNum).padStart(4, "0")}`;
},
humanTime: humanTime,
onThumbnailClick() {
this.$emit("onThumbnailClick");
},
tnHeight() {
return this.$refs.tn.height;
},
tnWidth() {
return this.$refs.tn.width;
},
onTnEnter() {
this.hover = true;
const start = Date.now()
if (this.doc._props.hasVidPreview) {
let img = new Image();
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
img.onload = () => {
this.currentThumbnailNum += 1;
this.scheduleNextTnNum(Date.now() - start);
}
}
},
onTnLeave() {
this.currentThumbnailNum = 0;
this.hover = false;
if (this.timeoutId !== null) {
window.clearTimeout(this.timeoutId);
this.timeoutId = null;
}
},
scheduleNextTnNum(offset = 0) {
const INTERVAL = (this.$store.state.optVidPreviewInterval ?? 700) - offset;
this.timeoutId = window.setTimeout(() => {
const start = Date.now();
if (!this.hover) {
return;
}
if (this.currentThumbnailNum === this.doc._props.tnNum - 1) {
this.currentThumbnailNum = 0;
this.scheduleNextTnNum();
} else {
let img = new Image();
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
img.onload = () => {
this.currentThumbnailNum += 1;
this.scheduleNextTnNum(Date.now() - start);
}
}
}, INTERVAL);
},
onTouchStart() {
this.$store.commit("busTnTouchStart", this.doc._id);
if (!this.hover) {
this.onTnEnter()
}
},
}
} }
</script> </script>
<style scoped> <style scoped>
.img-wrapper { .img-wrapper {
position: relative; position: relative;
} }
.img-wrapper:hover svg { .img-wrapper:hover svg {
fill: rgba(0, 0, 0, 1); fill: rgba(0, 0, 0, 1);
} }
.card-img-top { .card-img-top {
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.play { .play {
position: absolute; position: absolute;
width: 25px; width: 25px;
height: 25px; height: 25px;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
pointer-events: none; pointer-events: none;
} }
.play svg { .play svg {
fill: rgba(0, 0, 0, 0.7); fill: rgba(0, 0, 0, 0.7);
} }
.badge-resolution { .badge-resolution {
color: #c6c6c6; color: #c6c6c6;
background-color: #272727CC; background-color: #272727CC;
padding: 2px 3px; padding: 2px 3px;
} }
.card-img-overlay { .card-img-overlay {
pointer-events: none; pointer-events: none;
padding: 2px 6px; padding: 2px 6px;
bottom: 4px; bottom: 4px;
top: unset; top: unset;
left: unset; left: unset;
right: 0; right: 0;
} }
.small-badge { .small-badge {
padding: 1px 3px; padding: 1px 3px;
font-size: 70%; font-size: 70%;
} }
</style> </style>

View File

@ -1,201 +1,191 @@
<template> <template>
<div v-if="isMobile"> <div v-if="isMobile">
<b-form-select <b-form-select
:value="selectedIndicesIds" :value="selectedIndicesIds"
@change="onSelect($event)" @change="onSelect($event)"
:options="indices" multiple :select-size="6" text-field="name" :options="indices" multiple :select-size="6" text-field="name"
value-field="id"></b-form-select> value-field="id"></b-form-select>
</div> </div>
<div v-else> <div v-else>
<div class="d-flex justify-content-between align-content-center"> <div class="d-flex justify-content-between align-content-center">
<span> <span>
{{ selectedIndices.length }} {{ selectedIndices.length }}
{{ selectedIndices.length === 1 ? $t("indexPicker.selectedIndex") : $t("indexPicker.selectedIndices") }} {{ selectedIndices.length === 1 ? $t("indexPicker.selectedIndex") : $t("indexPicker.selectedIndices") }}
</span> </span>
<div> <div>
<b-button variant="link" @click="selectAll()"> {{ $t("indexPicker.selectAll") }}</b-button> <b-button variant="link" @click="selectAll()"> {{ $t("indexPicker.selectAll") }}</b-button>
<b-button variant="link" @click="selectNone()"> {{ $t("indexPicker.selectNone") }}</b-button> <b-button variant="link" @click="selectNone()"> {{ $t("indexPicker.selectNone") }}</b-button>
</div> </div>
</div>
<b-list-group id="index-picker-desktop" class="unselectable">
<b-list-group-item
v-for="idx in indices"
@click="toggleIndex(idx, $event)"
@click.shift="shiftClick(idx, $event)"
class="d-flex justify-content-between align-items-center list-group-item-action pointer"
:class="{active: lastClickIndex === idx}"
:key="idx.id"
>
<div class="d-flex">
<b-checkbox style="pointer-events: none" :checked="isSelected(idx)"></b-checkbox>
{{ idx.name }}
<div v-if="hasEmbeddings(idx)" style="vertical-align: center; margin-left: 5px">
<MLIcon small style="top: -1px; position: relative"></MLIcon>
</div>
<span class="text-muted timestamp-text ml-2"
style="top: 1px; position: relative">{{ formatIdxDate(idx.timestamp) }}</span>
</div>
<b-badge class="version-badge">v{{ idx.version }}</b-badge>
</b-list-group-item>
</b-list-group>
</div> </div>
<b-list-group id="index-picker-desktop" class="unselectable">
<b-list-group-item
v-for="idx in indices"
@click="toggleIndex(idx, $event)"
@click.shift="shiftClick(idx, $event)"
class="d-flex justify-content-between align-items-center list-group-item-action pointer"
:class="{active: lastClickIndex === idx}"
>
<div class="d-flex">
<b-checkbox style="pointer-events: none" :checked="isSelected(idx)"></b-checkbox>
{{ idx.name }}
<span class="text-muted timestamp-text ml-2">{{ formatIdxDate(idx.timestamp) }}</span>
</div>
<b-badge class="version-badge">v{{ idx.version }}</b-badge>
</b-list-group-item>
</b-list-group>
</div>
</template> </template>
<script> <script lang="ts">
import SmallBadge from "./SmallBadge.vue" import SmallBadge from "./SmallBadge.vue"
import {mapActions, mapGetters} from "vuex"; import {mapActions, mapGetters} from "vuex";
import Vue from "vue"; import Vue from "vue";
import {format} from "date-fns"; import {format} from "date-fns";
import MLIcon from "@/components/icons/MlIcon.vue";
export default Vue.extend({ export default Vue.extend({
components: { components: {
MLIcon, SmallBadge
SmallBadge },
data() {
return {
loading: true,
lastClickIndex: null
}
},
computed: {
...mapGetters([
"indices", "selectedIndices"
]),
selectedIndicesIds() {
return this.selectedIndices.map(idx => idx.id)
}, },
data() { isMobile() {
return { return window.innerWidth <= 650;
loading: true, }
lastClickIndex: null },
methods: {
...mapActions({
setSelectedIndices: "setSelectedIndices"
}),
shiftClick(index, e) {
if (this.lastClickIndex === null) {
return;
}
const select = this.isSelected(this.lastClickIndex);
let leftBoundary = this.indices.indexOf(this.lastClickIndex);
let rightBoundary = this.indices.indexOf(index);
if (rightBoundary < leftBoundary) {
let tmp = leftBoundary;
leftBoundary = rightBoundary;
rightBoundary = tmp;
}
for (let i = leftBoundary; i <= rightBoundary; i++) {
if (select) {
if (!this.isSelected(this.indices[i])) {
this.setSelectedIndices([this.indices[i], ...this.selectedIndices]);
}
} else {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx !== this.indices[i]));
} }
}
}, },
computed: { selectAll() {
...mapGetters([ this.setSelectedIndices(this.indices);
"indices", "selectedIndices"
]),
selectedIndicesIds() {
return this.selectedIndices.map(idx => idx.id)
},
isMobile() {
return window.innerWidth <= 650;
}
}, },
methods: { selectNone() {
...mapActions({ this.setSelectedIndices([]);
setSelectedIndices: "setSelectedIndices"
}),
shiftClick(index, e) {
if (this.lastClickIndex === null) {
return;
}
const select = this.isSelected(this.lastClickIndex);
let leftBoundary = this.indices.indexOf(this.lastClickIndex);
let rightBoundary = this.indices.indexOf(index);
if (rightBoundary < leftBoundary) {
let tmp = leftBoundary;
leftBoundary = rightBoundary;
rightBoundary = tmp;
}
for (let i = leftBoundary; i <= rightBoundary; i++) {
if (select) {
if (!this.isSelected(this.indices[i])) {
this.setSelectedIndices([this.indices[i], ...this.selectedIndices]);
}
} else {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx !== this.indices[i]));
}
}
},
selectAll() {
this.setSelectedIndices(this.indices);
},
selectNone() {
this.setSelectedIndices([]);
},
onSelect(value) {
this.setSelectedIndices(this.indices.filter(idx => value.includes(idx.id)));
},
formatIdxDate(timestamp) {
return format(new Date(timestamp * 1000), "yyyy-MM-dd");
},
toggleIndex(index, e) {
if (e.shiftKey) {
return;
}
this.lastClickIndex = index;
if (this.isSelected(index)) {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx.id !== index.id));
} else {
this.setSelectedIndices([index, ...this.selectedIndices]);
}
},
isSelected(index) {
return this.selectedIndices.find(idx => idx.id === index.id) != null;
},
hasEmbeddings(index) {
return index.models.length > 0;
},
}, },
onSelect(value) {
this.setSelectedIndices(this.indices.filter(idx => value.includes(idx.id)));
},
formatIdxDate(timestamp: number): string {
return format(new Date(timestamp * 1000), "yyyy-MM-dd");
},
toggleIndex(index, e) {
if (e.shiftKey) {
return;
}
this.lastClickIndex = index;
if (this.isSelected(index)) {
this.setSelectedIndices(this.selectedIndices.filter(idx => idx.id != index.id));
} else {
this.setSelectedIndices([index, ...this.selectedIndices]);
}
},
isSelected(index) {
return this.selectedIndices.find(idx => idx.id == index.id) != null;
}
},
}) })
</script> </script>
<style scoped> <style scoped>
.timestamp-text { .timestamp-text {
line-height: 24px; line-height: 24px;
font-size: 80%; font-size: 80%;
} }
.theme-black .version-badge { .theme-black .version-badge {
color: #eee !important; color: #eee !important;
background: none; background: none;
} }
.version-badge { .version-badge {
color: #222 !important; color: #222 !important;
background: none; background: none;
} }
.list-group-item { .list-group-item {
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
} }
#index-picker-desktop { #index-picker-desktop {
overflow-y: auto; overflow-y: auto;
max-height: 132px; max-height: 132px;
} }
.btn-link:focus { .btn-link:focus {
box-shadow: none; box-shadow: none;
} }
.unselectable { .unselectable {
user-select: none; user-select: none;
-ms-user-select: none; -ms-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
} }
.list-group-item.active { .list-group-item.active {
z-index: 2; z-index: 2;
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
} }
.theme-black .list-group-item { .theme-black .list-group-item {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255,255,255, 0.1);
} }
.theme-black .list-group-item:first-child { .theme-black .list-group-item:first-child {
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255,255,255, 0.05);
} }
.theme-black .list-group-item.active { .theme-black .list-group-item.active {
z-index: 2; z-index: 2;
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255,255,255, 0.3);
border-radius: 0; border-radius: 0;
} }
.theme-black .list-group { .theme-black .list-group {
border-radius: 0; border-radius: 0;
} }
</style> </style>

View File

@ -59,7 +59,7 @@ export default {
const fields = [ const fields = [
"title", "duration", "audioc", "videoc", "title", "duration", "audioc", "videoc",
"bitrate", "artist", "album", "album_artist", "genre", "font_name", "author", "media_comment", "bitrate", "artist", "album", "album_artist", "genre", "font_name", "author",
"modified_by", "pages", "tag", "modified_by", "pages", "tag",
"exif_make", "exif_software", "exif_exposure_time", "exif_fnumber", "exif_focal_length", "exif_make", "exif_software", "exif_exposure_time", "exif_fnumber", "exif_focal_length",
"exif_user_comment", "exif_iso_speed_ratings", "exif_model", "exif_datetime", "exif_user_comment", "exif_iso_speed_ratings", "exif_model", "exif_datetime",

View File

@ -9,8 +9,8 @@
<b-button :disabled="mlPredictionsLoading || mlLoading" @click="mlAnalyze" variant="primary" <b-button :disabled="mlPredictionsLoading || mlLoading" @click="mlAnalyze" variant="primary"
>{{ $t("ml.analyzeText") }} >{{ $t("ml.analyzeText") }}
</b-button> </b-button>
<b-select :disabled="mlPredictionsLoading || mlLoading" class="ml-2" v-model="nerModel"> <b-select :disabled="mlPredictionsLoading || mlLoading" class="ml-2" v-model="mlModel">
<b-select-option :value="opt.value" v-for="opt of ModelsRepo.getOptions()" :key="opt.value">{{ opt.text }} <b-select-option :value="opt.value" v-for="opt of ModelsRepo.getOptions()">{{ opt.text }}
</b-select-option> </b-select-option>
</b-select> </b-select>
</b-form> </b-form>
@ -46,7 +46,7 @@ import {mapGetters, mapMutations} from "vuex";
export default { export default {
name: "LazyContentDiv", name: "LazyContentDiv",
components: {AnalyzedContentSpansContainer, Preloader}, components: {AnalyzedContentSpansContainer, Preloader},
props: ["sid"], props: ["docId"],
data() { data() {
return { return {
ModelsRepo, ModelsRepo,
@ -57,20 +57,20 @@ export default {
modelPredictionProgress: 0, modelPredictionProgress: 0,
mlPredictionsLoading: false, mlPredictionsLoading: false,
mlLoading: false, mlLoading: false,
nerModel: null, mlModel: null,
analyzedContentSpans: [] analyzedContentSpans: []
} }
}, },
mounted() { mounted() {
if (this.$store.getters.optMlDefaultModel) { if (this.$store.getters.optMlDefaultModel) {
this.nerModel = this.$store.getters.optMlDefaultModel this.mlModel = this.$store.getters.optMlDefaultModel
} else { } else {
this.nerModel = ModelsRepo.getDefaultModel(); this.mlModel = ModelsRepo.getDefaultModel();
} }
Sist2Api Sist2Api
.getDocument(this.sid, this.$store.state.optHighlight, this.$store.state.fuzzy) .getDocument(this.docId, this.$store.state.optHighlight, this.$store.state.fuzzy)
.then(doc => { .then(doc => {
this.loading = false; this.loading = false;
@ -86,7 +86,7 @@ export default {
computed: { computed: {
...mapGetters(["optAutoAnalyze"]), ...mapGetters(["optAutoAnalyze"]),
modelSize() { modelSize() {
const modelData = ModelsRepo.data[this.nerModel]; const modelData = ModelsRepo.data[this.mlModel];
if (!modelData) { if (!modelData) {
return 0; return 0;
} }
@ -110,10 +110,10 @@ export default {
} }
}, },
async getMlModel() { async getMlModel() {
if (this.$store.getters.nerModel.name !== this.nerModel) { if (this.$store.getters.mlModel.name !== this.mlModel) {
this.mlLoading = true; this.mlLoading = true;
this.modelLoadingProgress = 0; this.modelLoadingProgress = 0;
const modelInfo = ModelsRepo.data[this.nerModel]; const modelInfo = ModelsRepo.data[this.mlModel];
const model = new BertNerModel( const model = new BertNerModel(
modelInfo.vocabUrl, modelInfo.vocabUrl,
@ -122,25 +122,25 @@ export default {
) )
await model.init(progress => this.modelLoadingProgress = progress); await model.init(progress => this.modelLoadingProgress = progress);
this.$store.commit("setNerModel", {model, name: this.nerModel}); this.$store.commit("setMlModel", {model, name: this.mlModel});
this.mlLoading = false; this.mlLoading = false;
return model return model
} }
return this.$store.getters.nerModel.model; return this.$store.getters.mlModel.model;
}, },
async mlAnalyze() { async mlAnalyze() {
if (!this.content) { if (!this.content) {
return; return;
} }
const modelInfo = ModelsRepo.data[this.nerModel]; const modelInfo = ModelsRepo.data[this.mlModel];
if (modelInfo === undefined) { if (modelInfo === undefined) {
return; return;
} }
this.$store.commit("setOptMlDefaultModel", this.nerModel); this.$store.commit("setOptMlDefaultModel", this.mlModel);
await this.$store.dispatch("updateConfiguration"); await this.$store.dispatch("updateConfiguration");
const model = await this.getMlModel(); const model = await this.getMlModel();

View File

@ -77,7 +77,6 @@ export default {
return listener(e); return listener(e);
} }
}; };
}, },
methods: { methods: {
keyDownListener(e) { keyDownListener(e) {

View File

@ -9,7 +9,7 @@
<span class="badge badge-pill version" v-if="$store && $store.state.sist2Info"> <span class="badge badge-pill version" v-if="$store && $store.state.sist2Info">
v{{ sist2Version() }}<span v-if="isDebug()">-dbg</span><span v-if="isLegacy() && !hideLegacy()">-<a v{{ sist2Version() }}<span v-if="isDebug()">-dbg</span><span v-if="isLegacy() && !hideLegacy()">-<a
href="https://github.com/sist2app/sist2/blob/master/docs/USAGE.md#elasticsearch" href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch"
target="_blank">legacyES</a></span><span v-if="$store.state.uiSqliteMode">-SQLite</span> target="_blank">legacyES</a></span><span v-if="$store.state.uiSqliteMode">-SQLite</span>
</span> </span>

View File

@ -43,7 +43,8 @@
</b-card> </b-card>
</template> </template>
<script> <script lang="ts">
import Sist2Api, {EsResult} from "@/Sist2Api";
import Vue from "vue"; import Vue from "vue";
import {humanFileSize} from "@/util"; import {humanFileSize} from "@/util";
import DisplayModeToggle from "@/components/DisplayModeToggle.vue"; import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
@ -51,7 +52,6 @@ import SortSelect from "@/components/SortSelect.vue";
import Preloader from "@/components/Preloader.vue"; import Preloader from "@/components/Preloader.vue";
import Sist2Query from "@/Sist2ElasticsearchQuery"; import Sist2Query from "@/Sist2ElasticsearchQuery";
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue"; import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
import Sist2Api from "@/Sist2Api";
export default Vue.extend({ export default Vue.extend({
name: "ResultsCard", name: "ResultsCard",
@ -64,7 +64,7 @@ export default Vue.extend({
return this.$store.state.lastQueryResults != null; return this.$store.state.lastQueryResults != null;
}, },
hitCount() { hitCount() {
return (this.$store.state.firstQueryResults).aggregations.total_count.value; return (this.$store.state.firstQueryResults as EsResult).aggregations.total_count.value;
}, },
tableItems() { tableItems() {
const items = []; const items = [];
@ -79,10 +79,10 @@ export default Vue.extend({
}, },
methods: { methods: {
took() { took() {
return (this.$store.state.lastQueryResults).took + "ms"; return (this.$store.state.lastQueryResults as EsResult).took + "ms";
}, },
totalSize() { totalSize() {
return humanFileSize((this.$store.state.firstQueryResults).aggregations.total_size.value); return humanFileSize((this.$store.state.firstQueryResults as EsResult).aggregations.total_size.value);
}, },
onToggle() { onToggle() {
const show = !document.getElementById("collapse-1").classList.contains("show"); const show = !document.getElementById("collapse-1").classList.contains("show");

View File

@ -5,7 +5,8 @@
<script> <script>
import noUiSlider from 'nouislider'; import noUiSlider from 'nouislider';
import 'nouislider/dist/nouislider.css'; import 'nouislider/dist/nouislider.css';
import {humanFileSize, mergeTooltips} from "@/util"; import {humanFileSize} from "@/util";
import {mergeTooltips} from "@/util-js";
export default { export default {
name: "SizeSlider", name: "SizeSlider",

View File

@ -2,7 +2,7 @@
<b-badge variant="secondary" :pill="pill">{{ text }}</b-badge> <b-badge variant="secondary" :pill="pill">{{ text }}</b-badge>
</template> </template>
<script> <script lang="ts">
import Vue from "vue"; import Vue from "vue";
export default Vue.extend({ export default Vue.extend({

View File

@ -1,5 +1,5 @@
<template> <template>
<b-dropdown variant="primary" :disabled="$store.getters.embedding !== null"> <b-dropdown variant="primary">
<b-dropdown-item :class="{'dropdown-active': sort === 'score'}" @click="onSelect('score')">{{ <b-dropdown-item :class="{'dropdown-active': sort === 'score'}" @click="onSelect('score')">{{
$t("sort.relevance") $t("sort.relevance")
}} }}

View File

@ -210,8 +210,4 @@ export default {
.theme-black .inspire-tree .matched > .wholerow { .theme-black .inspire-tree .matched > .wholerow {
background: rgba(251, 191, 41, 0.25); background: rgba(251, 191, 41, 0.25);
} }
#tagTree {
max-height: 350px;
overflow: auto;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="thumbnail_count-progress-bar" :style="{width: `${percentProgress}%`}"></div> <div class="thumbnail-progress-bar" :style="{width: `${percentProgress}%`}"></div>
</template> </template>
<script> <script>
@ -16,7 +16,7 @@ export default {
<style scoped> <style scoped>
.thumbnail_count-progress-bar { .thumbnail-progress-bar {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
@ -27,11 +27,11 @@ export default {
z-index: 9; z-index: 9;
} }
.theme-black .thumbnail_count-progress-bar { .theme-black .thumbnail-progress-bar {
background: rgba(0, 188, 212, 0.95); background: rgba(0, 188, 212, 0.95);
} }
.sub-document .thumbnail_count-progress-bar { .sub-document .thumbnail-progress-bar {
max-width: calc(100% - 8px); max-width: calc(100% - 8px);
left: 4px; left: 4px;
} }

View File

@ -1,76 +0,0 @@
<template>
<svg class="ml-icon" :class="{'m-icon': 1, 'ml-icon-big': big, 'ml-icon-clickable': clickable}" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512" xml:space="preserve" fill="currentColor" stroke="currentColor" @click="$emit('click')">
<g>
<path class="st0" d="M167.314,14.993C167.314,6.712,160.602,0,152.332,0h-5.514c-8.27,0-14.982,6.712-14.982,14.993v41.466h35.478
V14.993z"/>
<path class="st0"
d="M238.26,14.993C238.26,6.712,231.549,0,223.278,0h-5.504c-8.271,0-14.982,6.712-14.982,14.993v41.466h35.468 V14.993z"/>
<path class="st0"
d="M309.207,14.993C309.207,6.712,302.496,0,294.225,0h-5.504c-8.271,0-14.982,6.712-14.982,14.993v41.466h35.468 V14.993z"/>
<path class="st0"
d="M380.164,14.993C380.164,6.712,373.453,0,365.182,0h-5.514c-8.27,0-14.982,6.712-14.982,14.993v41.466h35.478 V14.993z"/>
<path class="st0"
d="M131.836,497.007c0,8.282,6.712,14.993,14.982,14.993h5.514c8.27,0,14.982-6.711,14.982-14.993V455.55h-35.478 V497.007z"/>
<path class="st0"
d="M202.792,497.007c0,8.282,6.712,14.993,14.982,14.993h5.504c8.27,0,14.982-6.711,14.982-14.993V455.55h-35.468 V497.007z"/>
<path class="st0"
d="M273.739,497.007c0,8.282,6.712,14.993,14.982,14.993h5.504c8.271,0,14.982-6.711,14.982-14.993V455.55 h-35.468V497.007z"/>
<path class="st0"
d="M344.686,497.007c0,8.282,6.712,14.993,14.982,14.993h5.514c8.271,0,14.982-6.711,14.982-14.993V455.55 h-35.478V497.007z"/>
<path class="st0"
d="M497.018,131.836H455.55v35.479h41.468c8.27,0,14.982-6.712,14.982-14.993v-5.493 C512,138.548,505.288,131.836,497.018,131.836z"/>
<path class="st0"
d="M497.018,202.793H455.55v35.468h41.468c8.27,0,14.982-6.712,14.982-14.982v-5.494 C512,209.504,505.288,202.793,497.018,202.793z"/>
<path class="st0"
d="M497.018,273.739H455.55v35.468h41.468c8.27,0,14.982-6.711,14.982-14.992v-5.494 C512,280.451,505.288,273.739,497.018,273.739z"/>
<path class="st0"
d="M497.018,344.686H455.55v35.479h41.468c8.27,0,14.982-6.712,14.982-14.993v-5.493 C512,351.398,505.288,344.686,497.018,344.686z"/>
<path class="st0"
d="M0,146.828v5.493c0,8.281,6.711,14.993,14.982,14.993H56.46v-35.479H14.982C6.711,131.836,0,138.548,0,146.828 z"/>
<path class="st0"
d="M0,217.785v5.494c0,8.27,6.711,14.982,14.982,14.982H56.46v-35.468H14.982C6.711,202.793,0,209.504,0,217.785z "/>
<path class="st0"
d="M0,288.721v5.494c0,8.281,6.711,14.992,14.982,14.992H56.46v-35.468H14.982C6.711,273.739,0,280.451,0,288.721 z"/>
<path class="st0"
d="M0,359.679v5.493c0,8.281,6.711,14.993,14.982,14.993H56.46v-35.479H14.982C6.711,344.686,0,351.398,0,359.679 z"/>
<path class="st0"
d="M78.628,433.382h354.753V78.628H78.628V433.382z M376.56,120.2c9.18,0,16.635,7.445,16.635,16.634 c0,9.18-7.455,16.624-16.635,16.624c-9.179,0-16.624-7.445-16.624-16.624C359.936,127.644,367.381,120.2,376.56,120.2z M376.56,361.32c9.18,0,16.635,7.445,16.635,16.635c0,9.179-7.455,16.623-16.635,16.623c-9.179,0-16.624-7.444-16.624-16.623 C359.936,368.764,367.381,361.32,376.56,361.32z M184.362,184.362h143.287v143.287H184.362V184.362z M135.439,120.2 c9.19,0,16.635,7.445,16.635,16.634c0,9.169-7.445,16.624-16.635,16.624c-9.178,0-16.623-7.455-16.623-16.624 C118.816,127.644,126.26,120.2,135.439,120.2z M135.439,361.32c9.19,0,16.635,7.445,16.635,16.635 c0,9.169-7.445,16.623-16.635,16.623c-9.178,0-16.623-7.454-16.623-16.623C118.816,368.764,126.26,361.32,135.439,361.32z"/>
</g>
</svg>
</template>
<script>
export default {
name: "MLIcon",
props: {
"big": Boolean,
"clickable": Boolean
}
}
</script>
<style scoped>
.ml-icon-clickable {
cursor: pointer;
}
.ml-icon-big {
width: 24px !important;
height: 24px !important;
}
.ml-icon {
width: 1rem;
min-width: 1rem;
margin-right: 0.2rem;
line-height: 1rem;
height: 1rem;
min-height: 1rem;
filter: brightness(45%);
}
.theme-black .ml-icon {
filter: brightness(80%);
}
</style>

View File

@ -18,7 +18,6 @@ export default {
tags: "Tags", tags: "Tags",
tagFilter: "Filter tags", tagFilter: "Filter tags",
forExample: "For example:", forExample: "For example:",
embeddingsSearchPlaceholder: "Embeddings search",
help: { help: {
simpleSearch: "Simple search", simpleSearch: "Simple search",
advancedSearch: "Advanced search", advancedSearch: "Advanced search",
@ -138,7 +137,7 @@ export default {
}, },
debug: "Debug information", debug: "Debug information",
debugDescription: "Information useful for debugging. If you encounter bugs or have suggestions for" + debugDescription: "Information useful for debugging. If you encounter bugs or have suggestions for" +
" new features, please submit a new issue <a href='https://github.com/sist2app/sist2/issues/new/choose'>here</a>.", " new features, please submit a new issue <a href='https://github.com/simon987/sist2/issues/new/choose'>here</a>.",
tagline: "Tagline", tagline: "Tagline",
toast: { toast: {
esConnErrTitle: "Elasticsearch connection error", esConnErrTitle: "Elasticsearch connection error",
@ -318,7 +317,7 @@ export default {
}, },
debug: "Debug Informationen", debug: "Debug Informationen",
debugDescription: "Informationen für das Debugging. Wenn du Bugs gefunden oder Anregungen für " + debugDescription: "Informationen für das Debugging. Wenn du Bugs gefunden oder Anregungen für " +
"neue Features hast, poste sie bitte <a href='https://github.com/sist2app/sist2/issues/new/choose'>hier</a>.", "neue Features hast, poste sie bitte <a href='https://github.com/simon987/sist2/issues/new/choose'>hier</a>.",
tagline: "Tagline", tagline: "Tagline",
toast: { toast: {
esConnErrTitle: "Elasticsearch Verbindungsfehler", esConnErrTitle: "Elasticsearch Verbindungsfehler",
@ -494,7 +493,7 @@ export default {
debug: "Information de débogage", debug: "Information de débogage",
debugDescription: "Informations utiles pour le débogage\n" + debugDescription: "Informations utiles pour le débogage\n" +
"Si vous rencontrez des bogues ou si vous avez des suggestions pour de nouvelles fonctionnalités," + "Si vous rencontrez des bogues ou si vous avez des suggestions pour de nouvelles fonctionnalités," +
" veuillez soumettre un nouvel Issue <a href='https://github.com/sist2app/sist2/issues/new/choose'>ici</a>.", " veuillez soumettre un nouvel Issue <a href='https://github.com/simon987/sist2/issues/new/choose'>ici</a>.",
tagline: "Tagline", tagline: "Tagline",
toast: { toast: {
esConnErrTitle: "Erreur de connexion Elasticsearch", esConnErrTitle: "Erreur de connexion Elasticsearch",
@ -668,7 +667,7 @@ export default {
}, },
debug: "调试信息", debug: "调试信息",
debugDescription: "对调试除错有用的信息。 若您遇到bug或者想建议新功能请提交新Issue到" + debugDescription: "对调试除错有用的信息。 若您遇到bug或者想建议新功能请提交新Issue到" +
"<a href='https://github.com/sist2app/sist2/issues/new/choose'>这里</a>.", "<a href='https://github.com/simon987/sist2/issues/new/choose'>这里</a>.",
tagline: "标签栏", tagline: "标签栏",
toast: { toast: {
esConnErrTitle: "Elasticsearch连接错误", esConnErrTitle: "Elasticsearch连接错误",
@ -846,7 +845,7 @@ export default {
}, },
debug: "Informacje dla programistów", debug: "Informacje dla programistów",
debugDescription: "Informacje przydatne do znajdowania błędów w oprogramowaniu. Jeśli napotkasz błąd lub masz" + debugDescription: "Informacje przydatne do znajdowania błędów w oprogramowaniu. Jeśli napotkasz błąd lub masz" +
" propozycje zmian, zgłoś to proszę <a href='https://github.com/sist2app/sist2/issues/new/choose'>tutaj</a>.", " propozycje zmian, zgłoś to proszę <a href='https://github.com/simon987/sist2/issues/new/choose'>tutaj</a>.",
tagline: "Slogan", tagline: "Slogan",
toast: { toast: {
esConnErrTitle: "Problem z połączeniem z Elasticsearch", esConnErrTitle: "Problem z połączeniem z Elasticsearch",

View File

@ -1,3 +1,4 @@
import '@babel/polyfill'
import 'mutationobserver-shim' import 'mutationobserver-shim'
import Vue from 'vue' import Vue from 'vue'
import './plugins/bootstrap-vue' import './plugins/bootstrap-vue'

View File

@ -1,118 +0,0 @@
const inf = Number.POSITIVE_INFINITY;
const START_TOK = 49406;
const END_TOK = 49407;
function min(array, key) {
return array
.reduce((a, b) => (key(a, b) ? b : a))
}
class TupleSet extends Set {
add(elem) {
return super.add(elem.join("`"));
}
has(elem) {
return super.has(elem.join("`"));
}
toList() {
return [...this].map(x => x.split("`"))
}
}
export class BPETokenizer {
_encoder = null;
_bpeRanks = null;
constructor(encoder, bpeRanks) {
this._encoder = encoder;
this._bpeRanks = bpeRanks;
}
getPairs(word) {
const pairs = new TupleSet();
let prevChar = word[0];
for (let i = 1; i < word.length; i++) {
pairs.add([prevChar, word[i]])
prevChar = word[i];
}
return pairs.toList();
}
bpe(token) {
let word = [...token];
word[word.length - 1] += "</w>";
let pairs = this.getPairs(word)
if (pairs.length === 0) {
return token + "</w>"
}
while (true) {
const bigram = min(pairs, (a, b) => {
return (this._bpeRanks[a.join("`")] ?? inf) > (this._bpeRanks[b.join("`") ?? inf])
});
if (this._bpeRanks[bigram.join("`")] === undefined) {
break;
}
const [first, second] = bigram;
let newWord = [];
let i = 0;
while (i < word.length) {
const j = word.indexOf(first, i);
if (j === -1) {
newWord.push(...word.slice(i));
break;
} else {
newWord.push(...word.slice(i, j));
i = j;
}
if (word[i] === first && i < word.length - 1 && word[i + 1] === second) {
newWord.push(first + second);
i += 2;
} else {
newWord.push(word[i]);
i += 1;
}
}
word = [...newWord]
if (word.length === 1) {
break;
} else {
pairs = this.getPairs(word);
}
}
return word.join(" ");
}
encode(text) {
let bpeTokens = [];
text = text.trim();
text = text.replaceAll(/\s+/g, " ");
text
.match(/<\|startoftext\|>|<\|endoftext\|>|'s|'t|'re|'ve|'m|'ll|'d|[a-zA-Z0-9]+/ig)
.forEach(token => {
bpeTokens.push(...this.bpe(token).split(" ").map(t => this._encoder[t]));
});
bpeTokens.unshift(START_TOK);
bpeTokens = bpeTokens.slice(0, 76);
bpeTokens.push(END_TOK);
while (bpeTokens.length < 77) {
bpeTokens.push(0);
}
return bpeTokens;
}
}

View File

@ -1,8 +1,6 @@
import BertTokenizer from "@/ml/BertTokenizer"; import BertTokenizer from "@/ml/BertTokenizer";
import * as tf from "@tensorflow/tfjs";
import axios from "axios"; import axios from "axios";
import {chunk as _chunk} from "underscore";
import * as ort from "onnxruntime-web";
import {argMax, downloadToBuffer, ORT_WASM_PATHS} from "@/ml/mlUtils";
export default class BertNerModel { export default class BertNerModel {
vocabUrl; vocabUrl;
@ -31,10 +29,7 @@ export default class BertNerModel {
} }
async loadModel(onProgress) { async loadModel(onProgress) {
ort.env.wasm.wasmPaths = ORT_WASM_PATHS; this._model = await tf.loadGraphModel(this.modelUrl, {onProgress});
const buf = await downloadToBuffer(this.modelUrl, onProgress);
this._model = await ort.InferenceSession.create(buf.buffer, {executionProviders: ["wasm"]});
} }
alignLabels(labels, wordIds, words) { alignLabels(labels, wordIds, words) {
@ -62,28 +57,21 @@ export default class BertNerModel {
async predict(text, callback) { async predict(text, callback) {
this._previousWordId = null; this._previousWordId = null;
const encoded = this._tokenizer.encodeText(text, this.inputSize); const encoded = this._tokenizer.encodeText(text, this.inputSize)
let i = 0;
for (let chunk of encoded.inputChunks) { for (let chunk of encoded.inputChunks) {
const rawResult = tf.tidy(() => this._model.execute({
input_ids: tf.tensor2d(chunk.inputIds, [1, this.inputSize], "int32"),
token_type_ids: tf.tensor2d(chunk.segmentIds, [1, this.inputSize], "int32"),
attention_mask: tf.tensor2d(chunk.inputMask, [1, this.inputSize], "int32"),
}));
const results = await this._model.run({ const labelIds = await tf.argMax(rawResult, -1);
input_ids: new ort.Tensor("int32", chunk.inputIds, [1, this.inputSize]), const labelIdsArray = await labelIds.array();
token_type_ids: new ort.Tensor("int32", chunk.segmentIds, [1, this.inputSize]), const labels = labelIdsArray[0].map(id => this.id2label[id]);
attention_mask: new ort.Tensor("int32", chunk.inputMask, [1, this.inputSize]), rawResult.dispose()
});
const labelIds = _chunk(results["output"].data, this.id2label.length).map(argMax); callback(this.alignLabels(labels, chunk.wordIds, encoded.words))
const labels = labelIds.map(id => this.id2label[id]);
callback(this.alignLabels(labels, chunk.wordIds, encoded.words));
i += 1;
// give browser some time to repaint
if (i % 2 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
} }
} }
} }

View File

@ -1,5 +1,4 @@
import {zip, chunk} from "underscore"; import {zip, chunk} from "underscore";
import {toInt64} from "@/ml/mlUtils";
const UNK_INDEX = 100; const UNK_INDEX = 100;
const CLS_INDEX = 101; const CLS_INDEX = 101;

View File

@ -1,59 +0,0 @@
import * as ort from "onnxruntime-web";
import {BPETokenizer} from "@/ml/BPETokenizer";
import axios from "axios";
import {downloadToBuffer, ORT_WASM_PATHS} from "@/ml/mlUtils";
import ModelStore from "@/ml/ModelStore";
export class CLIPTransformerModel {
_modelUrl = null;
_tokenizerUrl = null;
_model = null;
_tokenizer = null;
constructor(modelUrl, tokenizerUrl) {
this._modelUrl = modelUrl;
this._tokenizerUrl = tokenizerUrl;
}
async init(onProgress) {
await Promise.all([this.loadTokenizer(), this.loadModel(onProgress)]);
}
async loadModel(onProgress) {
ort.env.wasm.wasmPaths = ORT_WASM_PATHS;
if (window.crossOriginIsolated) {
ort.env.wasm.numThreads = 2;
}
let buf = await ModelStore.get(this._modelUrl);
if (!buf) {
buf = await downloadToBuffer(this._modelUrl, onProgress);
await ModelStore.set(this._modelUrl, buf);
}
this._model = await ort.InferenceSession.create(buf.buffer, {
executionProviders: ["wasm"],
});
}
async loadTokenizer() {
const resp = await axios.get(this._tokenizerUrl);
this._tokenizer = new BPETokenizer(resp.data.encoder, resp.data.bpe_ranks)
}
async predict(text) {
const tokenized = this._tokenizer.encode(text);
const inputs = {
input_ids: new ort.Tensor("int32", tokenized, [1, 77])
};
const results = await this._model.run(inputs);
return Array.from(
Object.values(results)
.find(result => result.size === 512).data
);
}
}

View File

@ -1,67 +0,0 @@
class ModelStore {
_ok;
_db;
_resolve;
_loadingPromise;
constructor() {
const request = window.indexedDB.open("ModelStore", 1);
request.onerror = () => {
this._ok = false;
}
request.onupgradeneeded = event => {
const db = event.target.result;
db.createObjectStore("models");
}
request.onsuccess = () => {
this._ok = true;
this._db = request.result;
this._resolve();
}
this._loadingPromise = new Promise(resolve => this._resolve = resolve);
}
async get(key) {
await this._loadingPromise;
const req = this._db.transaction(["models"], "readwrite")
.objectStore("models")
.get(key);
return new Promise(resolve => {
req.onsuccess = event => {
resolve(event.target.result);
};
req.onerror = event => {
console.log("ERROR:");
console.log(event);
resolve(null);
};
});
}
async set(key, val) {
await this._loadingPromise;
const req = this._db.transaction(["models"], "readwrite")
.objectStore("models")
.put(val, key);
return new Promise(resolve => {
req.onsuccess = () => {
resolve(true);
};
req.onerror = () => {
resolve(false);
};
});
}
}
export default new ModelStore();

View File

@ -1,46 +0,0 @@
export async function downloadToBuffer(url, onProgress) {
const resp = await fetch(url);
const contentLength = +resp.headers.get("Content-Length");
const buf = new Uint8ClampedArray(contentLength);
const reader = resp.body.getReader();
let cursor = 0;
if (onProgress) {
onProgress(0);
}
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
buf.set(value, cursor);
cursor += value.length;
if (onProgress) {
onProgress(cursor / contentLength);
}
}
return buf;
}
export function argMax(array) {
return array
.map((x, i) => [x, i])
.reduce((r, a) => (a[0] > r[0] ? a : r))[1];
}
export function toInt64(array) {
return new BigInt64Array(array.map(BigInt));
}
export const ORT_WASM_PATHS = {
"ort-wasm-simd.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm-simd.wasm",
"ort-wasm.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm.wasm",
"ort-wasm-simd-threaded.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm-simd-threaded.wasm",
"ort-wasm-threaded.wasm": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.15.1/dist/ort-wasm-threaded.wasm",
}

View File

@ -21,7 +21,7 @@ const authGuard = (to, from, next) => {
next(); next();
} }
const routes = [ const routes: Array<RouteConfig> = [
{ {
path: "/", path: "/",
name: "SearchPage", name: "SearchPage",

View File

@ -1,18 +1,20 @@
import Vue from "vue" import Vue from "vue"
import Vuex from "vuex" import Vuex from "vuex"
import VueRouter, {Route} from "vue-router";
import {EsHit, EsResult, EsTag, Index} from "@/Sist2Api";
import {deserializeMimes, randomSeed, serializeMimes} from "@/util"; import {deserializeMimes, randomSeed, serializeMimes} from "@/util";
import {getInstance} from "@/plugins/auth0.js"; import {getInstance} from "@/plugins/auth0.js";
const CONF_VERSION = 3; const CONF_VERSION = 3;
Vue.use(Vuex); Vue.use(Vuex)
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
seed: 0, seed: 0,
indices: [], indices: [] as Index[],
tags: [], tags: [] as EsTag[],
sist2Info: null, sist2Info: null as any,
sizeMin: undefined, sizeMin: undefined,
sizeMax: undefined, sizeMax: undefined,
@ -21,9 +23,6 @@ export default new Vuex.Store({
dateMin: undefined, dateMin: undefined,
dateMax: undefined, dateMax: undefined,
searchText: "", searchText: "",
embeddingText: "",
embedding: null,
embeddingDoc: null,
pathText: "", pathText: "",
sortMode: "score", sortMode: "score",
@ -58,16 +57,16 @@ export default new Vuex.Store({
optVidPreviewInterval: 700, optVidPreviewInterval: 700,
optSimpleLightbox: true, optSimpleLightbox: true,
optShowTagPickerFilter: true, optShowTagPickerFilter: true,
optMlRepositories: "https://raw.githubusercontent.com/sist2app/sist2-ner-models/main/repo.json", optMlRepositories: "https://raw.githubusercontent.com/simon987/sist2-ner-models/main/repo.json",
optAutoAnalyze: false, optAutoAnalyze: false,
optMlDefaultModel: null, optMlDefaultModel: null,
_onLoadSelectedIndices: [], _onLoadSelectedIndices: [] as string[],
_onLoadSelectedMimeTypes: [], _onLoadSelectedMimeTypes: [] as string[],
_onLoadSelectedTags: [], _onLoadSelectedTags: [] as string[],
selectedIndices: [], selectedIndices: [] as Index[],
selectedMimeTypes: [], selectedMimeTypes: [] as string[],
selectedTags: [], selectedTags: [] as string[],
lastQueryResults: null, lastQueryResults: null,
firstQueryResults: null, firstQueryResults: null,
@ -78,10 +77,10 @@ export default new Vuex.Store({
uiSqliteMode: false, uiSqliteMode: false,
uiLightboxIsOpen: false, uiLightboxIsOpen: false,
uiShowLightbox: false, uiShowLightbox: false,
uiLightboxSources: [], uiLightboxSources: [] as string[],
uiLightboxThumbs: [], uiLightboxThumbs: [] as string[],
uiLightboxCaptions: [], uiLightboxCaptions: [] as any[],
uiLightboxTypes: [], uiLightboxTypes: [] as string[],
uiLightboxKey: 0, uiLightboxKey: 0,
uiLightboxSlide: 0, uiLightboxSlide: 0,
uiReachedScrollEnd: false, uiReachedScrollEnd: false,
@ -89,14 +88,13 @@ export default new Vuex.Store({
uiDetailsMimeAgg: null, uiDetailsMimeAgg: null,
uiShowDetails: false, uiShowDetails: false,
uiMimeMap: [], uiMimeMap: [] as any[],
auth0Token: null, auth0Token: null,
nerModel: { mlModel: {
model: null, model: null,
name: null name: null
}, },
embeddingsModel: null
}, },
mutations: { mutations: {
setUiShowDetails: (state, val) => state.uiShowDetails = val, setUiShowDetails: (state, val) => state.uiShowDetails = val,
@ -120,7 +118,7 @@ export default new Vuex.Store({
if (state._onLoadSelectedIndices.length > 0) { if (state._onLoadSelectedIndices.length > 0) {
state.selectedIndices = val.filter( state.selectedIndices = val.filter(
(idx) => state._onLoadSelectedIndices.some(id => id === idx.id.toString(16)) (idx: Index) => state._onLoadSelectedIndices.some(prefix => idx.id.startsWith(prefix))
); );
} else { } else {
state.selectedIndices = val; state.selectedIndices = val;
@ -131,9 +129,6 @@ export default new Vuex.Store({
setDateBoundsMin: (state, val) => state.dateBoundsMin = val, setDateBoundsMin: (state, val) => state.dateBoundsMin = val,
setDateBoundsMax: (state, val) => state.dateBoundsMax = val, setDateBoundsMax: (state, val) => state.dateBoundsMax = val,
setSearchText: (state, val) => state.searchText = val, setSearchText: (state, val) => state.searchText = val,
setEmbeddingText: (state, val) => state.embeddingText = val,
setEmbedding: (state, val) => state.embedding = val,
setEmbeddingDoc: (state, val) => state.embeddingDoc = val,
setFuzzy: (state, val) => state.fuzzy = val, setFuzzy: (state, val) => state.fuzzy = val,
setLastQueryResult: (state, val) => state.lastQueryResults = val, setLastQueryResult: (state, val) => state.lastQueryResults = val,
setFirstQueryResult: (state, val) => state.firstQueryResults = val, setFirstQueryResult: (state, val) => state.firstQueryResults = val,
@ -143,18 +138,18 @@ export default new Vuex.Store({
setSelectedIndices: (state, val) => state.selectedIndices = val, setSelectedIndices: (state, val) => state.selectedIndices = val,
setSelectedMimeTypes: (state, val) => state.selectedMimeTypes = val, setSelectedMimeTypes: (state, val) => state.selectedMimeTypes = val,
setSelectedTags: (state, val) => state.selectedTags = val, setSelectedTags: (state, val) => state.selectedTags = val,
setUiLightboxIsOpen: (state, val) => state.uiLightboxIsOpen = val, setUiLightboxIsOpen: (state, val: boolean) => state.uiLightboxIsOpen = val,
_setUiShowLightbox: (state, val) => state.uiShowLightbox = val, _setUiShowLightbox: (state, val: boolean) => state.uiShowLightbox = val,
setUiLightboxKey: (state, val) => state.uiLightboxKey = val, setUiLightboxKey: (state, val: number) => state.uiLightboxKey = val,
_setKeySequence: (state, val) => state.keySequence = val, _setKeySequence: (state, val: number) => state.keySequence = val,
_setQuerySequence: (state, val) => state.querySequence = val, _setQuerySequence: (state, val: number) => state.querySequence = val,
addLightboxSource: (state, {source, thumbnail, caption, type}) => { addLightboxSource: (state, {source, thumbnail, caption, type}) => {
state.uiLightboxSources.push(source); state.uiLightboxSources.push(source);
state.uiLightboxThumbs.push(thumbnail); state.uiLightboxThumbs.push(thumbnail);
state.uiLightboxCaptions.push(caption); state.uiLightboxCaptions.push(caption);
state.uiLightboxTypes.push(type); state.uiLightboxTypes.push(type);
}, },
setUiLightboxSlide: (state, val) => state.uiLightboxSlide = val, setUiLightboxSlide: (state, val: number) => state.uiLightboxSlide = val,
setUiLightboxSources: (state, val) => state.uiLightboxSources = val, setUiLightboxSources: (state, val) => state.uiLightboxSources = val,
setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val, setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val,
@ -217,8 +212,7 @@ export default new Vuex.Store({
// noop // noop
}, },
setAuth0Token: (state, val) => state.auth0Token = val, setAuth0Token: (state, val) => state.auth0Token = val,
setNerModel: (state, val) => state.nerModel = val, setMlModel: (state, val) => state.mlModel = val,
setEmbeddingsModel: (state, val) => state.embeddingsModel = val,
}, },
actions: { actions: {
setSist2Info: (store, val) => { setSist2Info: (store, val) => {
@ -228,7 +222,7 @@ export default new Vuex.Store({
store.commit("setOptLang", val.lang); store.commit("setOptLang", val.lang);
} }
}, },
loadFromArgs({commit}, route) { loadFromArgs({commit}, route: Route) {
if (route.query.q) { if (route.query.q) {
commit("setSearchText", route.query.q); commit("setSearchText", route.query.q);
@ -263,11 +257,11 @@ export default new Vuex.Store({
} }
if (route.query.m) { if (route.query.m) {
commit("_setOnLoadSelectedMimeTypes", deserializeMimes(route.query.m)); commit("_setOnLoadSelectedMimeTypes", deserializeMimes(route.query.m as string));
} }
if (route.query.t) { if (route.query.t) {
commit("_setOnLoadSelectedTags", (route.query.t).split(",")); commit("_setOnLoadSelectedTags", (route.query.t as string).split(","));
} }
if (route.query.sort) { if (route.query.sort) {
@ -278,7 +272,7 @@ export default new Vuex.Store({
commit("setSeed", Number(route.query.seed)); commit("setSeed", Number(route.query.seed));
} }
}, },
async updateArgs({state}, router) { async updateArgs({state}, router: VueRouter) {
if (router.currentRoute.path !== "/") { if (router.currentRoute.path !== "/") {
return; return;
@ -288,14 +282,14 @@ export default new Vuex.Store({
query: { query: {
q: state.searchText.trim() ? state.searchText.trim().replace(/\s+/g, " ") : undefined, q: state.searchText.trim() ? state.searchText.trim().replace(/\s+/g, " ") : undefined,
fuzzy: state.fuzzy ? null : undefined, fuzzy: state.fuzzy ? null : undefined,
i: state.selectedIndices ? state.selectedIndices.map((idx) => idx.id.toString(16)) : undefined, i: state.selectedIndices ? state.selectedIndices.map((idx: Index) => idx.idPrefix) : undefined,
dMin: state.dateMin, dMin: state.dateMin,
dMax: state.dateMax, dMax: state.dateMax,
sMin: state.sizeMin, sMin: state.sizeMin,
sMax: state.sizeMax, sMax: state.sizeMax,
path: state.pathText ? state.pathText : undefined, path: state.pathText ? state.pathText : undefined,
m: serializeMimes(state.selectedMimeTypes), m: serializeMimes(state.selectedMimeTypes),
t: state.selectedTags.length === 0 ? undefined : state.selectedTags.join(","), t: state.selectedTags.length == 0 ? undefined : state.selectedTags.join(","),
sort: state.sortMode === "score" ? undefined : state.sortMode, sort: state.sortMode === "score" ? undefined : state.sortMode,
seed: state.sortMode === "random" ? state.seed.toString() : undefined seed: state.sortMode === "random" ? state.seed.toString() : undefined
} }
@ -304,11 +298,11 @@ export default new Vuex.Store({
}); });
}, },
updateConfiguration({state}) { updateConfiguration({state}) {
const conf = {}; const conf = {} as any;
Object.keys(state).forEach((key) => { Object.keys(state).forEach((key) => {
if (key.startsWith("opt")) { if (key.startsWith("opt")) {
conf[key] = (state)[key]; conf[key] = (state as any)[key];
} }
}); });
@ -321,14 +315,14 @@ export default new Vuex.Store({
if (confString) { if (confString) {
const conf = JSON.parse(confString); const conf = JSON.parse(confString);
if (!("version" in conf) || conf["version"] !== CONF_VERSION) { if (!("version" in conf) || conf["version"] != CONF_VERSION) {
localStorage.removeItem("sist2_configuration"); localStorage.removeItem("sist2_configuration");
window.location.reload(); window.location.reload();
} }
Object.keys(state).forEach((key) => { Object.keys(state).forEach((key) => {
if (key.startsWith("opt")) { if (key.startsWith("opt")) {
(state)[key] = conf[key]; (state as any)[key] = conf[key];
} }
}); });
} }
@ -376,15 +370,13 @@ export default new Vuex.Store({
}, },
modules: {}, modules: {},
getters: { getters: {
nerModel: (state) => state.nerModel, mlModel: (state) => state.mlModel,
embeddingsModel: (state) => state.embeddingsModel,
embedding: (state) => state.embedding,
seed: (state) => state.seed, seed: (state) => state.seed,
getPathText: (state) => state.pathText, getPathText: (state) => state.pathText,
indices: state => state.indices, indices: state => state.indices,
sist2Info: state => state.sist2Info, sist2Info: state => state.sist2Info,
indexMap: state => { indexMap: state => {
const map = {}; const map = {} as any;
state.indices.forEach(idx => map[idx.id] = idx); state.indices.forEach(idx => map[idx.id] = idx);
return map; return map;
}, },
@ -397,18 +389,17 @@ export default new Vuex.Store({
sizeMin: state => state.sizeMin, sizeMin: state => state.sizeMin,
sizeMax: state => state.sizeMax, sizeMax: state => state.sizeMax,
searchText: state => state.searchText, searchText: state => state.searchText,
embeddingText: state => state.embeddingText,
pathText: state => state.pathText, pathText: state => state.pathText,
fuzzy: state => state.fuzzy, fuzzy: state => state.fuzzy,
size: state => state.optSize, size: state => state.optSize,
sortMode: state => state.sortMode, sortMode: state => state.sortMode,
lastQueryResult: state => state.lastQueryResults, lastQueryResult: state => state.lastQueryResults,
lastDoc: function (state) { lastDoc: function (state): EsHit | null {
if (state.lastQueryResults == null) { if (state.lastQueryResults == null) {
return null; return null;
} }
return (state.lastQueryResults).hits.hits.slice(-1)[0]; return (state.lastQueryResults as unknown as EsResult).hits.hits.slice(-1)[0];
}, },
uiShowLightbox: state => state.uiShowLightbox, uiShowLightbox: state => state.uiShowLightbox,
uiLightboxSources: state => state.uiLightboxSources, uiLightboxSources: state => state.uiLightboxSources,
@ -449,7 +440,7 @@ export default new Vuex.Store({
optMlRepositories: state => state.optMlRepositories, optMlRepositories: state => state.optMlRepositories,
mlRepositoryList: state => { mlRepositoryList: state => {
const repos = state.optMlRepositories.split("\n") const repos = state.optMlRepositories.split("\n")
return repos[0] === "" ? [] : repos; return repos[0] == "" ? [] : repos;
}, },
optMlDefaultModel: state => state.optMlDefaultModel, optMlDefaultModel: state => state.optMlDefaultModel,
optAutoAnalyze: state => state.optAutoAnalyze, optAutoAnalyze: state => state.optAutoAnalyze,

139
sist2-vue/src/util-js.js Normal file
View File

@ -0,0 +1,139 @@
export function mergeTooltips(slider, threshold, separator, fixTooltips) {
const isMobile = window.innerWidth <= 650;
if (isMobile) {
threshold = 25;
}
const textIsRtl = getComputedStyle(slider).direction === 'rtl';
const isRtl = slider.noUiSlider.options.direction === 'rtl';
const isVertical = slider.noUiSlider.options.orientation === 'vertical';
const tooltips = slider.noUiSlider.getTooltips();
const origins = slider.noUiSlider.getOrigins();
// Move tooltips into the origin element. The default stylesheet handles this.
tooltips.forEach(function (tooltip, index) {
if (tooltip) {
origins[index].appendChild(tooltip);
}
});
slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) {
const pools = [[]];
const poolPositions = [[]];
const poolValues = [[]];
let atPool = 0;
// Assign the first tooltip to the first pool, if the tooltip is configured
if (tooltips[0]) {
pools[0][0] = 0;
poolPositions[0][0] = positions[0];
poolValues[0][0] = values[0];
}
for (let i = 1; i < positions.length; i++) {
if (!tooltips[i] || (positions[i] - positions[i - 1]) > threshold) {
atPool++;
pools[atPool] = [];
poolValues[atPool] = [];
poolPositions[atPool] = [];
}
if (tooltips[i]) {
pools[atPool].push(i);
poolValues[atPool].push(values[i]);
poolPositions[atPool].push(positions[i]);
}
}
pools.forEach(function (pool, poolIndex) {
const handlesInPool = pool.length;
for (let j = 0; j < handlesInPool; j++) {
const handleNumber = pool[j];
if (j === handlesInPool - 1) {
let offset = 0;
poolPositions[poolIndex].forEach(function (value) {
offset += 1000 - 10 * value;
});
const direction = isVertical ? 'bottom' : 'right';
const last = isRtl ? 0 : handlesInPool - 1;
const lastOffset = 1000 - 10 * poolPositions[poolIndex][last];
offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset;
// Center this tooltip over the affected handles
tooltips[handleNumber].innerHTML = poolValues[poolIndex].join(separator);
tooltips[handleNumber].style.display = 'block';
tooltips[handleNumber].style[direction] = offset + '%';
} else {
// Hide this tooltip
tooltips[handleNumber].style.display = 'none';
}
}
});
if (fixTooltips) {
const isMobile = window.innerWidth <= 650;
const len = isMobile ? 20 : 5;
if (positions[0] < len) {
tooltips[0].style.right = `${(1 - ((positions[0]) / len)) * -35}px`
} else {
tooltips[0].style.right = "0"
}
if (positions[1] > (100 - len)) {
tooltips[1].style.right = `${((positions[1] - (100 - len)) / len) * 35}px`
} else {
tooltips[1].style.right = "0"
}
}
});
}
export function burrow(table, addSelfDir, rootName) {
const root = {};
table.forEach(row => {
let layer = root;
row.taxonomy.forEach(key => {
layer[key] = key in layer ? layer[key] : {};
layer = layer[key];
});
if (Object.keys(layer).length === 0) {
layer["$size$"] = row.size;
} else if (addSelfDir) {
layer["."] = {
"$size$": row.size,
};
}
});
const descend = function (obj, depth) {
return Object.keys(obj).filter(k => k !== "$size$").map(k => {
const child = {
name: k,
depth: depth,
value: 0,
children: descend(obj[k], depth + 1)
};
if ("$size$" in obj[k]) {
child.value = obj[k]["$size$"];
}
return child;
});
};
return {
name: rootName,
children: descend(root, 1),
value: 0,
depth: 0,
}
}

View File

@ -1,329 +0,0 @@
export function mergeTooltips(slider, threshold, separator, fixTooltips) {
const isMobile = window.innerWidth <= 650;
if (isMobile) {
threshold = 25;
}
const textIsRtl = getComputedStyle(slider).direction === 'rtl';
const isRtl = slider.noUiSlider.options.direction === 'rtl';
const isVertical = slider.noUiSlider.options.orientation === 'vertical';
const tooltips = slider.noUiSlider.getTooltips();
const origins = slider.noUiSlider.getOrigins();
// Move tooltips into the origin element. The default stylesheet handles this.
tooltips.forEach(function (tooltip, index) {
if (tooltip) {
origins[index].appendChild(tooltip);
}
});
slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) {
const pools = [[]];
const poolPositions = [[]];
const poolValues = [[]];
let atPool = 0;
// Assign the first tooltip to the first pool, if the tooltip is configured
if (tooltips[0]) {
pools[0][0] = 0;
poolPositions[0][0] = positions[0];
poolValues[0][0] = values[0];
}
for (let i = 1; i < positions.length; i++) {
if (!tooltips[i] || (positions[i] - positions[i - 1]) > threshold) {
atPool++;
pools[atPool] = [];
poolValues[atPool] = [];
poolPositions[atPool] = [];
}
if (tooltips[i]) {
pools[atPool].push(i);
poolValues[atPool].push(values[i]);
poolPositions[atPool].push(positions[i]);
}
}
pools.forEach(function (pool, poolIndex) {
const handlesInPool = pool.length;
for (let j = 0; j < handlesInPool; j++) {
const handleNumber = pool[j];
if (j === handlesInPool - 1) {
let offset = 0;
poolPositions[poolIndex].forEach(function (value) {
offset += 1000 - 10 * value;
});
const direction = isVertical ? 'bottom' : 'right';
const last = isRtl ? 0 : handlesInPool - 1;
const lastOffset = 1000 - 10 * poolPositions[poolIndex][last];
offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset;
// Center this tooltip over the affected handles
tooltips[handleNumber].innerHTML = poolValues[poolIndex].join(separator);
tooltips[handleNumber].style.display = 'block';
tooltips[handleNumber].style[direction] = offset + '%';
} else {
// Hide this tooltip
tooltips[handleNumber].style.display = 'none';
}
}
});
if (fixTooltips) {
const isMobile = window.innerWidth <= 650;
const len = isMobile ? 20 : 5;
if (positions[0] < len) {
tooltips[0].style.right = `${(1 - ((positions[0]) / len)) * -35}px`
} else {
tooltips[0].style.right = "0"
}
if (positions[1] > (100 - len)) {
tooltips[1].style.right = `${((positions[1] - (100 - len)) / len) * 35}px`
} else {
tooltips[1].style.right = "0"
}
}
});
}
export function burrow(table, addSelfDir, rootName) {
const root = {};
table.forEach(row => {
let layer = root;
row.taxonomy.forEach(key => {
layer[key] = key in layer ? layer[key] : {};
layer = layer[key];
});
if (Object.keys(layer).length === 0) {
layer["$size$"] = row.size;
} else if (addSelfDir) {
layer["."] = {
"$size$": row.size,
};
}
});
const descend = function (obj, depth) {
return Object.keys(obj).filter(k => k !== "$size$").map(k => {
const child = {
name: k,
depth: depth,
value: 0,
children: descend(obj[k], depth + 1)
};
if ("$size$" in obj[k]) {
child.value = obj[k]["$size$"];
}
return child;
});
};
return {
name: rootName,
children: descend(root, 1),
value: 0,
depth: 0,
}
}
export function ext(hit) {
return srcExt(hit._source)
}
export function srcExt(src) {
return Object.prototype.hasOwnProperty.call(src, "extension")
&& src["extension"] !== "" ? "." + src["extension"] : "";
}
export function strUnescape(str) {
let result = "";
for (let i = 0; i < str.length; i++) {
const c = str[i];
const next = str[i + 1];
if (c === "]") {
if (next === "]") {
result += c;
i += 1;
} else {
result += String.fromCharCode(parseInt(str.slice(i, i + 2), 16));
i += 2;
}
} else {
result += c;
}
}
return result;
}
const thresh = 1000;
const units = ["k", "M", "G", "T", "P", "E", "Z", "Y"];
export function humanFileSize(bytes) {
if (bytes === 0) {
return "0 B"
}
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(1) + units[u];
}
export function humanTime(sec_num) {
sec_num = Math.floor(sec_num);
const hours = Math.floor(sec_num / 3600);
const minutes = Math.floor((sec_num - (hours * 3600)) / 60);
const seconds = sec_num - (hours * 3600) - (minutes * 60);
if (sec_num < 60) {
return `${sec_num}s`
}
if (sec_num < 3600) {
return `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
return `${hours < 10 ? "0" : ""}${hours}:${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
export function humanDate(numMilis) {
const date = (new Date(numMilis * 1000));
return date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2)
}
export function lum(c) {
c = c.substring(1);
const rgb = parseInt(c, 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
export function getSelectedTreeNodes(tree) {
const selectedNodes = new Set();
const selected = tree.selected();
for (let i = 0; i < selected.length; i++) {
if (selected[i].id === "any") {
return ["any"]
}
//Only get children
if (selected[i].text.indexOf("(") !== -1) {
if (selected[i].values) {
selectedNodes.add(selected[i].values.slice(-1)[0]);
} else {
selectedNodes.add(selected[i].id);
}
}
}
return Array.from(selectedNodes);
}
export function getTreeNodeAttributes(tree) {
const nodes = tree.selectable();
const attributes = {};
for (let i = 0; i < nodes.length; i++) {
let id = null;
if (nodes[i].text.indexOf("(") !== -1 && nodes[i].values) {
id = nodes[i].values.slice(-1)[0];
} else {
id = nodes[i].id
}
attributes[id] = {
checked: nodes[i].itree.state.checked,
collapsed: nodes[i].itree.state.collapsed,
}
}
return attributes;
}
export function serializeMimes(mimes) {
if (mimes.length === 0) {
return undefined;
}
return mimes.map(mime => compressMime(mime)).join("");
}
export function deserializeMimes(mimeString) {
return mimeString
.replaceAll(/([IVATUF])/g, "$$$&")
.split("$")
.map(mime => decompressMime(mime))
.slice(1) // Ignore the first (empty) token
}
export function compressMime(mime) {
return mime
.replace("image/", "I")
.replace("video/", "V")
.replace("application/", "A")
.replace("text/", "T")
.replace("audio/", "U")
.replace("font/", "F")
.replace("+", ",")
.replace("x-", "X")
}
export function decompressMime(mime) {
return mime
.replace("I", "image/")
.replace("V", "video/")
.replace("A", "application/")
.replace("T", "text/")
.replace("U", "audio/")
.replace("F", "font/")
.replace(",", "+")
.replace("X", "x-")
}
export function randomSeed() {
return Math.round(Math.random() * 100000);
}
export function sid(doc) {
if (doc._id.includes(".")) {
return doc._id
}
const num = BigInt(doc._id);
const indexId = (num >> BigInt(32));
const docId = num & BigInt(0xFFFFFFFF);
return indexId.toString(16).padStart(8, "0") + "." + docId.toString(16).padStart(8, "0");
}

177
sist2-vue/src/util.ts Normal file
View File

@ -0,0 +1,177 @@
import {EsHit} from "@/Sist2Api";
export function ext(hit: EsHit) {
return srcExt(hit._source)
}
export function srcExt(src) {
return Object.prototype.hasOwnProperty.call(src, "extension")
&& src["extension"] !== "" ? "." + src["extension"] : "";
}
export function strUnescape(str: string): string {
let result = "";
for (let i = 0; i < str.length; i++) {
const c = str[i];
const next = str[i + 1];
if (c === "]") {
if (next === "]") {
result += c;
i += 1;
} else {
result += String.fromCharCode(parseInt(str.slice(i, i + 2), 16));
i += 2;
}
} else {
result += c;
}
}
return result;
}
const thresh = 1000;
const units = ["k", "M", "G", "T", "P", "E", "Z", "Y"];
export function humanFileSize(bytes: number): string {
if (bytes === 0) {
return "0 B"
}
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(1) + units[u];
}
export function humanTime(sec_num: number): string {
sec_num = Math.floor(sec_num);
const hours = Math.floor(sec_num / 3600);
const minutes = Math.floor((sec_num - (hours * 3600)) / 60);
const seconds = sec_num - (hours * 3600) - (minutes * 60);
if (sec_num < 60) {
return `${sec_num}s`
}
if (sec_num < 3600) {
return `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
return `${hours < 10 ? "0" : ""}${hours}:${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
export function humanDate(numMilis: number): string {
const date = (new Date(numMilis * 1000));
return date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2)
}
export function lum(c: string) {
c = c.substring(1);
const rgb = parseInt(c, 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
export function getSelectedTreeNodes(tree: any) {
const selectedNodes = new Set();
const selected = tree.selected();
for (let i = 0; i < selected.length; i++) {
if (selected[i].id === "any") {
return ["any"]
}
//Only get children
if (selected[i].text.indexOf("(") !== -1) {
if (selected[i].values) {
selectedNodes.add(selected[i].values.slice(-1)[0]);
} else {
selectedNodes.add(selected[i].id);
}
}
}
return Array.from(selectedNodes);
}
export function getTreeNodeAttributes(tree: any) {
const nodes = tree.selectable();
const attributes = {};
for (let i = 0; i < nodes.length; i++) {
let id = null;
if (nodes[i].text.indexOf("(") !== -1 && nodes[i].values) {
id = nodes[i].values.slice(-1)[0];
} else {
id = nodes[i].id
}
attributes[id] = {
checked: nodes[i].itree.state.checked,
collapsed: nodes[i].itree.state.collapsed,
}
}
return attributes;
}
export function serializeMimes(mimes: string[]): string | undefined {
if (mimes.length == 0) {
return undefined;
}
return mimes.map(mime => compressMime(mime)).join("");
}
export function deserializeMimes(mimeString: string): string[] {
return mimeString
.replaceAll(/([IVATUF])/g, "$$$&")
.split("$")
.map(mime => decompressMime(mime))
.slice(1) // Ignore the first (empty) token
}
export function compressMime(mime: string): string {
return mime
.replace("image/", "I")
.replace("video/", "V")
.replace("application/", "A")
.replace("text/", "T")
.replace("audio/", "U")
.replace("font/", "F")
.replace("+", ",")
.replace("x-", "X")
}
export function decompressMime(mime: string): string {
return mime
.replace("I", "image/")
.replace("V", "video/")
.replace("A", "application/")
.replace("T", "text/")
.replace("U", "audio/")
.replace("F", "font/")
.replace(",", "+")
.replace("X", "x-")
}
export function randomSeed(): number {
return Math.round(Math.random() * 100000);
}

View File

@ -81,7 +81,6 @@
<li><code>doc.artist</code></li> <li><code>doc.artist</code></li>
<li><code>doc.title</code></li> <li><code>doc.title</code></li>
<li><code>doc.genre</code></li> <li><code>doc.genre</code></li>
<li><code>doc.media_comment</code></li>
<li><code>doc.album_artist</code></li> <li><code>doc.album_artist</code></li>
<li><code>doc.exif_make</code></li> <li><code>doc.exif_make</code></li>
<li><code>doc.exif_model</code></li> <li><code>doc.exif_model</code></li>
@ -137,7 +136,7 @@
{{ $t("opt.fuzzy") }} {{ $t("opt.fuzzy") }}
</b-form-checkbox> </b-form-checkbox>
<b-form-checkbox :checked="optSearchInPath" @input="setOptSearchInPath">{{ <b-form-checkbox :disabled="uiSqliteMode" :checked="optSearchInPath" @input="setOptSearchInPath">{{
$t("opt.searchInPath") $t("opt.searchInPath")
}} }}
</b-form-checkbox> </b-form-checkbox>

View File

@ -1,142 +1,142 @@
<template> <template>
<div style="margin-left: auto; margin-right: auto;" class="container"> <div style="margin-left: auto; margin-right: auto;" class="container">
<Preloader v-if="loading"></Preloader> <Preloader v-if="loading"></Preloader>
<b-card v-else-if="!loading && found"> <b-card v-else-if="!loading && found">
<b-card-title :title="doc._source.name + ext(doc)"> <b-card-title :title="doc._source.name + ext(doc)">
{{ doc._source.name + ext(doc) }} {{ doc._source.name + ext(doc) }}
</b-card-title> </b-card-title>
<!-- Thumbnail--> <!-- Thumbnail-->
<div style="position: relative; margin-left: auto; margin-right: auto; text-align: center"> <div style="position: relative; margin-left: auto; margin-right: auto; text-align: center">
<FullThumbnail :doc="doc" :small-badge="false" @onThumbnailClick="onThumbnailClick()"></FullThumbnail> <FullThumbnail :doc="doc" :small-badge="false" @onThumbnailClick="onThumbnailClick()"></FullThumbnail>
</div> </div>
<!-- Audio player--> <!-- Audio player-->
<audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls <audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls
:type="doc._source.mime" :type="doc._source.mime"
:src="`f/${sid(doc)}`"></audio> :src="`f/${doc._source.index}/${doc._id}`"></audio>
<InfoTable :doc="doc" v-if="doc"></InfoTable> <InfoTable :doc="doc" v-if="doc"></InfoTable>
<div v-if="doc._source.content" class="content-div">{{ doc._source.content }}</div> <div v-if="doc._source.content" class="content-div">{{ doc._source.content }}</div>
</b-card> </b-card>
<div v-else> <div v-else>
<b-card> <b-card>
<b-card-title>{{ $t("filePage.notFound") }}</b-card-title> <b-card-title>{{ $t("filePage.notFound") }}</b-card-title>
</b-card> </b-card>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
import Preloader from "@/components/Preloader.vue"; import Preloader from "@/components/Preloader.vue";
import InfoTable from "@/components/InfoTable.vue"; import InfoTable from "@/components/InfoTable.vue";
import Sist2Api from "@/Sist2Api"; import Sist2Api from "@/Sist2Api";
import {ext, sid} from "@/util"; import {ext} from "@/util";
import Vue from "vue"; import Vue from "vue";
import sist2 from "@/Sist2Api";
import FullThumbnail from "@/components/FullThumbnail"; import FullThumbnail from "@/components/FullThumbnail";
export default Vue.extend({ export default Vue.extend({
name: "FilePage", name: "FilePage",
components: { components: {
FullThumbnail, FullThumbnail,
Preloader, Preloader,
InfoTable InfoTable
}, },
data() { data() {
return { return {
loading: true, loading: true,
found: false, found: false,
doc: null doc: null
}
},
methods: {
ext: ext,
sid: sid,
onThumbnailClick() {
window.open(`/f/${sid(this.doc)}`, "_blank");
},
findByCustomField(field, id) {
return {
query: {
bool: {
must: [
{
match: {
[field]: id
}
}
]
}
},
size: 1
}
},
findById(id) {
return {
query: {
bool: {
must: [
{
match: {
"_id": id
}
}
]
}
},
size: 1
}
},
findByName(name) {
return {
query: {
bool: {
must: [
{
match: {
"name": name
}
}
]
}
},
size: 1
}
}
},
mounted() {
let query = null;
if (this.$route.query.byId) {
query = this.findById(this.$route.query.byId);
} else if (this.$route.query.byName) {
query = this.findByName(this.$route.query.byName);
} else if (this.$route.query.by && this.$route.query.q) {
query = this.findByCustomField(this.$route.query.by, this.$route.query.q)
}
if (query) {
Sist2Api.esQuery(query).then(result => {
if (result.hits.hits.length === 0) {
this.found = false;
} else {
this.doc = result.hits.hits[0];
this.found = true;
}
this.loading = false;
});
} else {
this.loading = false;
this.found = false;
}
} }
},
methods: {
ext: ext,
onThumbnailClick() {
window.open(`/f/${this.doc.index}/${this.doc._id}`, "_blank");
},
findByCustomField(field, id) {
return {
query: {
bool: {
must: [
{
match: {
[field]: id
}
}
]
}
},
size: 1
}
},
findById(id) {
return {
query: {
bool: {
must: [
{
match: {
"_id": id
}
}
]
}
},
size: 1
}
},
findByName(name) {
return {
query: {
bool: {
must: [
{
match: {
"name": name
}
}
]
}
},
size: 1
}
}
},
mounted() {
let query = null;
if (this.$route.query.byId) {
query = this.findById(this.$route.query.byId);
} else if (this.$route.query.byName) {
query = this.findByName(this.$route.query.byName);
} else if (this.$route.query.by && this.$route.query.q) {
query = this.findByCustomField(this.$route.query.by, this.$route.query.q)
}
if (query) {
Sist2Api.esQuery(query).then(result => {
if (result.hits.hits.length === 0) {
this.found = false;
} else {
this.doc = result.hits.hits[0];
this.found = true;
}
this.loading = false;
});
} else {
this.loading = false;
this.found = false;
}
}
}); });
</script> </script>
<style scoped> <style scoped>
.img-wrapper { .img-wrapper {
display: inline-block; display: inline-block;
} }
</style> </style>

View File

@ -13,7 +13,6 @@
<b-card v-show="!uiLoading && !showEsConnectionError" id="search-panel"> <b-card v-show="!uiLoading && !showEsConnectionError" id="search-panel">
<SearchBar @show-help="showHelp=true"></SearchBar> <SearchBar @show-help="showHelp=true"></SearchBar>
<EmbeddingsSearchBar v-if="hasEmbeddings" class="mt-3"></EmbeddingsSearchBar>
<b-row> <b-row>
<b-col style="height: 70px;" sm="6"> <b-col style="height: 70px;" sm="6">
<SizeSlider></SizeSlider> <SizeSlider></SizeSlider>
@ -59,15 +58,16 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import {sid} from "@/util";
import Preloader from "@/components/Preloader.vue"; import Preloader from "@/components/Preloader.vue";
import {mapActions, mapGetters, mapMutations} from "vuex"; import {mapActions, mapGetters, mapMutations} from "vuex";
import sist2 from "../Sist2Api";
import Sist2Api, {EsHit, EsResult} from "../Sist2Api";
import SearchBar from "@/components/SearchBar.vue"; import SearchBar from "@/components/SearchBar.vue";
import IndexPicker from "@/components/IndexPicker.vue"; import IndexPicker from "@/components/IndexPicker.vue";
import Vue from "vue"; import Vue from "vue";
import Sist2Query from "@/Sist2ElasticsearchQuery"; import Sist2Query from "@/Sist2ElasticsearchQuery";
import {debounce as _debounce} from "underscore"; import _debounce from "lodash/debounce";
import DocCardWall from "@/components/DocCardWall.vue"; import DocCardWall from "@/components/DocCardWall.vue";
import Lightbox from "@/components/Lightbox.vue"; import Lightbox from "@/components/Lightbox.vue";
import LightboxCaption from "@/components/LightboxCaption.vue"; import LightboxCaption from "@/components/LightboxCaption.vue";
@ -79,13 +79,11 @@ import DateSlider from "@/components/DateSlider.vue";
import TagPicker from "@/components/TagPicker.vue"; import TagPicker from "@/components/TagPicker.vue";
import DocList from "@/components/DocList.vue"; import DocList from "@/components/DocList.vue";
import HelpDialog from "@/components/HelpDialog.vue"; import HelpDialog from "@/components/HelpDialog.vue";
import EmbeddingsSearchBar from "@/components/EmbeddingsSearchBar.vue"; import Sist2SqliteQuery from "@/Sist2SqliteQuery";
import Sist2Api from "@/Sist2Api";
export default Vue.extend({ export default Vue.extend({
components: { components: {
EmbeddingsSearchBar,
HelpDialog, HelpDialog,
DocList, DocList,
TagPicker, TagPicker,
@ -93,9 +91,10 @@ export default Vue.extend({
SizeSlider, PathTree, ResultsCard, MimePicker, Lightbox, DocCardWall, IndexPicker, SearchBar, Preloader SizeSlider, PathTree, ResultsCard, MimePicker, Lightbox, DocCardWall, IndexPicker, SearchBar, Preloader
}, },
data: () => ({ data: () => ({
loading: false,
uiLoading: true, uiLoading: true,
search: undefined, search: undefined as any,
docs: [], docs: [] as EsHit[],
docIds: new Set(), docIds: new Set(),
docChecksums: new Set(), docChecksums: new Set(),
searchBusy: false, searchBusy: false,
@ -105,23 +104,20 @@ export default Vue.extend({
}), }),
computed: { computed: {
...mapGetters(["indices", "optDisplay"]), ...mapGetters(["indices", "optDisplay"]),
hasEmbeddings() {
return Sist2Api.models().length > 0;
},
}, },
mounted() { mounted() {
// Handle touch events // Handle touch events
window.ontouchend = () => this.$store.commit("busTouchEnd"); window.ontouchend = () => this.$store.commit("busTouchEnd");
window.ontouchcancel = () => this.$store.commit("busTouchEnd"); window.ontouchcancel = this.$store.commit("busTouchEnd");
this.search = _debounce(async (clear) => { this.search = _debounce(async (clear: boolean) => {
if (clear) { if (clear) {
await this.clearResults(); await this.clearResults();
} }
await this.searchNow(); await this.searchNow();
}, 350, false); }, 350, {leading: false});
this.$store.dispatch("loadFromArgs", this.$route).then(() => { this.$store.dispatch("loadFromArgs", this.$route).then(() => {
this.$store.subscribe(() => this.$store.dispatch("updateArgs", this.$router)); this.$store.subscribe(() => this.$store.dispatch("updateArgs", this.$router));
@ -130,7 +126,6 @@ export default Vue.extend({
"setSizeMin", "setSizeMax", "setDateMin", "setDateMax", "setSearchText", "setPathText", "setSizeMin", "setSizeMax", "setDateMin", "setDateMax", "setSearchText", "setPathText",
"setSortMode", "setOptHighlight", "setOptFragmentSize", "setFuzzy", "setSize", "setSelectedIndices", "setSortMode", "setOptHighlight", "setOptFragmentSize", "setFuzzy", "setSize", "setSelectedIndices",
"setSelectedMimeTypes", "setSelectedTags", "setOptQueryMode", "setOptSearchInPath", "setSelectedMimeTypes", "setSelectedTags", "setOptQueryMode", "setOptSearchInPath",
"setEmbedding"
].includes(mutation.type)) { ].includes(mutation.type)) {
if (this.searchBusy) { if (this.searchBusy) {
return; return;
@ -157,7 +152,7 @@ export default Vue.extend({
}).catch(error => { }).catch(error => {
console.log(error); console.log(error);
if (error.response.status === 503 || error.response.status === 500) { if (error.response.status == 503 || error.response.status == 500) {
this.showEsConnectionError = true; this.showEsConnectionError = true;
this.uiLoading = false; this.uiLoading = false;
} else { } else {
@ -186,7 +181,7 @@ export default Vue.extend({
bodyClass: "toast-body-error", bodyClass: "toast-body-error",
}); });
}, },
showSyntaxErrorToast: function () { showSyntaxErrorToast: function (): void {
this.$bvToast.toast( this.$bvToast.toast(
this.$t("toast.esQueryErr"), this.$t("toast.esQueryErr"),
{ {
@ -202,11 +197,10 @@ export default Vue.extend({
await this.$store.dispatch("incrementQuerySequence"); await this.$store.dispatch("incrementQuerySequence");
this.$store.commit("busSearch"); this.$store.commit("busSearch");
Sist2Api.search().then(async (resp) => { Sist2Api.search().then(async (resp: EsResult) => {
await this.handleSearch(resp); await this.handleSearch(resp);
this.searchBusy = false; this.searchBusy = false;
}).catch(err => { }).catch(err => {
console.log(err)
if (err.response.status === 500 && this.$store.state.optQueryMode === "advanced") { if (err.response.status === 500 && this.$store.state.optQueryMode === "advanced") {
this.showSyntaxErrorToast(); this.showSyntaxErrorToast();
} else { } else {
@ -221,8 +215,8 @@ export default Vue.extend({
await this.$store.dispatch("clearResults"); await this.$store.dispatch("clearResults");
this.$store.commit("setUiReachedScrollEnd", false); this.$store.commit("setUiReachedScrollEnd", false);
}, },
async handleSearch(resp) { async handleSearch(resp: EsResult) {
if (resp.hits.hits.length === 0 || resp.hits.hits.length < this.$store.state.optSize) { if (resp.hits.hits.length == 0 || resp.hits.hits.length < this.$store.state.optSize) {
this.$store.commit("setUiReachedScrollEnd", true); this.$store.commit("setUiReachedScrollEnd", true);
} }
@ -245,9 +239,9 @@ export default Vue.extend({
if (hit._props.isPlayableImage || hit._props.isPlayableVideo) { if (hit._props.isPlayableImage || hit._props.isPlayableVideo) {
hit._seq = await this.$store.dispatch("getKeySequence"); hit._seq = await this.$store.dispatch("getKeySequence");
this.$store.commit("addLightboxSource", { this.$store.commit("addLightboxSource", {
source: `f/${sid(hit)}`, source: `f/${hit._source.index}/${hit._id}`,
thumbnail_count: hit._props.hasThumbnail thumbnail: hit._props.hasThumbnail
? `t/${sid(hit)}` ? `t/${hit._source.index}/${hit._id}`
: null, : null,
caption: { caption: {
component: LightboxCaption, component: LightboxCaption,

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