From 51bf716854e18c71e7409556bbc24f8a3a7a2af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schr=C3=B6ter?= Date: Mon, 7 Oct 2024 13:40:17 +0200 Subject: [PATCH 1/6] Redesign for optional titles or tags --- main.go | 129 ++++++-- web-app/src/components/DocumentProcessor.tsx | 306 ++++++++++--------- 2 files changed, 257 insertions(+), 178 deletions(-) diff --git a/main.go b/main.go index 7c5a3bb..735c661 100644 --- a/main.go +++ b/main.go @@ -51,13 +51,28 @@ type GetDocumentsApiResponse struct { } `json:"results"` } +// Document is a stripped down version of the document object from paperless-ngx. +// Response payload for /documents endpoint and part of request payload for /generate-suggestions endpoint type Document struct { - ID int `json:"id"` - Title string `json:"title"` - Content string `json:"content"` - Tags []string `json:"tags"` - SuggestedTitle string `json:"suggested_title,omitempty"` - SuggestedTags []string `json:"suggested_tags,omitempty"` + ID int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` +} + +// GenerateSuggestionsRequest is the request payload for generating suggestions for /generate-suggestions endpoint +type GenerateSuggestionsRequest struct { + Documents []Document `json:"documents"` + GenerateTitles bool `json:"generate_titles,omitempty"` + GenerateTags bool `json:"generate_tags,omitempty"` +} + +// DocumentSuggestion is the response payload for /generate-suggestions endpoint and the request payload for /update-documents endpoint (as an array) +type DocumentSuggestion struct { + ID int `json:"id"` + OriginalDocument Document `json:"original_document"` + SuggestedTitle string `json:"suggested_title,omitempty"` + SuggestedTags []string `json:"suggested_tags,omitempty"` } var ( @@ -207,14 +222,14 @@ func documentsHandler(c *gin.Context) { func generateSuggestionsHandler(c *gin.Context) { ctx := c.Request.Context() - var documents []Document - if err := c.ShouldBindJSON(&documents); err != nil { + var suggestionRequest GenerateSuggestionsRequest + if err := c.ShouldBindJSON(&suggestionRequest); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request payload: %v", err)}) log.Printf("Invalid request payload: %v", err) return } - results, err := processDocuments(ctx, documents) + results, err := generateDocumentSuggestions(ctx, suggestionRequest) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error processing documents: %v", err)}) log.Printf("Error processing documents: %v", err) @@ -227,7 +242,7 @@ func generateSuggestionsHandler(c *gin.Context) { // updateDocumentsHandler updates documents with new titles func updateDocumentsHandler(c *gin.Context) { ctx := c.Request.Context() - var documents []Document + var documents []DocumentSuggestion if err := c.ShouldBindJSON(&documents); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request payload: %v", err)}) log.Printf("Invalid request payload: %v", err) @@ -348,7 +363,7 @@ func getDocumentsByTags(ctx context.Context, baseURL, apiToken string, tags []st return documents, nil } -func processDocuments(ctx context.Context, documents []Document) ([]Document, error) { +func generateDocumentSuggestions(ctx context.Context, suggestionRequest GenerateSuggestionsRequest) ([]DocumentSuggestion, error) { llm, err := createLLM() if err != nil { return nil, fmt.Errorf("failed to create LLM client: %v", err) @@ -369,6 +384,9 @@ func processDocuments(ctx context.Context, documents []Document) ([]Document, er availableTagNames = append(availableTagNames, tagName) } + documents := suggestionRequest.Documents + documentSuggestions := []DocumentSuggestion{} + var wg sync.WaitGroup var mu sync.Mutex errors := make([]error, 0) @@ -385,27 +403,50 @@ func processDocuments(ctx context.Context, documents []Document) ([]Document, er content = content[:5000] } - suggestedTitle, err := getSuggestedTitle(ctx, llm, content) - if err != nil { - mu.Lock() - errors = append(errors, fmt.Errorf("Document %d: %v", documentID, err)) - mu.Unlock() - log.Printf("Error processing document %d: %v", documentID, err) - return + var suggestedTitle string + var suggestedTags []string + + if suggestionRequest.GenerateTitles { + suggestedTitle, err = getSuggestedTitle(ctx, llm, content) + if err != nil { + mu.Lock() + errors = append(errors, fmt.Errorf("Document %d: %v", documentID, err)) + mu.Unlock() + log.Printf("Error processing document %d: %v", documentID, err) + return + } } - suggestedTags, err := getSuggestedTags(ctx, llm, content, suggestedTitle, availableTagNames) - if err != nil { - mu.Lock() - errors = append(errors, fmt.Errorf("Document %d: %v", documentID, err)) - mu.Unlock() - log.Printf("Error generating tags for document %d: %v", documentID, err) - return + if suggestionRequest.GenerateTags { + suggestedTags, err = getSuggestedTags(ctx, llm, content, suggestedTitle, availableTagNames) + if err != nil { + mu.Lock() + errors = append(errors, fmt.Errorf("Document %d: %v", documentID, err)) + mu.Unlock() + log.Printf("Error generating tags for document %d: %v", documentID, err) + return + } } mu.Lock() - doc.SuggestedTitle = suggestedTitle - doc.SuggestedTags = suggestedTags + suggestion := DocumentSuggestion{ + ID: documentID, + OriginalDocument: *doc, + } + // Titles + if suggestionRequest.GenerateTitles { + suggestion.SuggestedTitle = suggestedTitle + } else { + suggestion.SuggestedTitle = doc.Title + } + + // Tags + if suggestionRequest.GenerateTags { + suggestion.SuggestedTags = suggestedTags + } else { + suggestion.SuggestedTags = removeTagFromList(doc.Tags, tagToFilter) + } + documentSuggestions = append(documentSuggestions, suggestion) mu.Unlock() log.Printf("Document %d processed successfully.", documentID) }(&documents[i]) @@ -417,7 +458,17 @@ func processDocuments(ctx context.Context, documents []Document) ([]Document, er return nil, errors[0] } - return documents, nil + return documentSuggestions, nil +} + +func removeTagFromList(tags []string, tagToRemove string) []string { + filteredTags := []string{} + for _, tag := range tags { + if tag != tagToRemove { + filteredTags = append(filteredTags, tag) + } + } + return filteredTags } func getSuggestedTags(ctx context.Context, llm llms.Model, content string, suggestedTitle string, availableTags []string) ([]string, error) { @@ -507,7 +558,7 @@ Content: return strings.TrimSpace(strings.Trim(completion.Choices[0].Content, "\"")), nil } -func updateDocuments(ctx context.Context, baseURL, apiToken string, documents []Document) error { +func updateDocuments(ctx context.Context, baseURL, apiToken string, documents []DocumentSuggestion) error { client := &http.Client{} // Fetch all available tags @@ -524,8 +575,13 @@ func updateDocuments(ctx context.Context, baseURL, apiToken string, documents [] newTags := []int{} + tags := document.SuggestedTags + if len(tags) == 0 { + tags = document.OriginalDocument.Tags + } + // Map suggested tag names to IDs - for _, tagName := range document.SuggestedTags { + for _, tagName := range tags { if tagID, exists := availableTags[tagName]; exists { // Skip the tag that we are filtering if tagName == tagToFilter { @@ -537,13 +593,20 @@ func updateDocuments(ctx context.Context, baseURL, apiToken string, documents [] } } - updatedFields["tags"] = newTags - + if len(newTags) > 0 { + updatedFields["tags"] = newTags + } else { + log.Printf("No valid tags found for document %d, skipping.", documentID) + } suggestedTitle := document.SuggestedTitle if len(suggestedTitle) > 128 { suggestedTitle = suggestedTitle[:128] } - updatedFields["title"] = suggestedTitle + if suggestedTitle != "" { + updatedFields["title"] = suggestedTitle + } else { + log.Printf("No valid title found for document %d, skipping.", documentID) + } // Send the update request url := fmt.Sprintf("%s/api/documents/%d/", baseURL, documentID) diff --git a/web-app/src/components/DocumentProcessor.tsx b/web-app/src/components/DocumentProcessor.tsx index a1bea32..70f5704 100644 --- a/web-app/src/components/DocumentProcessor.tsx +++ b/web-app/src/components/DocumentProcessor.tsx @@ -15,44 +15,58 @@ interface Document { title: string; content: string; tags: string[]; - suggested_title?: string; - suggested_tags?: { value: string; label: string }[]; } -type ApiDocument = Omit & { +interface GenerateSuggestionsRequest { + documents: Document[]; + generate_titles?: boolean; + generate_tags?: boolean; +} + +interface DocumentSuggestion { + id: number; + original_document: Document; + suggested_title?: string; suggested_tags?: string[]; -}; +} const DocumentProcessor: React.FC = () => { const [documents, setDocuments] = useState([]); - const [availableTags, setAvailableTags] = useState<{ value: string; label: string }[]>([]); + const [documentSuggestions, setDocumentSuggestions] = useState< + DocumentSuggestion[] + >([]); + const [availableTags, setAvailableTags] = useState< + { value: string; label: string }[] + >([]); const [loading, setLoading] = useState(true); const [processing, setProcessing] = useState(false); const [updating, setUpdating] = useState(false); const [successModalOpen, setSuccessModalOpen] = useState(false); const [filterTag, setFilterTag] = useState(undefined); + const [generateTitles, setGenerateTitles] = useState(true); + const [generateTags, setGenerateTags] = useState(true); useEffect(() => { const fetchData = async () => { try { const [filterTagResponse, documentsResponse, tagsResponse] = await Promise.all([ - axios.get("/api/filter-tag"), - axios.get("/api/documents"), - axios.get("/api/tags"), + axios.get< + { tag: string } | undefined + > + ("/api/filter-tag"), + axios.get< + Document[] + >("/api/documents"), + axios.get<{ + [tag: string]: number; + }>("/api/tags"), ]); setFilterTag(filterTagResponse.data?.tag); - const rawDocuments = documentsResponse.data as ApiDocument[]; - const documents = rawDocuments.map((doc) => ({ - ...doc, - suggested_tags: doc.tags.map((tag) => ({ value: tag, label: tag })), - })); - console.log(documents); - setDocuments(documents); + setDocuments(documentsResponse.data); // Store available tags as objects with value and label - // tagsResponse.data is a map of name to id const tags = Object.entries(tagsResponse.data).map(([name]) => ({ value: name, label: name, @@ -71,16 +85,17 @@ const DocumentProcessor: React.FC = () => { const handleProcessDocuments = async () => { setProcessing(true); try { - const apiDocuments: ApiDocument[] = documents.map((doc) => ({ - ...doc, - suggested_tags: doc.suggested_tags?.map((tag) => tag.value) || [], - })); + const requestPayload: GenerateSuggestionsRequest = { + documents, + generate_titles: generateTitles, + generate_tags: generateTags, + }; - const response = await axios.post("/api/generate-suggestions", apiDocuments); - setDocuments(response.data.map((doc) => ({ - ...doc, - suggested_tags: doc.suggested_tags?.map((tag) => ({ value: tag, label: tag })) || [], - }))); + const response = await axios.post( + "/api/generate-suggestions", + requestPayload + ); + setDocumentSuggestions(response.data); } catch (error) { console.error("Error generating suggestions:", error); } finally { @@ -91,13 +106,9 @@ const DocumentProcessor: React.FC = () => { const handleUpdateDocuments = async () => { setUpdating(true); try { - const apiDocuments: ApiDocument[] = documents.map((doc) => ({ - ...doc, - tags: [], // Remove tags from the API document - suggested_tags: doc.suggested_tags?.map((tag) => tag.value) || [], - })); - await axios.patch("/api/update-documents", apiDocuments); + await axios.patch("/api/update-documents", documentSuggestions); setSuccessModalOpen(true); + resetSuggestions(); } catch (error) { console.error("Error updating documents:", error); } finally { @@ -106,17 +117,12 @@ const DocumentProcessor: React.FC = () => { }; const resetSuggestions = () => { - const resetDocs = documents.map((doc) => ({ - ...doc, - suggested_title: undefined, - suggested_tags: [], - })); - setDocuments(resetDocs); + setDocumentSuggestions([]); }; const fetchDocuments = async () => { try { - const response = await axios.get("/api/documents"); // API endpoint to fetch documents + const response = await axios.get("/api/documents"); setDocuments(response.data); } catch (error) { console.error("Error fetching documents:", error); @@ -171,7 +177,7 @@ const DocumentProcessor: React.FC = () => { )} - {!documents.some((doc) => doc.suggested_title) && ( + {documentSuggestions.length === 0 && (

@@ -196,120 +202,130 @@ const DocumentProcessor: React.FC = () => { {processing ? "Processing..." : "Generate Suggestions"}

-
- - - - - - - - - {documents.map((doc) => ( - - - - - ))} - -
- ID - - Title -
- {doc.id} - - {doc.title} -
+
+ + +
+
+ {documents.map((doc) => ( +
+

{doc.title}

+
+                  {doc.content.length > 100 ? `${doc.content.substring(0, 100)}...` : doc.content}
+                
+
+ {doc.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+
+

{doc.title}

+
+                      {doc.content}
+                    
+
+ {doc.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+
+
+ ))}
)} - {documents.some((doc) => doc.suggested_title) && ( + {documentSuggestions.length > 0 && (

Review and Edit Suggested Titles

-
- - - - - - - - - - - {documents.map( - (doc) => - doc.suggested_title && ( - - - - - - - ) - )} - -
- ID - - Original Title - - Suggested Title - - Suggested Tags -
- {doc.id} - - {doc.title} - - { - const updatedDocuments = documents.map((d) => - d.id === doc.id - ? { ...d, suggested_title: e.target.value } - : d - ); - setDocuments(updatedDocuments); - }} - className="w-full border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - - { - const updatedTags = [...(doc.suggested_tags || []), { value: tag.value as string, label: tag.label }]; - const updatedDocuments = documents.map((d) => - d.id === doc.id - ? { ...d, suggested_tags: updatedTags } - : d - ); - setDocuments(updatedDocuments); - }} - onDelete={(i) => { - const updatedTags = doc.suggested_tags?.filter( - (_, index) => index !== i - ); - const updatedDocuments = documents.map((d) => - d.id === doc.id - ? { ...d, suggested_tags: updatedTags } - : d - ); - setDocuments(updatedDocuments); - }} - allowNew={false} - placeholderText="Add a tag" - /> -
+
+ {documentSuggestions.map((doc) => ( +
+

+ {doc.original_document.title} +

+ { + const updatedSuggestions = documentSuggestions.map((d) => + d.id === doc.id + ? { ...d, suggested_title: e.target.value } + : d + ); + setDocumentSuggestions(updatedSuggestions); + }} + className="w-full border border-gray-300 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ ({ + value: tag, + label: tag, + })) || [] + } + suggestions={availableTags} + onAdd={(tag) => { + const tagValue = tag.value as string; + const updatedTags = [ + ...(doc.suggested_tags || []), + tagValue, + ]; + const updatedSuggestions = documentSuggestions.map((d) => + d.id === doc.id + ? { ...d, suggested_tags: updatedTags } + : d + ); + setDocumentSuggestions(updatedSuggestions); + }} + onDelete={(i) => { + const updatedTags = doc.suggested_tags?.filter( + (_, index) => index !== i + ); + const updatedSuggestions = documentSuggestions.map((d) => + d.id === doc.id + ? { ...d, suggested_tags: updatedTags } + : d + ); + setDocumentSuggestions(updatedSuggestions); + }} + allowNew={false} + placeholderText="Add a tag" + /> +
+
+ ))}
-
+
- . -
-
- )} - - {documentSuggestions.length === 0 && ( -
-
-

- Documents to Process -

- - -
-
- - -
-
- {documents.map((doc) => ( -
-

{doc.title}

-
-                  {doc.content.length > 100 ? `${doc.content.substring(0, 100)}...` : doc.content}
-                
-
- {doc.tags.map((tag, index) => ( - - {tag} - - ))} -
-
-
-

{doc.title}

-
-                      {doc.content}
-                    
-
- {doc.tags.map((tag, index) => ( - - {tag} - - ))} -
-
-
-
- ))} -
-
- )} - - {documentSuggestions.length > 0 && ( -
-

- Review and Edit Suggested Titles -

-
- {documentSuggestions.map((doc) => ( -
-

- {doc.original_document.title} -

- { - const updatedSuggestions = documentSuggestions.map((d) => - d.id === doc.id - ? { ...d, suggested_title: e.target.value } - : d - ); - setDocumentSuggestions(updatedSuggestions); - }} - className="w-full border border-gray-300 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- ({ - value: tag, - label: tag, - })) || [] - } - suggestions={availableTags} - onAdd={(tag) => { - const tagValue = tag.value as string; - const updatedTags = [ - ...(doc.suggested_tags || []), - tagValue, - ]; - const updatedSuggestions = documentSuggestions.map((d) => - d.id === doc.id - ? { ...d, suggested_tags: updatedTags } - : d - ); - setDocumentSuggestions(updatedSuggestions); - }} - onDelete={(i) => { - const updatedTags = doc.suggested_tags?.filter( - (_, index) => index !== i - ); - const updatedSuggestions = documentSuggestions.map((d) => - d.id === doc.id - ? { ...d, suggested_tags: updatedTags } - : d - ); - setDocumentSuggestions(updatedSuggestions); - }} - allowNew={false} - placeholderText="Add a tag" - /> -
-
- ))} -
-
- - -
-
- )} - - - -
- -
- - - - - -
-
-
-
-
- - Documents Updated - -
-

- The documents have been successfully updated with the - new titles. -

-
-
-
-
- -
-
-
-
-
-
-
- ); -}; - -export default DocumentProcessor; diff --git a/web-app/src/components/DocumentsToProcess.tsx b/web-app/src/components/DocumentsToProcess.tsx new file mode 100644 index 0000000..c8c030e --- /dev/null +++ b/web-app/src/components/DocumentsToProcess.tsx @@ -0,0 +1,75 @@ +import ArrowPathIcon from "@heroicons/react/24/outline/ArrowPathIcon"; +import React from "react"; +import { Document } from "../DocumentProcessor"; +import DocumentCard from "./DocumentCard"; + +interface DocumentsToProcessProps { + documents: Document[]; + generateTitles: boolean; + setGenerateTitles: React.Dispatch>; + generateTags: boolean; + setGenerateTags: React.Dispatch>; + onProcess: () => void; + processing: boolean; + onReload: () => void; +} + +const DocumentsToProcess: React.FC = ({ + documents, + generateTitles, + setGenerateTitles, + generateTags, + setGenerateTags, + onProcess, + processing, + onReload, +}) => ( +
+
+

Documents to Process

+
+ + +
+
+ +
+ + +
+ +
+ {documents.map((doc) => ( + + ))} +
+
+); + +export default DocumentsToProcess; \ No newline at end of file diff --git a/web-app/src/components/NoDocuments.tsx b/web-app/src/components/NoDocuments.tsx new file mode 100644 index 0000000..9a58c20 --- /dev/null +++ b/web-app/src/components/NoDocuments.tsx @@ -0,0 +1,36 @@ +import { ArrowPathIcon } from "@heroicons/react/24/outline"; +import React from "react"; + +interface NoDocumentsProps { + filterTag: string | null; + onReload: () => void; + processing: boolean; +} + +const NoDocuments: React.FC = ({ + filterTag, + onReload, + processing, +}) => ( +
+

+ No documents found with filter tag{" "} + {filterTag && ( + + {filterTag} + + )} + . +

+ +
+); + +export default NoDocuments; \ No newline at end of file diff --git a/web-app/src/components/SuccessModal.tsx b/web-app/src/components/SuccessModal.tsx new file mode 100644 index 0000000..4751f4d --- /dev/null +++ b/web-app/src/components/SuccessModal.tsx @@ -0,0 +1,85 @@ +import React, { Fragment } from "react"; +import { Dialog, DialogTitle, Transition } from "@headlessui/react"; +import { CheckCircleIcon } from "@heroicons/react/24/outline"; + +interface SuccessModalProps { + isOpen: boolean; + onClose: () => void; +} + +const SuccessModal: React.FC = ({ isOpen, onClose }) => ( + + +
+ +
+ + + + + +
+
+
+
+
+ + Documents Updated + +
+

+ The documents have been successfully updated with the + new titles and tags. +

+
+
+
+
+ +
+
+
+
+
+
+); + +export default SuccessModal; \ No newline at end of file diff --git a/web-app/src/components/SuggestionCard.tsx b/web-app/src/components/SuggestionCard.tsx new file mode 100644 index 0000000..4a0d8a4 --- /dev/null +++ b/web-app/src/components/SuggestionCard.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { ReactTags } from "react-tag-autocomplete"; +import { DocumentSuggestion, TagOption } from "../DocumentProcessor"; + +interface SuggestionCardProps { + suggestion: DocumentSuggestion; + availableTags: TagOption[]; + onTitleChange: (docId: number, title: string) => void; + onTagAddition: (docId: number, tag: TagOption) => void; + onTagDeletion: (docId: number, index: number) => void; +} + +const SuggestionCard: React.FC = ({ + suggestion, + availableTags, + onTitleChange, + onTagAddition, + onTagDeletion, +}) => ( +
+
+

+ {suggestion.original_document.title} +

+ +
+ onTitleChange(suggestion.id, e.target.value)} + className="w-full border border-gray-300 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ ({ + id: index.toString(), + name: tag, + label: tag, + value: index.toString(), + })) || [] + } + suggestions={availableTags.map(tag => ({ id: tag.id, name: tag.name, label: tag.name, value: tag.id }))} + onAdd={(tag) => onTagAddition(suggestion.id, { id: String(tag.label), name: String(tag.value) })} + onDelete={(index) => onTagDeletion(suggestion.id, index)} + allowNew={true} + placeholderText="Add a tag" + /> +
+
+); + +export default SuggestionCard; \ No newline at end of file diff --git a/web-app/src/components/SuggestionsReview.tsx b/web-app/src/components/SuggestionsReview.tsx new file mode 100644 index 0000000..cf7a2e1 --- /dev/null +++ b/web-app/src/components/SuggestionsReview.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { DocumentSuggestion, TagOption } from "../DocumentProcessor"; +import SuggestionCard from "./SuggestionCard"; + +interface SuggestionsReviewProps { + suggestions: DocumentSuggestion[]; + availableTags: TagOption[]; + onTitleChange: (docId: number, title: string) => void; + onTagAddition: (docId: number, tag: TagOption) => void; + onTagDeletion: (docId: number, index: number) => void; + onBack: () => void; + onUpdate: () => void; + updating: boolean; +} + +const SuggestionsReview: React.FC = ({ + suggestions, + availableTags, + onTitleChange, + onTagAddition, + onTagDeletion, + onBack, + onUpdate, + updating, +}) => ( +
+

+ Review and Edit Suggested Titles +

+
+ {suggestions.map((doc) => ( + + ))} +
+
+ + +
+
+); + +export default SuggestionsReview; \ No newline at end of file diff --git a/web-app/tsconfig.app.tsbuildinfo b/web-app/tsconfig.app.tsbuildinfo index abb7240..daa87b6 100644 --- a/web-app/tsconfig.app.tsbuildinfo +++ b/web-app/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/documentprocessor.tsx"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/app.tsx","./src/documentprocessor.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/documentcard.tsx","./src/components/documentstoprocess.tsx","./src/components/nodocuments.tsx","./src/components/successmodal.tsx","./src/components/suggestioncard.tsx","./src/components/suggestionsreview.tsx"],"version":"5.6.2"} \ No newline at end of file From aa67b2443a15322650c138639de5140241ca0fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schr=C3=B6ter?= Date: Mon, 7 Oct 2024 20:51:28 +0200 Subject: [PATCH 3/6] Able to apply empty tags --- main.go | 51 ++------------------------------------------------- 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/main.go b/main.go index 735c661..c408c64 100644 --- a/main.go +++ b/main.go @@ -259,50 +259,6 @@ func updateDocumentsHandler(c *gin.Context) { c.Status(http.StatusOK) } -func getIDMappingForTags(ctx context.Context, baseURL, apiToken string, tagsToFilter []string) (map[string]int, error) { - url := fmt.Sprintf("%s/api/tags/", baseURL) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Token %s", apiToken)) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("Error fetching tags: %d, %s", resp.StatusCode, string(bodyBytes)) - } - - var tagsResponse struct { - Results []struct { - ID int `json:"id"` - Name string `json:"name"` - } `json:"results"` - } - - err = json.NewDecoder(resp.Body).Decode(&tagsResponse) - if err != nil { - return nil, err - } - - tagIDMapping := make(map[string]int) - for _, tag := range tagsResponse.Results { - for _, filterTag := range tagsToFilter { - if tag.Name == filterTag { - tagIDMapping[tag.Name] = tag.ID - } - } - } - - return tagIDMapping, nil -} - func getDocumentsByTags(ctx context.Context, baseURL, apiToken string, tags []string) ([]Document, error) { tagQueries := make([]string, len(tags)) for i, tag := range tags { @@ -593,11 +549,8 @@ func updateDocuments(ctx context.Context, baseURL, apiToken string, documents [] } } - if len(newTags) > 0 { - updatedFields["tags"] = newTags - } else { - log.Printf("No valid tags found for document %d, skipping.", documentID) - } + updatedFields["tags"] = newTags + suggestedTitle := document.SuggestedTitle if len(suggestedTitle) > 128 { suggestedTitle = suggestedTitle[:128] From ddcb2459252324bd4962537b494eec2334365b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schr=C3=B6ter?= Date: Mon, 7 Oct 2024 21:27:47 +0200 Subject: [PATCH 4/6] Redesign suggestion card --- web-app/src/components/SuggestionCard.tsx | 104 ++++++++++++++-------- 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/web-app/src/components/SuggestionCard.tsx b/web-app/src/components/SuggestionCard.tsx index 4a0d8a4..d6c4d34 100644 --- a/web-app/src/components/SuggestionCard.tsx +++ b/web-app/src/components/SuggestionCard.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; import { ReactTags } from "react-tag-autocomplete"; import { DocumentSuggestion, TagOption } from "../DocumentProcessor"; @@ -17,41 +16,74 @@ const SuggestionCard: React.FC = ({ onTitleChange, onTagAddition, onTagDeletion, -}) => ( -
-
-

- {suggestion.original_document.title} -

- +}) => { + const document = suggestion.original_document; + return ( +
+
+
+

+ {document.title} +

+

+ {document.content.length > 40 + ? `${document.content.substring(0, 40)}...` + : document.content} +

+
+ {document.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+

{document.content}

+
+
+
+
+ onTitleChange(suggestion.id, e.target.value)} + className="w-full border border-gray-300 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ ({ + id: index.toString(), + name: tag, + label: tag, + value: index.toString(), + })) || [] + } + suggestions={availableTags.map((tag) => ({ + id: tag.id, + name: tag.name, + label: tag.name, + value: tag.id, + }))} + onAdd={(tag) => + onTagAddition(suggestion.id, { + id: String(tag.label), + name: String(tag.value), + }) + } + onDelete={(index) => onTagDeletion(suggestion.id, index)} + allowNew={true} + placeholderText="Add a tag" + /> +
+
- onTitleChange(suggestion.id, e.target.value)} - className="w-full border border-gray-300 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- ({ - id: index.toString(), - name: tag, - label: tag, - value: index.toString(), - })) || [] - } - suggestions={availableTags.map(tag => ({ id: tag.id, name: tag.name, label: tag.name, value: tag.id }))} - onAdd={(tag) => onTagAddition(suggestion.id, { id: String(tag.label), name: String(tag.value) })} - onDelete={(index) => onTagDeletion(suggestion.id, index)} - allowNew={true} - placeholderText="Add a tag" - /> -
-
-); + ); +}; export default SuggestionCard; \ No newline at end of file From cc04527e36a7f1b655e06419c58830720f5d7082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schr=C3=B6ter?= Date: Mon, 7 Oct 2024 21:38:34 +0200 Subject: [PATCH 5/6] Enhance drop shadow of cards --- web-app/src/components/DocumentCard.tsx | 2 +- web-app/src/components/SuggestionCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web-app/src/components/DocumentCard.tsx b/web-app/src/components/DocumentCard.tsx index 01ddbca..26e0bed 100644 --- a/web-app/src/components/DocumentCard.tsx +++ b/web-app/src/components/DocumentCard.tsx @@ -6,7 +6,7 @@ interface DocumentCardProps { } const DocumentCard: React.FC = ({ document }) => ( -
+

{document.title}

{document.content.length > 100 diff --git a/web-app/src/components/SuggestionCard.tsx b/web-app/src/components/SuggestionCard.tsx index d6c4d34..5507a4c 100644 --- a/web-app/src/components/SuggestionCard.tsx +++ b/web-app/src/components/SuggestionCard.tsx @@ -19,7 +19,7 @@ const SuggestionCard: React.FC = ({ }) => { const document = suggestion.original_document; return ( -

+

From b47f0627302b8a397e153c64cfcca7e7b0d12ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Schr=C3=B6ter?= Date: Mon, 7 Oct 2024 22:14:05 +0200 Subject: [PATCH 6/6] Introduce dark mode --- web-app/index.html | 6 ++-- web-app/src/DocumentProcessor.tsx | 14 ++++----- web-app/src/components/DocumentCard.tsx | 14 ++++----- web-app/src/components/DocumentsToProcess.tsx | 12 ++++--- web-app/src/components/NoDocuments.tsx | 6 ++-- web-app/src/components/SuccessModal.tsx | 19 ++++++------ web-app/src/components/SuggestionCard.tsx | 31 ++++++++++++++----- web-app/src/components/SuggestionsReview.tsx | 8 ++--- 8 files changed, 63 insertions(+), 47 deletions(-) diff --git a/web-app/index.html b/web-app/index.html index 7764df0..fcdbc4f 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -1,13 +1,13 @@ - + Paperless GPT - +
- + \ No newline at end of file diff --git a/web-app/src/DocumentProcessor.tsx b/web-app/src/DocumentProcessor.tsx index 22b87fd..f3745e5 100644 --- a/web-app/src/DocumentProcessor.tsx +++ b/web-app/src/DocumentProcessor.tsx @@ -185,22 +185,20 @@ const DocumentProcessor: React.FC = () => { if (loading) { return ( -
-
Loading documents...
+
+
Loading documents...
); } return ( -
+
-

- Paperless GPT -

+

Paperless GPT

{error && ( -
+
{error}
)} @@ -246,4 +244,4 @@ const DocumentProcessor: React.FC = () => { ); }; -export default DocumentProcessor; +export default DocumentProcessor; \ No newline at end of file diff --git a/web-app/src/components/DocumentCard.tsx b/web-app/src/components/DocumentCard.tsx index 26e0bed..bc6ec2a 100644 --- a/web-app/src/components/DocumentCard.tsx +++ b/web-app/src/components/DocumentCard.tsx @@ -6,9 +6,9 @@ interface DocumentCardProps { } const DocumentCard: React.FC = ({ document }) => ( -
-

{document.title}

-

+

+

{document.title}

+

{document.content.length > 100 ? `${document.content.substring(0, 100)}...` : document.content} @@ -17,21 +17,21 @@ const DocumentCard: React.FC = ({ document }) => ( {document.tags.map((tag) => ( {tag} ))}

-
-
+
+

{document.title}

{document.content}

{document.tags.map((tag) => ( {tag} diff --git a/web-app/src/components/DocumentsToProcess.tsx b/web-app/src/components/DocumentsToProcess.tsx index c8c030e..aec66a9 100644 --- a/web-app/src/components/DocumentsToProcess.tsx +++ b/web-app/src/components/DocumentsToProcess.tsx @@ -26,19 +26,19 @@ const DocumentsToProcess: React.FC = ({ }) => (
-

Documents to Process

+

Documents to Process

@@ -51,16 +51,18 @@ const DocumentsToProcess: React.FC = ({ type="checkbox" checked={generateTitles} onChange={(e) => setGenerateTitles(e.target.checked)} + className="dark:bg-gray-700 dark:border-gray-600" /> - Generate Titles + Generate Titles
diff --git a/web-app/src/components/NoDocuments.tsx b/web-app/src/components/NoDocuments.tsx index 9a58c20..2c94b51 100644 --- a/web-app/src/components/NoDocuments.tsx +++ b/web-app/src/components/NoDocuments.tsx @@ -12,11 +12,11 @@ const NoDocuments: React.FC = ({ onReload, processing, }) => ( -
+

No documents found with filter tag{" "} {filterTag && ( - + {filterTag} )} @@ -25,7 +25,7 @@ const NoDocuments: React.FC = ({ diff --git a/web-app/src/components/SuggestionCard.tsx b/web-app/src/components/SuggestionCard.tsx index 5507a4c..25da077 100644 --- a/web-app/src/components/SuggestionCard.tsx +++ b/web-app/src/components/SuggestionCard.tsx @@ -19,13 +19,13 @@ const SuggestionCard: React.FC = ({ }) => { const document = suggestion.original_document; return ( -

+
-

+

{document.title}

-

+

{document.content.length > 40 ? `${document.content.substring(0, 40)}...` : document.content} @@ -34,15 +34,15 @@ const SuggestionCard: React.FC = ({ {document.tags.map((tag) => ( {tag} ))}

-
-
+
+

{document.content}

@@ -52,7 +52,7 @@ const SuggestionCard: React.FC = ({ type="text" value={suggestion.suggested_title || ""} onChange={(e) => onTitleChange(suggestion.id, e.target.value)} - className="w-full border border-gray-300 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full border border-gray-300 dark:border-gray-600 rounded px-2 py-1 mt-2 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-gray-200" />
= ({ onDelete={(index) => onTagDeletion(suggestion.id, index)} allowNew={true} placeholderText="Add a tag" + classNames={{ + root: "react-tags dark:bg-gray-800", + rootIsActive: "is-active", + rootIsDisabled: "is-disabled", + rootIsInvalid: "is-invalid", + label: "react-tags__label", + tagList: "react-tags__list", + tagListItem: "react-tags__list-item", + tag: "react-tags__tag dark:bg-blue-900 dark:text-blue-200", + tagName: "react-tags__tag-name", + comboBox: "react-tags__combobox dark:bg-gray-700 dark:text-gray-200", + input: "react-tags__combobox-input dark:bg-gray-700 dark:text-gray-200", + listBox: "react-tags__listbox dark:bg-gray-700 dark:text-gray-200", + option: "react-tags__listbox-option dark:bg-gray-700 dark:text-gray-200 hover:bg-blue-500 dark:hover:bg-blue-800", + optionIsActive: "is-active", + highlight: "react-tags__highlight dark:bg-gray-800", + }} />
diff --git a/web-app/src/components/SuggestionsReview.tsx b/web-app/src/components/SuggestionsReview.tsx index cf7a2e1..3a1f648 100644 --- a/web-app/src/components/SuggestionsReview.tsx +++ b/web-app/src/components/SuggestionsReview.tsx @@ -24,7 +24,7 @@ const SuggestionsReview: React.FC = ({ updating, }) => (
-

+

Review and Edit Suggested Titles

@@ -42,7 +42,7 @@ const SuggestionsReview: React.FC = ({
@@ -51,8 +51,8 @@ const SuggestionsReview: React.FC = ({ disabled={updating} className={`${ updating - ? "bg-green-400 cursor-not-allowed" - : "bg-green-600 hover:bg-green-700" + ? "bg-green-400 dark:bg-green-600 cursor-not-allowed" + : "bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-800" } text-white px-4 py-2 rounded focus:outline-none`} > {updating ? "Updating..." : "Apply Suggestions"}