mirror of
https://github.com/icereed/paperless-gpt.git
synced 2025-03-12 12:58:02 -05:00
UNDO feature - easily track changes (#54)
This commit is contained in:
parent
5b3373743a
commit
b788f09185
19 changed files with 1152 additions and 48 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,4 +2,5 @@
|
|||
.DS_Store
|
||||
prompts/
|
||||
tests/tmp
|
||||
tmp/
|
||||
tmp/
|
||||
db/
|
11
Dockerfile
11
Dockerfile
|
@ -18,11 +18,14 @@ COPY go.mod go.sum ./
|
|||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
# Pre-compile go-sqlite3 to avoid doing this every time
|
||||
RUN CGO_ENABLED=1 go build -tags musl -o /dev/null github.com/mattn/go-sqlite3
|
||||
|
||||
# Build the Go binary with the musl build tag
|
||||
RUN go build -tags musl -o paperless-gpt .
|
||||
# Now copy the actual source files
|
||||
COPY *.go .
|
||||
|
||||
# Build the binary using caching for both go modules and build cache
|
||||
RUN CGO_ENABLED=1 GOMAXPROCS=$(nproc) go build -tags musl -o paperless-gpt .
|
||||
|
||||
# Stage 2: Build Vite frontend
|
||||
FROM node:20-alpine AS frontend
|
||||
|
|
|
@ -70,6 +70,7 @@ services:
|
|||
environment:
|
||||
PAPERLESS_BASE_URL: 'http://paperless-ngx:8000'
|
||||
PAPERLESS_API_TOKEN: 'your_paperless_api_token'
|
||||
PAPERLESS_PUBLIC_URL: 'http://paperless.mydomain.com' # Optional, your public link to access Paperless
|
||||
LLM_PROVIDER: 'openai' # or 'ollama'
|
||||
LLM_MODEL: 'gpt-4o' # or 'llama2'
|
||||
OPENAI_API_KEY: 'your_openai_api_key' # Required if using OpenAI
|
||||
|
@ -137,6 +138,7 @@ If you prefer to run the application manually:
|
|||
|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
|
||||
| `PAPERLESS_BASE_URL` | The base URL of your paperless-ngx instance (e.g., `http://paperless-ngx:8000`). | Yes |
|
||||
| `PAPERLESS_API_TOKEN` | API token for accessing paperless-ngx. You can generate one in the paperless-ngx admin interface. | Yes |
|
||||
| `PAPERLESS_PUBLIC_URL` | The public URL for your Paperless instance, if it is different to your `PAPERLESS_BASE_URL` - say if you are running in Docker Compose | No |
|
||||
| `LLM_PROVIDER` | The LLM provider to use (`openai` or `ollama`). | Yes |
|
||||
| `LLM_MODEL` | The model name to use (e.g., `gpt-4o`, `gpt-3.5-turbo`, `llama2`). | Yes |
|
||||
| `OPENAI_API_KEY` | Your OpenAI API key. Required if using OpenAI as the LLM provider. | Cond. |
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -138,7 +139,7 @@ func (app *App) updateDocumentsHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
err := app.Client.UpdateDocuments(ctx, documents)
|
||||
err := app.Client.UpdateDocuments(ctx, documents, app.Database, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error updating documents: %v", err)})
|
||||
log.Errorf("Error updating documents: %v", err)
|
||||
|
@ -237,8 +238,94 @@ func (app *App) getDocumentHandler() gin.HandlerFunc {
|
|||
document, err := app.Client.GetDocument(c, parsedID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
log.Errorf("Error fetching document: %v", err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, document)
|
||||
}
|
||||
}
|
||||
|
||||
// Section for local-db actions
|
||||
|
||||
func (app *App) getModificationHistoryHandler(c *gin.Context) {
|
||||
modifications, err := GetAllModifications(app.Database)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve modification history"})
|
||||
log.Errorf("Failed to retrieve modification history: %v", err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, modifications)
|
||||
}
|
||||
|
||||
func (app *App) undoModificationHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
modID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid modification ID"})
|
||||
log.Errorf("Invalid modification ID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
modification, err := GetModification(app.Database, uint(modID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve modification"})
|
||||
log.Errorf("Failed to retrieve modification: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if modification.Undone {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Modification has already been undone"})
|
||||
log.Errorf("Modification has already been undone: %v", id)
|
||||
return
|
||||
}
|
||||
|
||||
// Ok, we're actually doing the update:
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Make the document suggestions for UpdateDocuments
|
||||
var suggestion DocumentSuggestion
|
||||
suggestion.ID = int(modification.DocumentID)
|
||||
suggestion.OriginalDocument, err = app.Client.GetDocument(ctx, int(modification.DocumentID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve original document"})
|
||||
log.Errorf("Failed to retrieve original document: %v", err)
|
||||
return
|
||||
}
|
||||
switch modification.ModField {
|
||||
case "title":
|
||||
suggestion.SuggestedTitle = modification.PreviousValue
|
||||
case "tags":
|
||||
var tags []string
|
||||
err := json.Unmarshal([]byte(modification.PreviousValue), &tags)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unmarshal previous tags"})
|
||||
log.Errorf("Failed to unmarshal previous tags: %v", err)
|
||||
return
|
||||
}
|
||||
suggestion.SuggestedTags = tags
|
||||
case "content":
|
||||
suggestion.SuggestedContent = modification.PreviousValue
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid modification field"})
|
||||
log.Errorf("Invalid modification field: %v", modification.ModField)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the document
|
||||
err = app.Client.UpdateDocuments(ctx, []DocumentSuggestion{suggestion}, app.Database, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update document"})
|
||||
log.Errorf("Failed to update document: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Successful, so set modification as undone
|
||||
err = SetModificationUndone(app.Database, modification)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to mark modification as undone"})
|
||||
return
|
||||
}
|
||||
|
||||
// Else all was ok
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
|
9
go.mod
9
go.mod
|
@ -12,7 +12,9 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tmc/langchaingo v0.1.12
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sync v0.9.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -33,11 +35,14 @@ require (
|
|||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jupiterrider/ffi v0.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
@ -53,7 +58,7 @@ require (
|
|||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
18
go.sum
18
go.sum
|
@ -48,6 +48,10 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
|||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jupiterrider/ffi v0.2.0 h1:tMM70PexgYNmV+WyaYhJgCvQAvtTCs3wXeILPutihnA=
|
||||
|
@ -60,6 +64,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
|
@ -119,8 +125,8 @@ golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
|||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -139,8 +145,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
@ -157,6 +163,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
|
|
79
local_db.go
Normal file
79
local_db.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ModificationHistory represents the schema of the modification_history table
|
||||
type ModificationHistory struct {
|
||||
ID uint `gorm:"primaryKey"` // Auto-incrementing primary key
|
||||
DocumentID uint `gorm:"not null"` // Foreign key to documents table (if applicable)
|
||||
DateChanged string `gorm:"not null"` // Date and time of modification
|
||||
ModField string `gorm:"size:255;not null"` // Field being modified
|
||||
PreviousValue string `gorm:"size:1048576"` // Previous value of the field
|
||||
NewValue string `gorm:"size:1048576"` // New value of the field
|
||||
Undone bool `gorm:"not null;default:false"` // Whether the modification has been undone
|
||||
UndoneDate string `gorm:"default:null"` // Date and time of undoing the modification
|
||||
}
|
||||
|
||||
// InitializeDB initializes the SQLite database and migrates the schema
|
||||
func InitializeDB() *gorm.DB {
|
||||
// Ensure db directory exists
|
||||
dbDir := "db"
|
||||
if err := os.MkdirAll(dbDir, os.ModePerm); err != nil {
|
||||
log.Fatalf("Failed to create db directory: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dbDir, "modification_history.db")
|
||||
|
||||
// Connect to SQLite database
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Migrate the schema (create the table if it doesn't exist)
|
||||
err = db.AutoMigrate(&ModificationHistory{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to migrate database schema: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// InsertModification inserts a new modification record into the database
|
||||
func InsertModification(db *gorm.DB, record *ModificationHistory) error {
|
||||
log.Debugf("Passed modification record: %+v", record)
|
||||
record.DateChanged = time.Now().Format(time.RFC3339) // Set the DateChanged field to the current time
|
||||
log.Debugf("Inserting modification record: %+v", record)
|
||||
result := db.Create(&record) // GORM's Create method
|
||||
log.Debugf("Insertion result: %+v", result)
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// GetModification retrieves a modification record by its ID
|
||||
func GetModification(db *gorm.DB, id uint) (*ModificationHistory, error) {
|
||||
var record ModificationHistory
|
||||
result := db.First(&record, id) // GORM's First method retrieves the first record matching the ID
|
||||
return &record, result.Error
|
||||
}
|
||||
|
||||
// GetAllModifications retrieves all modification records from the database
|
||||
func GetAllModifications(db *gorm.DB) ([]ModificationHistory, error) {
|
||||
var records []ModificationHistory
|
||||
result := db.Order("date_changed DESC").Find(&records) // GORM's Find method retrieves all records
|
||||
return records, result.Error
|
||||
}
|
||||
|
||||
// UndoModification marks a modification record as undone and sets the undo date
|
||||
func SetModificationUndone(db *gorm.DB, record *ModificationHistory) error {
|
||||
record.Undone = true
|
||||
record.UndoneDate = time.Now().Format(time.RFC3339)
|
||||
result := db.Save(&record) // GORM's Save method
|
||||
return result.Error
|
||||
}
|
22
main.go
22
main.go
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/ollama"
|
||||
"github.com/tmc/langchaingo/llms/openai"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Global Variables and Constants
|
||||
|
@ -73,6 +74,7 @@ Be very selective and only choose the most relevant tags since too many tags wil
|
|||
// App struct to hold dependencies
|
||||
type App struct {
|
||||
Client *PaperlessClient
|
||||
Database *gorm.DB
|
||||
LLM llms.Model
|
||||
VisionLLM llms.Model
|
||||
}
|
||||
|
@ -87,6 +89,9 @@ func main() {
|
|||
// Initialize PaperlessClient
|
||||
client := NewPaperlessClient(paperlessBaseURL, paperlessAPIToken)
|
||||
|
||||
// Initialize Database
|
||||
database := InitializeDB()
|
||||
|
||||
// Load Templates
|
||||
loadTemplates()
|
||||
|
||||
|
@ -105,6 +110,7 @@ func main() {
|
|||
// Initialize App with dependencies
|
||||
app := &App{
|
||||
Client: client,
|
||||
Database: database,
|
||||
LLM: llm,
|
||||
VisionLLM: visionLlm,
|
||||
}
|
||||
|
@ -165,6 +171,20 @@ func main() {
|
|||
enabled := isOcrEnabled()
|
||||
c.JSON(http.StatusOK, gin.H{"enabled": enabled})
|
||||
})
|
||||
|
||||
// Local db actions
|
||||
api.GET("/modifications", app.getModificationHistoryHandler)
|
||||
api.POST("/undo-modification/:id", app.undoModificationHandler)
|
||||
|
||||
// Get public Paperless environment (as set in environment variables)
|
||||
api.GET("/paperless-url", func(c *gin.Context) {
|
||||
baseUrl := os.Getenv("PAPERLESS_PUBLIC_URL")
|
||||
if baseUrl == "" {
|
||||
baseUrl = os.Getenv("PAPERLESS_BASE_URL")
|
||||
}
|
||||
baseUrl = strings.TrimRight(baseUrl, "/")
|
||||
c.JSON(http.StatusOK, gin.H{"url": baseUrl})
|
||||
})
|
||||
}
|
||||
|
||||
// Serve static files for the frontend under /assets
|
||||
|
@ -268,7 +288,7 @@ func (app *App) processAutoTagDocuments() (int, error) {
|
|||
return 0, fmt.Errorf("error generating suggestions: %w", err)
|
||||
}
|
||||
|
||||
err = app.Client.UpdateDocuments(ctx, suggestions)
|
||||
err = app.Client.UpdateDocuments(ctx, suggestions, app.Database, false)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error updating documents: %w", err)
|
||||
}
|
||||
|
|
101
paperless.go
101
paperless.go
|
@ -11,11 +11,13 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gen2brain/go-fitz"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PaperlessClient struct to interact with the Paperless-NGX API
|
||||
|
@ -26,6 +28,32 @@ type PaperlessClient struct {
|
|||
CacheFolder string
|
||||
}
|
||||
|
||||
func hasSameTags(original, suggested []string) bool {
|
||||
if len(original) != len(suggested) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Create copies to avoid modifying original slices
|
||||
orig := make([]string, len(original))
|
||||
sugg := make([]string, len(suggested))
|
||||
|
||||
copy(orig, original)
|
||||
copy(sugg, suggested)
|
||||
|
||||
// Sort both slices
|
||||
sort.Strings(orig)
|
||||
sort.Strings(sugg)
|
||||
|
||||
// Compare elements
|
||||
for i := range orig {
|
||||
if orig[i] != sugg[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NewPaperlessClient creates a new instance of PaperlessClient with a default HTTP client
|
||||
func NewPaperlessClient(baseURL, apiToken string) *PaperlessClient {
|
||||
cacheFolder := os.Getenv("PAPERLESS_GPT_CACHE_DIR")
|
||||
|
@ -218,7 +246,7 @@ func (c *PaperlessClient) GetDocument(ctx context.Context, documentID int) (Docu
|
|||
}
|
||||
|
||||
// UpdateDocuments updates the specified documents with suggested changes
|
||||
func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []DocumentSuggestion) error {
|
||||
func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []DocumentSuggestion, db *gorm.DB, isUndo bool) error {
|
||||
// Fetch all available tags
|
||||
availableTags, err := c.GetAllTags(ctx)
|
||||
if err != nil {
|
||||
|
@ -229,21 +257,43 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum
|
|||
for _, document := range documents {
|
||||
documentID := document.ID
|
||||
|
||||
// Original fields will store any updated fields to store records for
|
||||
originalFields := make(map[string]interface{})
|
||||
updatedFields := make(map[string]interface{})
|
||||
newTags := []int{}
|
||||
|
||||
tags := document.SuggestedTags
|
||||
if len(tags) == 0 {
|
||||
tags = document.OriginalDocument.Tags
|
||||
originalTags := document.OriginalDocument.Tags
|
||||
|
||||
originalTagsJSON, err := json.Marshal(originalTags)
|
||||
if err != nil {
|
||||
log.Errorf("Error marshalling JSON for document %d: %v", documentID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// remove autoTag to prevent infinite loop (even if it is in the original tags)
|
||||
tags = removeTagFromList(tags, autoTag)
|
||||
originalTags = removeTagFromList(originalTags, autoTag)
|
||||
|
||||
if len(tags) == 0 {
|
||||
tags = originalTags
|
||||
} else {
|
||||
// We have suggested tags to change
|
||||
originalFields["tags"] = originalTags
|
||||
// remove autoTag to prevent infinite loop - this is required in case of undo
|
||||
tags = removeTagFromList(tags, autoTag)
|
||||
}
|
||||
|
||||
updatedTagsJSON, err := json.Marshal(tags)
|
||||
if err != nil {
|
||||
log.Errorf("Error marshalling JSON for document %d: %v", documentID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Map suggested tag names to IDs
|
||||
for _, tagName := range tags {
|
||||
if tagID, exists := availableTags[tagName]; exists {
|
||||
// Skip the tag that we are filtering
|
||||
if tagName == manualTag {
|
||||
if !isUndo && tagName == manualTag {
|
||||
continue
|
||||
}
|
||||
newTags = append(newTags, tagID)
|
||||
|
@ -259,6 +309,7 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum
|
|||
suggestedTitle = suggestedTitle[:128]
|
||||
}
|
||||
if suggestedTitle != "" {
|
||||
originalFields["title"] = document.OriginalDocument.Title
|
||||
updatedFields["title"] = suggestedTitle
|
||||
} else {
|
||||
log.Warnf("No valid title found for document %d, skipping.", documentID)
|
||||
|
@ -267,8 +318,11 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum
|
|||
// Suggested Content
|
||||
suggestedContent := document.SuggestedContent
|
||||
if suggestedContent != "" {
|
||||
originalFields["content"] = document.OriginalDocument.Content
|
||||
updatedFields["content"] = suggestedContent
|
||||
}
|
||||
log.Debugf("Document %d: Original fields: %v", documentID, originalFields)
|
||||
log.Debugf("Document %d: Updated fields: %v Tags: %v", documentID, updatedFields, tags)
|
||||
|
||||
// Marshal updated fields to JSON
|
||||
jsonData, err := json.Marshal(updatedFields)
|
||||
|
@ -290,6 +344,43 @@ func (c *PaperlessClient) UpdateDocuments(ctx context.Context, documents []Docum
|
|||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
log.Errorf("Error updating document %d: %d, %s", documentID, resp.StatusCode, string(bodyBytes))
|
||||
return fmt.Errorf("error updating document %d: %d, %s", documentID, resp.StatusCode, string(bodyBytes))
|
||||
} else {
|
||||
for field, value := range originalFields {
|
||||
log.Printf("Document %d: Updated %s from %v to %v", documentID, field, originalFields[field], value)
|
||||
// Insert the modification record into the database
|
||||
var modificationRecord ModificationHistory
|
||||
if field == "tags" {
|
||||
// Make sure we only store changes where tags are changed - not the same before and after
|
||||
// And we have to use tags, not updatedFields as they are IDs not fields
|
||||
if !hasSameTags(document.OriginalDocument.Tags, tags) {
|
||||
modificationRecord = ModificationHistory{
|
||||
DocumentID: uint(documentID),
|
||||
ModField: field,
|
||||
PreviousValue: string(originalTagsJSON),
|
||||
NewValue: string(updatedTagsJSON),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Only store mod if field actually changed
|
||||
if originalFields[field] != updatedFields[field] {
|
||||
modificationRecord = ModificationHistory{
|
||||
DocumentID: uint(documentID),
|
||||
ModField: field,
|
||||
PreviousValue: fmt.Sprintf("%v", originalFields[field]),
|
||||
NewValue: fmt.Sprintf("%v", updatedFields[field]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only store if we have a valid modification record
|
||||
if (modificationRecord != ModificationHistory{}) {
|
||||
err = InsertModification(db, &modificationRecord)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Error inserting modification record for document %d: %v", documentID, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Document %d updated successfully.", documentID)
|
||||
|
|
|
@ -13,6 +13,8 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Helper struct to hold common test data and methods
|
||||
|
@ -22,6 +24,7 @@ type testEnv struct {
|
|||
client *PaperlessClient
|
||||
requestCount int
|
||||
mockResponses map[string]http.HandlerFunc
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// newTestEnv initializes a new test environment
|
||||
|
@ -31,6 +34,11 @@ func newTestEnv(t *testing.T) *testEnv {
|
|||
mockResponses: make(map[string]http.HandlerFunc),
|
||||
}
|
||||
|
||||
// Initialize test database
|
||||
db, err := InitializeTestDB()
|
||||
require.NoError(t, err)
|
||||
env.db = db
|
||||
|
||||
// Create a mock server with a handler that dispatches based on URL path
|
||||
env.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
env.requestCount++
|
||||
|
@ -50,6 +58,22 @@ func newTestEnv(t *testing.T) *testEnv {
|
|||
return env
|
||||
}
|
||||
|
||||
func InitializeTestDB() (*gorm.DB, error) {
|
||||
// Use in-memory SQLite for testing
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Migrate schema
|
||||
err = db.AutoMigrate(&ModificationHistory{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// teardown closes the mock server
|
||||
func (env *testEnv) teardown() {
|
||||
env.server.Close()
|
||||
|
@ -327,7 +351,7 @@ func TestUpdateDocuments(t *testing.T) {
|
|||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err := env.client.UpdateDocuments(ctx, documents)
|
||||
err := env.client.UpdateDocuments(ctx, documents, env.db, false)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
311
web-app/package-lock.json
generated
311
web-app/package-lock.json
generated
|
@ -10,17 +10,23 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.8",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"axios": "^1.7.7",
|
||||
"classnames": "^2.5.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-tag-autocomplete": "^7.3.0"
|
||||
"react-tag-autocomplete": "^7.3.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
|
@ -48,6 +54,24 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
|
||||
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
|
||||
"dependencies": {
|
||||
"colorspace": "1.1.x",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
@ -723,6 +747,19 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdi/js": {
|
||||
"version": "7.4.47",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
|
||||
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ=="
|
||||
},
|
||||
"node_modules/@mdi/react": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz",
|
||||
"integrity": "sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -1313,6 +1350,15 @@
|
|||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
|
||||
"integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
|
@ -1338,6 +1384,11 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
|
||||
|
@ -1660,6 +1711,11 @@
|
|||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
@ -1887,6 +1943,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
@ -1902,8 +1967,38 @@
|
|||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||
},
|
||||
"node_modules/colorspace": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||
"dependencies": {
|
||||
"color": "^3.1.3",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
|
@ -1963,6 +2058,15 @@
|
|||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
|
@ -2024,6 +2128,11 @@
|
|||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
|
@ -2305,6 +2414,11 @@
|
|||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
|
@ -2364,6 +2478,11 @@
|
|||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
|
@ -2577,6 +2696,16 @@
|
|||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
|
@ -2652,6 +2781,17 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
@ -2726,6 +2866,11 @@
|
|||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
|
@ -2775,6 +2920,22 @@
|
|||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
|
@ -2857,8 +3018,7 @@
|
|||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
|
@ -2936,6 +3096,14 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
|
@ -3362,6 +3530,19 @@
|
|||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-tooltip": {
|
||||
"version": "5.28.0",
|
||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz",
|
||||
"integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.1",
|
||||
"classnames": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -3371,6 +3552,19 @@
|
|||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
|
@ -3477,6 +3671,33 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
|
@ -3530,6 +3751,14 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
@ -3539,6 +3768,22 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
|
@ -3729,6 +3974,11 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
|
@ -3768,6 +4018,14 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
||||
|
@ -3839,6 +4097,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
|
||||
|
@ -3881,8 +4145,7 @@
|
|||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.7",
|
||||
|
@ -3958,6 +4221,40 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
|
|
@ -13,17 +13,23 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.8",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"axios": "^1.7.7",
|
||||
"classnames": "^2.5.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-tag-autocomplete": "^7.3.0"
|
||||
"react-tag-autocomplete": "^7.3.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
// App.tsx or App.jsx
|
||||
import React from 'react';
|
||||
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import DocumentProcessor from './DocumentProcessor';
|
||||
import ExperimentalOCR from './ExperimentalOCR'; // New component
|
||||
import History from './History';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<DocumentProcessor />} />
|
||||
<Route path="/experimental-ocr" element={<ExperimentalOCR />} />
|
||||
</Routes>
|
||||
<div style={{ display: "flex", height: "100vh" }}>
|
||||
<Sidebar onSelectPage={(page) => console.log(page)} />
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<DocumentProcessor />} />
|
||||
<Route path="/experimental-ocr" element={<ExperimentalOCR />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import axios from "axios";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "react-tag-autocomplete/example/src/styles.css"; // Ensure styles are loaded
|
||||
import DocumentsToProcess from "./components/DocumentsToProcess";
|
||||
import NoDocuments from "./components/NoDocuments";
|
||||
|
@ -46,22 +45,17 @@ const DocumentProcessor: React.FC = () => {
|
|||
const [generateTags, setGenerateTags] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Temporary feature flags
|
||||
const [ocrEnabled, setOcrEnabled] = useState(false);
|
||||
|
||||
// Custom hook to fetch initial data
|
||||
const fetchInitialData = useCallback(async () => {
|
||||
try {
|
||||
const [filterTagRes, documentsRes, tagsRes, ocrEnabledRes] = await Promise.all([
|
||||
const [filterTagRes, documentsRes, tagsRes] = await Promise.all([
|
||||
axios.get<{ tag: string }>("/api/filter-tag"),
|
||||
axios.get<Document[]>("/api/documents"),
|
||||
axios.get<Record<string, number>>("/api/tags"),
|
||||
axios.get<{enabled: boolean}>("/api/experimental/ocr"),
|
||||
]);
|
||||
|
||||
setFilterTag(filterTagRes.data.tag);
|
||||
setDocuments(documentsRes.data);
|
||||
setOcrEnabled(ocrEnabledRes.data.enabled);
|
||||
const tags = Object.keys(tagsRes.data).map((tag) => ({
|
||||
id: tag,
|
||||
name: tag,
|
||||
|
@ -199,16 +193,6 @@ const DocumentProcessor: React.FC = () => {
|
|||
<div className="max-w-5xl mx-auto p-6 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200">
|
||||
<header className="text-center">
|
||||
<h1 className="text-4xl font-bold mb-8">Paperless GPT</h1>
|
||||
{ocrEnabled && (
|
||||
<div>
|
||||
<Link
|
||||
to="/experimental-ocr"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded transition duration-200 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
OCR via LLMs (Experimental)
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{error && (
|
||||
|
|
126
web-app/src/History.tsx
Normal file
126
web-app/src/History.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import UndoCard from './components/UndoCard';
|
||||
|
||||
interface ModificationHistory {
|
||||
ID: number;
|
||||
DocumentID: number;
|
||||
DateChanged: string;
|
||||
ModField: string;
|
||||
PreviousValue: string;
|
||||
NewValue: string;
|
||||
Undone: boolean;
|
||||
UndoneDate: string | null;
|
||||
}
|
||||
|
||||
const History: React.FC = () => {
|
||||
const [modifications, setModifications] = useState<ModificationHistory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [paperlessUrl, setPaperlessUrl] = useState<string>('');
|
||||
|
||||
// Get Paperless URL
|
||||
useEffect(() => {
|
||||
const fetchUrl = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/paperless-url');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch public URL');
|
||||
}
|
||||
const { url } = await response.json();
|
||||
setPaperlessUrl(url);
|
||||
} catch (err) {
|
||||
console.error('Error fetching Paperless URL:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUrl();
|
||||
}, []);
|
||||
|
||||
// Get all modifications
|
||||
useEffect(() => {
|
||||
fetchModifications();
|
||||
}, []);
|
||||
|
||||
const fetchModifications = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/modifications');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch modifications');
|
||||
}
|
||||
const data = await response.json();
|
||||
setModifications(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUndo = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/undo-modification/${id}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to undo modification');
|
||||
}
|
||||
|
||||
// Use ISO 8601 format for consistency
|
||||
const now = new Date().toISOString();
|
||||
|
||||
setModifications(mods => mods.map(mod =>
|
||||
mod.ID === id
|
||||
? { ...mod, Undone: true, UndoneDate: now }
|
||||
: mod
|
||||
));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to undo modification');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-red-500 dark:text-red-400 p-4 text-center">
|
||||
Error: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">
|
||||
Modification History
|
||||
</h1>
|
||||
<div className="mb-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
Note: when undoing tag changes, this will not re-add 'paperless-gpt-auto'
|
||||
</div>
|
||||
{modifications.length === 0 ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center">
|
||||
No modifications found
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-1">
|
||||
{modifications.map((modification) => (
|
||||
<UndoCard
|
||||
key={modification.ID}
|
||||
{...modification}
|
||||
onUndo={handleUndo}
|
||||
paperlessUrl={paperlessUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
12
web-app/src/assets/logo.svg
Normal file
12
web-app/src/assets/logo.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M299,891.7c-4.2-19.8-12.5-59.6-13.6-59.6c-176.7-105.7-155.8-288.7-97.3-393.4
|
||||
c12.5,131.8,245.8,222.8,109.8,383.9c-1.1,2,6.2,27.2,12.5,50.2c27.2-46,68-101.4,65.8-106.7C208.9,358.2,731.9,326.9,840.6,73.7
|
||||
c49.1,244.8-25.1,623.5-445.5,719.7c-2,1.1-76.3,131.8-79.5,132.9c0-2-31.4-1.1-27.2-11.5C290.7,908.4,294.8,900.1,299,891.7
|
||||
L299,891.7z M293.8,793.4c53.3-61.8-9.4-167.4-47.1-201.9C310.5,701.3,306.3,765.1,293.8,793.4L293.8,793.4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 869 B |
75
web-app/src/components/Sidebar.css
Normal file
75
web-app/src/components/Sidebar.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 60px;
|
||||
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #34495e;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-header.collapsed {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-items li {
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-items li.active {
|
||||
background-color: darkslategray;
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-items li:hover {
|
||||
background-color: #1abc9c;
|
||||
}
|
||||
|
||||
.menu-items li a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .menu-items li a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .logo {
|
||||
height: 40px;
|
||||
margin: auto;
|
||||
}
|
||||
|
81
web-app/src/components/Sidebar.tsx
Normal file
81
web-app/src/components/Sidebar.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import axios from "axios";
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import "./Sidebar.css";
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Icon } from '@mdi/react';
|
||||
import { mdiHomeOutline, mdiTextBoxSearchOutline, mdiHistory } from '@mdi/js';
|
||||
import logo from "../assets/logo.svg";
|
||||
|
||||
|
||||
interface SidebarProps {
|
||||
onSelectPage: (page: string) => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ onSelectPage }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
const handlePageClick = (page: string) => {
|
||||
onSelectPage(page);
|
||||
};
|
||||
|
||||
// Get whether experimental OCR is enabled
|
||||
const [ocrEnabled, setOcrEnabled] = useState(false);
|
||||
const fetchOcrEnabled = useCallback(async () => {
|
||||
try {
|
||||
const res = await axios.get<{ enabled: boolean }>("/api/experimental/ocr");
|
||||
setOcrEnabled(res.data.enabled);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOcrEnabled();
|
||||
}, [fetchOcrEnabled]);
|
||||
|
||||
const menuItems = [
|
||||
{ name: 'home', path: '/', icon: mdiHomeOutline, title: 'Home' },
|
||||
{ name: 'history', path: '/history', icon: mdiHistory, title: 'History' },
|
||||
];
|
||||
|
||||
// If OCR is enabled, add the OCR menu item
|
||||
if (ocrEnabled) {
|
||||
menuItems.push({ name: 'ocr', path: '/experimental-ocr', icon: mdiTextBoxSearchOutline, title: 'OCR' });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`sidebar min-w-[64px] ${collapsed ? "collapsed" : ""}`}>
|
||||
<div className={`sidebar-header ${collapsed ? "collapsed" : ""}`}>
|
||||
{!collapsed && <img src={logo} alt="Logo" className="logo w-8 h-8 object-contain flex-shrink-0" />}
|
||||
<button className="toggle-btn" onClick={toggleSidebar}>
|
||||
☰
|
||||
</button>
|
||||
</div>
|
||||
<ul className="menu-items">
|
||||
{menuItems.map((item) => (
|
||||
<li key={item.name} className={location.pathname === item.path ? "active" : ""}>
|
||||
<Link
|
||||
to={item.path}
|
||||
onClick={() => handlePageClick(item.name)}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{/* <Icon path={item.icon} size={1} />
|
||||
{!collapsed && <span> {item.title}</span>} */}
|
||||
<div className="w-7 h-7 flex items-center justify-center flex-shrink-0">
|
||||
<Icon path={item.icon} size={1} />
|
||||
</div>
|
||||
{!collapsed && <span className="ml-2">{item.title}</span>}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
193
web-app/src/components/UndoCard.tsx
Normal file
193
web-app/src/components/UndoCard.tsx
Normal file
|
@ -0,0 +1,193 @@
|
|||
// UndoCard.tsx
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'react-tooltip'
|
||||
|
||||
interface ModificationProps {
|
||||
ID: number;
|
||||
DocumentID: number;
|
||||
DateChanged: string;
|
||||
ModField: string;
|
||||
PreviousValue: string;
|
||||
NewValue: string;
|
||||
Undone: boolean;
|
||||
UndoneDate: string | null;
|
||||
onUndo: (id: number) => void;
|
||||
paperlessUrl: string;
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string | null): string => {
|
||||
if (!dateString) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Invalid date';
|
||||
}
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
} catch {
|
||||
return 'Invalid date';
|
||||
}
|
||||
};
|
||||
|
||||
const buildPaperlessUrl = (paperlessUrl: string, documentId: number): string => {
|
||||
return `${paperlessUrl}/documents/${documentId}/details`;
|
||||
};
|
||||
|
||||
const UndoCard: React.FC<ModificationProps> = ({
|
||||
ID,
|
||||
DocumentID,
|
||||
DateChanged,
|
||||
ModField,
|
||||
PreviousValue,
|
||||
NewValue,
|
||||
Undone,
|
||||
UndoneDate,
|
||||
onUndo,
|
||||
paperlessUrl,
|
||||
}) => {
|
||||
const formatValue = (value: string, field: string) => {
|
||||
if (field === 'tags') {
|
||||
try {
|
||||
const tags = JSON.parse(value) as string[];
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs font-medium px-2.5 py-0.5 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
} else if (field.toLowerCase().includes('date')) {
|
||||
return formatDate(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative bg-white dark:bg-gray-800 p-4 rounded-md shadow-md">
|
||||
<div className="grid grid-cols-6">
|
||||
<div className="col-span-5"> {/* Left content */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div className="">
|
||||
<div className="text-xs uppercase text-gray-500 dark:text-gray-400 font-semibold mb-1">
|
||||
Date Modified
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{DateChanged && formatDate(DateChanged)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<a
|
||||
href={buildPaperlessUrl(paperlessUrl, DocumentID)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
<div className="text-xs uppercase text-gray-500 dark:text-gray-400 font-semibold mb-1">
|
||||
Document ID
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{DocumentID}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<div className="text-xs uppercase text-gray-500 dark:text-gray-400 font-semibold mb-1">
|
||||
Modified Field
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{ModField}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className={`text-sm flex flex-nowrap ${Undone ? 'line-through' : ''}`}>
|
||||
<span className="text-red-500 dark:text-red-400">Previous: </span>
|
||||
<span
|
||||
className="text-gray-600 dark:text-gray-300 truncate overflow-hidden flex-shrink-0 whitespace-nowrap flex-1 max-w-full group relative"
|
||||
{ // Add tooltip if value is too long and not tags
|
||||
...(ModField !== 'tags' && PreviousValue.length > 100 ? {
|
||||
'data-tooltip-id': `tooltip-${ID}-prev`
|
||||
} : {})}
|
||||
>
|
||||
{formatValue(PreviousValue, ModField)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`text-sm flex flex-nowrap ${Undone ? 'line-through' : ''}`}>
|
||||
<span className="text-green-500 dark:text-green-400">New: </span>
|
||||
<span
|
||||
className="text-gray-600 dark:text-gray-300 truncate overflow-hidden flex-shrink-0 whitespace-nowrap flex-1 max-w-full group relative"
|
||||
{ // Add tooltip if value is too long and not tags
|
||||
...(ModField !== 'tags' && NewValue.length > 100 ? {
|
||||
'data-tooltip-id': `tooltip-${ID}-new`
|
||||
} : {})}
|
||||
>
|
||||
{formatValue(NewValue, ModField)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
id={`tooltip-${ID}-prev`}
|
||||
place="bottom"
|
||||
className="flex-wrap"
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
wordWrap: 'break-word',
|
||||
zIndex: 10,
|
||||
whiteSpace: 'pre-line',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{PreviousValue}
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
id={`tooltip-${ID}-new`}
|
||||
place="bottom"
|
||||
className="flex-wrap"
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
wordWrap: 'break-word',
|
||||
zIndex: 10,
|
||||
whiteSpace: 'pre-line',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{NewValue}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid place-items-center"> {/* Button content */}
|
||||
<button
|
||||
onClick={() => onUndo(ID)}
|
||||
disabled={Undone}
|
||||
className={`mt-2 mb-2 p-4 text-sm font-medium rounded-md min-w-[100px] max-w-[150px] text-center break-words ${Undone
|
||||
? 'bg-gray-300 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-500 dark:bg-blue-600 text-white hover:bg-blue-600 dark:hover:bg-blue-700'
|
||||
} transition-colors duration-200`}
|
||||
>
|
||||
{Undone ? (
|
||||
<>
|
||||
<span className="block text-xs">Undone on</span>
|
||||
<span className="block text-xs">{formatDate(UndoneDate)}</span>
|
||||
</>
|
||||
) : (
|
||||
'Undo'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UndoCard;
|
Loading…
Reference in a new issue