From df40962159a43114f44fb9478c7b4e521d69f731 Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Fri, 8 Nov 2024 20:28:34 +0100 Subject: [PATCH] feat: update form --- .../views/organizer/components/error-box.tsx | 43 ++++++ .../components/license-validator.tsx | 88 +++++++++++++ plugin/views/organizer/folders/box.tsx | 122 ++++++++++++------ plugin/views/organizer/folders/hooks.tsx | 66 ---------- plugin/views/organizer/view.tsx | 19 ++- plugin/views/settings/general-tab.tsx | 73 ++++++++++- web/app/api/(newai)/folders/v2/route.ts | 4 +- web/app/api/usage/route.ts | 32 +++++ web/app/components/license-form.tsx | 4 +- web/app/dashboard/lifetime/page.tsx | 14 -- 10 files changed, 340 insertions(+), 125 deletions(-) create mode 100644 plugin/views/organizer/components/error-box.tsx create mode 100644 plugin/views/organizer/components/license-validator.tsx delete mode 100644 plugin/views/organizer/folders/hooks.tsx create mode 100644 web/app/api/usage/route.ts diff --git a/plugin/views/organizer/components/error-box.tsx b/plugin/views/organizer/components/error-box.tsx new file mode 100644 index 00000000..0a76e632 --- /dev/null +++ b/plugin/views/organizer/components/error-box.tsx @@ -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 = ({ + message, + description, + actionButton, +}) => { + return ( + +
+
+
+
+ {message} +
+ {description && ( +

+ {description} +

+ )} +
+
+ + {actionButton && ( +
+ {actionButton} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/plugin/views/organizer/components/license-validator.tsx b/plugin/views/organizer/components/license-validator.tsx new file mode 100644 index 00000000..5b1e13bb --- /dev/null +++ b/plugin/views/organizer/components/license-validator.tsx @@ -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 = ({ + apiKey, + onValidationComplete, + plugin, +}) => { + const [isValidating, setIsValidating] = React.useState(true); + const [licenseError, setLicenseError] = React.useState(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 ; + } + + if (licenseError) { + return ( + + + + + } + /> + ); + } + + return null; +}; \ No newline at end of file diff --git a/plugin/views/organizer/folders/box.tsx b/plugin/views/organizer/folders/box.tsx index f13f5d4e..ce570c21 100644 --- a/plugin/views/organizer/folders/box.tsx +++ b/plugin/views/organizer/folders/box.tsx @@ -21,61 +21,105 @@ export const SimilarFolderBox: React.FC = ({ const [suggestions, setSuggestions] = React.useState([]); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(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 = () => ( + +
+
+
+ Error: Failed to fetch +
+

+ {error?.message || "An unexpected error occurred"} +

+
+
+ +
+
+ + +
+
+
+ ); + const renderContent = () => { if (loading) { return ; } if (error) { - return ( -
-

Error: {error.message}

- -
- ); + return renderError(); } if (existingFolders.length === 0 && newFolders.length === 0) { diff --git a/plugin/views/organizer/folders/hooks.tsx b/plugin/views/organizer/folders/hooks.tsx deleted file mode 100644 index 943a09e5..00000000 --- a/plugin/views/organizer/folders/hooks.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useState, useEffect } from "react"; -import FileOrganizer from "../../../index"; -import { TFile } from "obsidian"; -import { logMessage } from "../../../../utils"; - -interface UseFolderSuggestionsProps { - plugin: FileOrganizer; - file: TFile | null; - content: string; - refreshKey: number; -} - -interface FolderSuggestion { - isNewFolder: boolean; - folder: string; - reason: string; -} - -export const useFolderSuggestions = ({ - plugin, - file, - content, - refreshKey, -}: UseFolderSuggestionsProps) => { - const [suggestions, setSuggestions] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const suggestFolders = 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); - } - }; - - suggestFolders(); - }, [content, refreshKey, file, plugin]); - - // Derive existing and new folders from suggestions - const existingFolders = suggestions - .filter(s => !s.isNewFolder) - .map(s => s.folder); - - const newFolders = suggestions - .filter(s => s.isNewFolder) - .map(s => s.folder); - - return { - existingFolders, - newFolders, - suggestions, // Include full suggestions with reasons - loading, - error - }; -}; diff --git a/plugin/views/organizer/view.tsx b/plugin/views/organizer/view.tsx index c3ffb680..70c221b8 100644 --- a/plugin/views/organizer/view.tsx +++ b/plugin/views/organizer/view.tsx @@ -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; @@ -33,18 +35,21 @@ export const AssistantView: React.FC = ({ const [noteContent, setNoteContent] = React.useState(""); const [refreshKey, setRefreshKey] = React.useState(0); const [error, setError] = React.useState(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 @@ -107,6 +112,18 @@ export const AssistantView: React.FC = ({ [] ); + + + if (!isLicenseValid) { + return ( + setIsLicenseValid(true)} + plugin={plugin} + /> + ); + } + if (error) { return ( = ({ plugin }) => { const [licenseKey, setLicenseKey] = useState(plugin.settings.API_KEY); + const [usageData, setUsageData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchUsageData(); + }, []); + + const fetchUsageData = async () => { + try { + const response = await fetch(`${plugin.getServerUrl()}/api/usage`, { + headers: { + Authorization: `Bearer ${plugin.settings.API_KEY}`, + }, + }); + + if (!response.ok) throw new Error('Failed to fetch usage data'); + + const data = await response.json(); + setUsageData(data); + } catch (error) { + console.error('Error fetching usage data:', error); + new Notice('Failed to fetch usage data'); + } finally { + setLoading(false); + } + }; + // log usage data + console.log(usageData, 'usage'); const handleLicenseKeyChange = async (value: string) => { setLicenseKey(value); @@ -24,6 +59,10 @@ export const GeneralTab: React.FC = ({ plugin }) => { } }; + const formatNumber = (num: number) => { + return new Intl.NumberFormat().format(num); + }; + return (
@@ -96,6 +135,38 @@ export const GeneralTab: React.FC = ({ plugin }) => {

+ + {usageData && ( +
+
+
Token Usage
+
+ Your current token usage and limits +
+
+
+
+
+
+
+
+ {formatNumber(usageData.tokenUsage)} / {formatNumber(usageData.maxTokenUsage)} tokens +
+
+ Current Plan: {usageData.currentPlan || 'Free'} +
+
+
+
+ )}
); }; diff --git a/web/app/api/(newai)/folders/v2/route.ts b/web/app/api/(newai)/folders/v2/route.ts index f5d9aa63..0ff2b30a 100644 --- a/web/app/api/(newai)/folders/v2/route.ts +++ b/web/app/api/(newai)/folders/v2/route.ts @@ -23,9 +23,8 @@ export async function POST(request: NextRequest) { reason: z.string(), }) ) - .max(5), }), - system: `Given the content and (if useful) the file name: "${fileName}", suggest relevant folders from the following list: ${folders.join( + system: `Given the content and (if useful) the file name: "${fileName}", suggest at least 3 folders you can use the following list: ${folders.join( ", if none of the folders are relevant, suggest new folders" )}, ${ customInstructions @@ -38,6 +37,7 @@ export async function POST(request: NextRequest) { const tokens = response.usage.totalTokens; console.log("incrementing token usage folders", userId, tokens); await incrementAndLogTokenUsage(userId, tokens); + return NextResponse.json({ folders: response.object.suggestedFolders.sort( (a, b) => b.score - a.score diff --git a/web/app/api/usage/route.ts b/web/app/api/usage/route.ts new file mode 100644 index 00000000..d14fb478 --- /dev/null +++ b/web/app/api/usage/route.ts @@ -0,0 +1,32 @@ +import { NextResponse, NextRequest } from "next/server"; +import { handleAuthorization } from "@/lib/handleAuthorization"; +import { db, UserUsageTable } from "@/drizzle/schema"; +import { eq } from "drizzle-orm"; + +export async function GET(request: NextRequest) { + try { + const { userId } = await handleAuthorization(request); + + const userUsage = await db + .select({ + tokenUsage: UserUsageTable.tokenUsage, + maxTokenUsage: UserUsageTable.maxTokenUsage, + subscriptionStatus: UserUsageTable.subscriptionStatus, + currentPlan: UserUsageTable.currentPlan + }) + .from(UserUsageTable) + .where(eq(UserUsageTable.userId, userId)) + .limit(1); + + if (!userUsage.length) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json(userUsage[0]); + } catch (error) { + return NextResponse.json( + { error: error.message }, + { status: error.status || 500 } + ); + } +} \ No newline at end of file diff --git a/web/app/components/license-form.tsx b/web/app/components/license-form.tsx index bfc8f1f4..69eccaba 100644 --- a/web/app/components/license-form.tsx +++ b/web/app/components/license-form.tsx @@ -56,10 +56,10 @@ const LicenseForm = () => { return ( // center elements -
+
{isPaid ? ( <> - +
diff --git a/web/app/dashboard/lifetime/page.tsx b/web/app/dashboard/lifetime/page.tsx index beb0c28b..14affc52 100644 --- a/web/app/dashboard/lifetime/page.tsx +++ b/web/app/dashboard/lifetime/page.tsx @@ -47,19 +47,6 @@ export default async function LifetimeAccessPage() {
  1. Deploy your instance: -
    - - Deploy with Vercel - - or -
    • You'll need to sign up/in on Vercel and GitHub.