Skip to content

Commit

Permalink
feat: update form
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminshafii committed Nov 8, 2024
1 parent 761ec43 commit df40962
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 125 deletions.
43 changes: 43 additions & 0 deletions plugin/views/organizer/components/error-box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from "react";
import { motion } from "framer-motion";

interface ErrorBoxProps {
message: string;
description?: string;
actionButton?: React.ReactNode;
}

export const ErrorBox: React.FC<ErrorBoxProps> = ({
message,
description,
actionButton,
}) => {
return (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="p-4 bg-[--background-primary-alt] rounded-lg shadow-md"
>
<div className="space-y-3">
<div className="flex items-start justify-between">
<div>
<div className="text-[--text-error] font-medium mb-1">
{message}
</div>
{description && (
<p className="text-sm text-[--text-muted]">
{description}
</p>
)}
</div>
</div>

{actionButton && (
<div className="flex items-center gap-2 mt-4">
{actionButton}
</div>
)}
</div>
</motion.div>
);
};
88 changes: 88 additions & 0 deletions plugin/views/organizer/components/license-validator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as React from "react";
import { ErrorBox } from "./error-box";
import { EmptyState } from "./empty-state";
import FileOrganizer from "../../..";

interface LicenseValidatorProps {
apiKey: string;
onValidationComplete: () => void;
plugin: FileOrganizer;
}

export const LicenseValidator: React.FC<LicenseValidatorProps> = ({
apiKey,
onValidationComplete,
plugin,
}) => {
const [isValidating, setIsValidating] = React.useState(true);
const [licenseError, setLicenseError] = React.useState<string | null>(null);

const validateLicense = React.useCallback(async () => {
try {
setIsValidating(true);
setLicenseError(null);

// should be replaced with a hardcoded value
const response = await fetch(`${plugin.getServerUrl()}/api/check-key`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
});

const data = await response.json();

if (!response.ok) {
setLicenseError(data.error || "Invalid license key");
} else if (data.message !== "Valid key") {
setLicenseError("Invalid license key response");
} else {
onValidationComplete();
}
} catch (err) {
setLicenseError("Failed to validate license key");
} finally {
setIsValidating(false);
}
}, [apiKey, onValidationComplete]);

React.useEffect(() => {
validateLicense();
}, [validateLicense]);

if (isValidating) {
return <EmptyState message="Validating license key..." />;
}

if (licenseError) {
return (
<ErrorBox
message={`License key error: ${licenseError}`}
description="Please check your license key in the plugin settings."
actionButton={
<div className="flex gap-2">
<button
onClick={validateLicense}
className="px-3 py-1.5 text-[--text-on-accent] rounded hover:opacity-90 transition-opacity duration-200"
>
Retry
</button>
<button
onClick={() => {
// Open Obsidian settings and navigate to plugin settings
plugin.app.setting.open();
plugin.app.setting.openTabById("fileorganizer2000");
}}
className="px-3 py-1.5 bg-[--interactive-accent] text-[--text-on-accent] rounded hover:bg-[--interactive-accent-hover] transition-colors duration-200"
>
Open Settings
</button>
</div>
}
/>
);
}

return null;
};
122 changes: 83 additions & 39 deletions plugin/views/organizer/folders/box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,61 +21,105 @@ export const SimilarFolderBox: React.FC<SimilarFolderBoxProps> = ({
const [suggestions, setSuggestions] = React.useState<FolderSuggestion[]>([]);
const [loading, setLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<Error | null>(null);
const [retryCount, setRetryCount] = React.useState(0);

React.useEffect(() => {
const suggestFolders = async () => {
if (!file) return;
setSuggestions([]);
setLoading(true);
setError(null);
const suggestFolders = React.useCallback(async () => {
if (!file) return;
setSuggestions([]);
setLoading(true);
setError(null);

try {
const folderSuggestions = await plugin.guessRelevantFolders(
content,
file.path
);
setSuggestions(folderSuggestions);
} catch (err) {
console.error("Error fetching folders:", err);
setError(err as Error);
} finally {
setLoading(false);
}
};
try {
const folderSuggestions = await plugin.guessRelevantFolders(
content,
file.path
);
setSuggestions(folderSuggestions);
} catch (err) {
console.error("Error fetching folders:", err);
const errorMessage = typeof err === 'object' && err !== null
? (err.error?.message || err.error || err.message || "Unknown error")
: String(err);

setError(new Error(errorMessage));
} finally {
setLoading(false);
}
}, [content, file, plugin]);

React.useEffect(() => {
suggestFolders();
}, [content, refreshKey, file, plugin]);
}, [suggestFolders, refreshKey]);

// Derive existing and new folders from suggestions
const existingFolders = suggestions.filter(s => !s.isNewFolder);
const newFolders = suggestions.filter(s => s.isNewFolder);
const handleRetry = () => {
setRetryCount(prev => prev + 1);
suggestFolders();
};

const handleFolderClick = async (folder: string) => {
if (file) {
try {
await plugin.moveFile(file, file.basename, folder);
new Notice(`Moved ${file.basename} to ${folder}`);
} catch (error) {
console.error("Error moving file:", error);
new Notice(`Failed to move ${file.basename} to ${folder}`);
}
if (!file) return;

setLoading(true);
try {
await plugin.moveFile(file, file.basename, folder);
new Notice(`Moved ${file.basename} to ${folder}`);
} catch (error) {
console.error("Error moving file:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
new Notice(`Failed to move ${file.basename} to ${folder}: ${errorMessage}`);
} finally {
setLoading(false);
}
};

// Derive existing and new folders from suggestions
const existingFolders = suggestions.filter(s => !s.isNewFolder);
const newFolders = suggestions.filter(s => s.isNewFolder);

const renderError = () => (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-3 rounded-md border-opacity-20"
>
<div className="flex items-start justify-between">
<div>
<div className="text-[--text-error] font-medium mb-1">
Error: Failed to fetch
</div>
<p className="text-sm text-[--text-muted]">
{error?.message || "An unexpected error occurred"}
</p>
</div>
</div>

<div className="flex items-center gap-3 text-sm">
<div className="flex gap-2">
<button
onClick={handleRetry}
disabled={loading}
className="px-3 py-1.5 bg-[--interactive-accent] text-[--text-on-accent] rounded hover:bg-[--interactive-accent-hover] disabled:opacity-50 transition-colors duration-200"
>
{loading ? "Retrying..." : "Retry"}
</button>
<button
onClick={() => setError(null)}
className="px-3 py-1.5 border border-[--background-modifier-border] rounded hover:bg-[--background-modifier-hover] transition-colors duration-200"
>
Dismiss
</button>
</div>
</div>
</motion.div>
);

const renderContent = () => {
if (loading) {
return <SkeletonLoader count={4} width="100px" height="30px" rows={1} />;
}

if (error) {
return (
<div className="text-[--text-error] p-2 rounded-md bg-[--background-modifier-error]">
<p>Error: {error.message}</p>
<button className="mt-2 px-3 py-1 bg-[--interactive-accent] text-white rounded-md hover:bg-[--interactive-accent-hover]">
Retry
</button>
</div>
);
return renderError();
}

if (existingFolders.length === 0 && newFolders.length === 0) {
Expand Down
66 changes: 0 additions & 66 deletions plugin/views/organizer/folders/hooks.tsx

This file was deleted.

19 changes: 18 additions & 1 deletion plugin/views/organizer/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { ClassificationContainer } from "./ai-format/templates";
import { TranscriptionButton } from "./transcript";
import { SimilarFilesBox } from "./files";
import { EmptyState } from "./components/empty-state";
import { ErrorBox } from "./components/error-box";
import { logMessage } from "../../../utils";
import { LicenseValidator } from "./components/license-validator";

interface AssistantViewProps {
plugin: FileOrganizer;
Expand All @@ -33,18 +35,21 @@ export const AssistantView: React.FC<AssistantViewProps> = ({
const [noteContent, setNoteContent] = React.useState<string>("");
const [refreshKey, setRefreshKey] = React.useState<number>(0);
const [error, setError] = React.useState<string | null>(null);
const [isLicenseValid, setIsLicenseValid] = React.useState(false);

const isMediaFile = React.useMemo(
() => checkIfIsMediaFile(activeFile),
[activeFile]
);

const isInIgnoredPatterns = React.useMemo(
// check if tis is part of an ignored folder
() =>
plugin
.getAllIgnoredFolders()
.some(folder => activeFile?.path.startsWith(folder)),
[activeFile, plugin.getAllIgnoredFolders]
);

const updateActiveFile = React.useCallback(async () => {
logMessage("updating active file");
// Check if the Assistant view is visible before processing
Expand Down Expand Up @@ -107,6 +112,18 @@ export const AssistantView: React.FC<AssistantViewProps> = ({
[]
);



if (!isLicenseValid) {
return (
<LicenseValidator
apiKey={plugin.settings.API_KEY}
onValidationComplete={() => setIsLicenseValid(true)}
plugin={plugin}
/>
);
}

if (error) {
return (
<EmptyState
Expand Down
Loading

0 comments on commit df40962

Please sign in to comment.