feat: add correspondent management and update related components

This commit is contained in:
Dominik Schröter 2025-01-13 15:33:42 +01:00
parent 066ed0bb72
commit 91da6eff80
11 changed files with 120 additions and 20 deletions

View file

@ -6,6 +6,7 @@ import (
"encoding/base64"
"fmt"
"image"
"slices"
"strings"
"sync"
@ -61,6 +62,7 @@ func (app *App) getSuggestedTags(
content string,
suggestedTitle string,
availableTags []string,
originalTags []string,
logger *logrus.Entry) ([]string, error) {
likelyLanguage := getLikelyLanguage()
@ -71,6 +73,7 @@ func (app *App) getSuggestedTags(
err := tagTemplate.Execute(&promptBuffer, map[string]interface{}{
"Language": likelyLanguage,
"AvailableTags": availableTags,
"OriginalTags": originalTags,
"Title": suggestedTitle,
"Content": content,
})
@ -103,6 +106,12 @@ func (app *App) getSuggestedTags(
suggestedTags[i] = strings.TrimSpace(tag)
}
// append the original tags to the suggested tags
suggestedTags = append(suggestedTags, originalTags...)
// Remove duplicates
slices.Sort(suggestedTags)
suggestedTags = slices.Compact(suggestedTags)
// Filter out tags that are not in the available tags list
filteredTags := []string{}
for _, tag := range suggestedTags {
@ -278,7 +287,7 @@ func (app *App) generateDocumentSuggestions(ctx context.Context, suggestionReque
}
if suggestionRequest.GenerateTags {
suggestedTags, err = app.getSuggestedTags(ctx, content, suggestedTitle, availableTagNames, docLogger)
suggestedTags, err = app.getSuggestedTags(ctx, content, suggestedTitle, availableTagNames, doc.Tags, docLogger)
if err != nil {
mu.Lock()
errorsList = append(errorsList, fmt.Errorf("Document %d: %v", documentID, err))

View file

@ -163,6 +163,11 @@ func (client *PaperlessClient) GetDocumentsByTags(ctx context.Context, tags []st
return nil, err
}
allCorrespondents, err := client.GetAllCorrespondents(ctx)
if err != nil {
return nil, err
}
documents := make([]Document, 0, len(documentsResponse.Results))
for _, result := range documentsResponse.Results {
tagNames := make([]string, len(result.Tags))
@ -175,10 +180,21 @@ func (client *PaperlessClient) GetDocumentsByTags(ctx context.Context, tags []st
}
}
correspondentName := ""
if result.Correspondent != 0 {
for name, id := range allCorrespondents {
if result.Correspondent == id {
correspondentName = name
break
}
}
}
documents = append(documents, Document{
ID: result.ID,
Title: result.Title,
Content: result.Content,
Correspondent: correspondentName,
Tags: tagNames,
})
}
@ -227,6 +243,12 @@ func (client *PaperlessClient) GetDocument(ctx context.Context, documentID int)
return Document{}, err
}
allCorrespondents, err := client.GetAllCorrespondents(ctx)
if err != nil {
return Document{}, err
}
// Match tag IDs to tag names
tagNames := make([]string, len(documentResponse.Tags))
for i, resultTagID := range documentResponse.Tags {
for tagName, tagID := range allTags {
@ -237,10 +259,20 @@ func (client *PaperlessClient) GetDocument(ctx context.Context, documentID int)
}
}
// Match correspondent ID to correspondent name
correspondentName := ""
for name, id := range allCorrespondents {
if documentResponse.Correspondent == id {
correspondentName = name
break
}
}
return Document{
ID: documentResponse.ID,
Title: documentResponse.Title,
Content: documentResponse.Content,
Correspondent: correspondentName,
Tags: tagNames,
}, nil
}
@ -302,8 +334,6 @@ func (client *PaperlessClient) UpdateDocuments(ctx context.Context, documents []
// remove autoTag to prevent infinite loop - this is required in case of undo
tags = removeTagFromList(tags, autoTag)
// keep previous tags
tags = append(tags, originalTags...)
// remove duplicates
slices.Sort(tags)
tags = slices.Compact(tags)

View file

@ -348,7 +348,8 @@ func TestUpdateDocuments(t *testing.T) {
// Expected updated fields
expectedFields := map[string]interface{}{
"title": "New Title",
"tags": []interface{}{float64(idTag1), float64(idTag2), float64(idTag3)}, // keep also previous tags
// do not keep previous tags since the tag generation will already take care to include old ones:
"tags": []interface{}{float64(idTag2), float64(idTag3)},
}
assert.Equal(t, expectedFields, updatedFields)

View file

@ -11,7 +11,7 @@ type GetDocumentsApiResponse struct {
All []int `json:"all"`
Results []struct {
ID int `json:"id"`
Correspondent interface{} `json:"correspondent"`
Correspondent int `json:"correspondent"`
DocumentType interface{} `json:"document_type"`
StoragePath interface{} `json:"storage_path"`
Title string `json:"title"`
@ -38,7 +38,7 @@ type GetDocumentsApiResponse struct {
type GetDocumentApiResponse struct {
ID int `json:"id"`
Correspondent interface{} `json:"correspondent"`
Correspondent int `json:"correspondent"`
DocumentType interface{} `json:"document_type"`
StoragePath interface{} `json:"storage_path"`
Title string `json:"title"`
@ -63,6 +63,7 @@ type Document struct {
Title string `json:"title"`
Content string `json:"content"`
Tags []string `json:"tags"`
Correspondent string `json:"correspondent"`
}
// GenerateSuggestionsRequest is the request payload for generating suggestions for /generate-suggestions endpoint

View file

@ -11,12 +11,14 @@ export interface Document {
title: string;
content: string;
tags: string[];
correspondent: string;
}
export interface GenerateSuggestionsRequest {
documents: Document[];
generate_titles?: boolean;
generate_tags?: boolean;
generate_correspondents?: boolean;
}
export interface DocumentSuggestion {
@ -25,6 +27,7 @@ export interface DocumentSuggestion {
suggested_title?: string;
suggested_tags?: string[];
suggested_content?: string;
suggested_correspondent?: string;
}
export interface TagOption {
@ -43,6 +46,7 @@ const DocumentProcessor: React.FC = () => {
const [filterTag, setFilterTag] = useState<string | null>(null);
const [generateTitles, setGenerateTitles] = useState(true);
const [generateTags, setGenerateTags] = useState(true);
const [generateCorrespondents, setGenerateCorrespondents] = useState(true);
const [error, setError] = useState<string | null>(null);
// Custom hook to fetch initial data
@ -81,6 +85,7 @@ const DocumentProcessor: React.FC = () => {
documents,
generate_titles: generateTitles,
generate_tags: generateTags,
generate_correspondents: generateCorrespondents,
};
const { data } = await axios.post<DocumentSuggestion[]>(
@ -137,6 +142,7 @@ const DocumentProcessor: React.FC = () => {
);
};
const handleTitleChange = (docId: number, title: string) => {
setSuggestions((prevSuggestions) =>
prevSuggestions.map((doc) =>
@ -145,6 +151,14 @@ const DocumentProcessor: React.FC = () => {
);
};
const handleCorrespondentChange = (docId: number, correspondent: string) => {
setSuggestions((prevSuggestions) =>
prevSuggestions.map((doc) =>
doc.id === docId ? { ...doc, suggested_correspondent: correspondent } : doc
)
);
}
const resetSuggestions = () => {
setSuggestions([]);
};
@ -214,6 +228,8 @@ const DocumentProcessor: React.FC = () => {
setGenerateTitles={setGenerateTitles}
generateTags={generateTags}
setGenerateTags={setGenerateTags}
generateCorrespondents={generateCorrespondents}
setGenerateCorrespondents={setGenerateCorrespondents}
onProcess={handleProcessDocuments}
processing={processing}
onReload={reloadDocuments}
@ -225,6 +241,7 @@ const DocumentProcessor: React.FC = () => {
onTitleChange={handleTitleChange}
onTagAddition={handleTagAddition}
onTagDeletion={handleTagDeletion}
onCorrespondentChange={handleCorrespondentChange}
onBack={resetSuggestions}
onUpdate={handleUpdateDocuments}
updating={updating}

View file

@ -13,6 +13,9 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ document }) => (
? `${document.content.substring(0, 100)}...`
: document.content}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">
Correspondent: <span className="font-bold text-blue-600 dark:text-blue-400">{document.correspondent}</span>
</p>
<div className="mt-4">
{document.tags.map((tag) => (
<span
@ -27,6 +30,9 @@ const DocumentCard: React.FC<DocumentCardProps> = ({ document }) => (
<div className="text-sm text-white p-2 bg-gray-800 dark:bg-gray-900 rounded-md w-full max-h-full overflow-y-auto">
<h3 className="text-lg font-semibold text-white">{document.title}</h3>
<p className="mt-2 whitespace-pre-wrap">{document.content}</p>
<p className="mt-2">
Correspondent: <span className="font-bold text-blue-400">{document.correspondent}</span>
</p>
<div className="mt-4">
{document.tags.map((tag) => (
<span

View file

@ -9,6 +9,8 @@ interface DocumentsToProcessProps {
setGenerateTitles: React.Dispatch<React.SetStateAction<boolean>>;
generateTags: boolean;
setGenerateTags: React.Dispatch<React.SetStateAction<boolean>>;
generateCorrespondents: boolean;
setGenerateCorrespondents: React.Dispatch<React.SetStateAction<boolean>>;
onProcess: () => void;
processing: boolean;
onReload: () => void;
@ -20,6 +22,8 @@ const DocumentsToProcess: React.FC<DocumentsToProcessProps> = ({
setGenerateTitles,
generateTags,
setGenerateTags,
generateCorrespondents,
setGenerateCorrespondents,
onProcess,
processing,
onReload,
@ -64,6 +68,15 @@ const DocumentsToProcess: React.FC<DocumentsToProcessProps> = ({
/>
<span className="text-gray-700 dark:text-gray-200">Generate Tags</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={generateCorrespondents}
onChange={(e) => setGenerateCorrespondents(e.target.checked)}
className="dark:bg-gray-700 dark:border-gray-600"
/>
<span className="text-gray-700 dark:text-gray-200">Generate Correspondents</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View file

@ -8,6 +8,7 @@ interface SuggestionCardProps {
onTitleChange: (docId: number, title: string) => void;
onTagAddition: (docId: number, tag: TagOption) => void;
onTagDeletion: (docId: number, index: number) => void;
onCorrespondentChange: (docId: number, correspondent: string) => void;
}
const SuggestionCard: React.FC<SuggestionCardProps> = ({
@ -16,6 +17,7 @@ const SuggestionCard: React.FC<SuggestionCardProps> = ({
onTitleChange,
onTagAddition,
onTagDeletion,
onCorrespondentChange,
}) => {
const sortedAvailableTags = availableTags.sort((a, b) => a.name.localeCompare(b.name));
const document = suggestion.original_document;
@ -49,6 +51,9 @@ const SuggestionCard: React.FC<SuggestionCardProps> = ({
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Suggested Title
</label>
<input
type="text"
value={suggestion.suggested_title || ""}
@ -56,6 +61,9 @@ const SuggestionCard: React.FC<SuggestionCardProps> = ({
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"
/>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Suggested Tags
</label>
<ReactTags
selected={
suggestion.suggested_tags?.map((tag, index) => ({
@ -99,6 +107,18 @@ const SuggestionCard: React.FC<SuggestionCardProps> = ({
}}
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Suggested Correspondent
</label>
<input
type="text"
value={suggestion.suggested_correspondent || ""}
onChange={(e) => onCorrespondentChange(suggestion.id, e.target.value)}
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"
placeholder="Correspondent"
/>
</div>
</div>
</div>
);

View file

@ -8,6 +8,7 @@ interface SuggestionsReviewProps {
onTitleChange: (docId: number, title: string) => void;
onTagAddition: (docId: number, tag: TagOption) => void;
onTagDeletion: (docId: number, index: number) => void;
onCorrespondentChange: (docId: number, correspondent: string) => void;
onBack: () => void;
onUpdate: () => void;
updating: boolean;
@ -19,6 +20,7 @@ const SuggestionsReview: React.FC<SuggestionsReviewProps> = ({
onTitleChange,
onTagAddition,
onTagDeletion,
onCorrespondentChange,
onBack,
onUpdate,
updating,
@ -36,6 +38,7 @@ const SuggestionsReview: React.FC<SuggestionsReviewProps> = ({
onTitleChange={onTitleChange}
onTagAddition={onTagAddition}
onTagDeletion={onTagDeletion}
onCorrespondentChange={onCorrespondentChange}
/>
))}
</div>

View file

@ -1 +1 @@
{"root":["./src/app.tsx","./src/documentprocessor.tsx","./src/experimentalocr.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"}
{"root":["./src/app.tsx","./src/documentprocessor.tsx","./src/experimentalocr.tsx","./src/history.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/documentcard.tsx","./src/components/documentstoprocess.tsx","./src/components/nodocuments.tsx","./src/components/sidebar.tsx","./src/components/successmodal.tsx","./src/components/suggestioncard.tsx","./src/components/suggestionsreview.tsx","./src/components/undocard.tsx"],"version":"5.7.2"}

View file

@ -1 +1 @@
{"root":["./vite.config.ts"],"version":"5.6.2"}
{"root":["./vite.config.ts"],"version":"5.7.2"}