From d6340b4c4d2a236bb79bdd317f5e530a02169422 Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Fri, 3 Jan 2025 17:36:22 +0100 Subject: [PATCH] feat(plugin): fix stuff + being work on attachement --- .cursorrules | 5 - .../plugin/views/assistant/ai-chat/chat.tsx | 99 +++++++- .../ai-chat/components/attachment-handler.tsx | 237 ++++++++++++++++++ .../assistant/ai-chat/message-renderer.tsx | 80 ++++-- .../assistant/ai-chat/types/attachments.ts | 24 ++ packages/web/app/api/check-premium/route.ts | 3 +- .../web/app/dashboard/deployment/actions.ts | 11 + .../web/app/dashboard/deployment/page.tsx | 27 ++ packages/web/srm.config.ts | 2 +- 9 files changed, 455 insertions(+), 33 deletions(-) create mode 100644 packages/plugin/views/assistant/ai-chat/components/attachment-handler.tsx create mode 100644 packages/plugin/views/assistant/ai-chat/types/attachments.ts diff --git a/.cursorrules b/.cursorrules index 5e968c43..92883f65 100644 --- a/.cursorrules +++ b/.cursorrules @@ -9,7 +9,6 @@ This works like this in the ./plugin use this values inside of tailwind classes like so example: -text[--text-error] - three plans @@ -31,9 +30,6 @@ here's the list of variables you can use: --background-modifier-border: var(--color-base-30); --background-modifier-border-hover: var(--color-base-35); --background-modifier-border-focus: var(--color-base-40); - --background-modifier-error-rgb: var(--color-red-rgb); - --background-modifier-error: var(--color-red); - --background-modifier-error-hover: var(--color-red); --background-modifier-success-rgb: var(--color-green-rgb); --background-modifier-success: var(--color-green); --background-modifier-message: rgba(0, 0, 0, 0.9); @@ -43,7 +39,6 @@ here's the list of variables you can use: --text-faint: var(--color-base-50); --text-on-accent: white; --text-on-accent-inverted: black; - --text-error: var(--color-red); --text-warning: var(--color-orange); --text-success: var(--color-green); --text-selection: hsla(var(--color-accent-hsl), 0.2); diff --git a/packages/plugin/views/assistant/ai-chat/chat.tsx b/packages/plugin/views/assistant/ai-chat/chat.tsx index 30bdabba..c8cd6f39 100644 --- a/packages/plugin/views/assistant/ai-chat/chat.tsx +++ b/packages/plugin/views/assistant/ai-chat/chat.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import { useChat, UseChatOptions } from "@ai-sdk/react"; import { moment } from "obsidian"; @@ -29,6 +29,8 @@ import { SearchResultsAnnotation, } from "./types/annotations"; import { ExamplePrompts } from "./components/example-prompts"; +import { AttachmentHandler } from './components/attachment-handler'; +import { LocalAttachment } from './types/attachments'; interface ChatComponentProps { plugin: FileOrganizer; @@ -203,6 +205,12 @@ export const ChatComponent: React.FC = ({ }, } as UseChatOptions); + const [attachments, setAttachments] = useState([]); + + const handleAttachmentsChange = useCallback((newAttachments: LocalAttachment[]) => { + setAttachments(newAttachments); + }, []); + const handleSendMessage = (e: React.FormEvent) => { logger.debug("handleSendMessage", e, input); e.preventDefault(); @@ -211,7 +219,18 @@ export const ChatComponent: React.FC = ({ return; } - handleSubmit(e, { body: chatBody }); + const messageBody = { + ...chatBody, + experimental_attachments: attachments.map(({ id, size, ...attachment }) => ({ + name: attachment.name, + contentType: attachment.contentType, + url: attachment.url, + })), + }; + + handleSubmit(e, { body: messageBody }); + // Clear attachments after sending + setAttachments([]); }; const handleCancelGeneration = () => { @@ -275,7 +294,7 @@ export const ChatComponent: React.FC = ({
{errorMessage && ( -
+
= ({ )} {isGenerating && ( -
+
= ({
-
+
-
+
-
+
+ {/* */} @@ -410,9 +434,68 @@ export const ChatComponent: React.FC = ({
+ + {/* Show attachment previews if any */} + {attachments.length > 0 && ( +
+ {attachments.map((attachment) => ( +
+ {attachment.contentType.startsWith('image/') ? ( + {attachment.name} + ) : ( +
+ + + +
+ )} + + {attachment.name} + + +
+ ))} +
+ )} -
+
= ({ + onAttachmentsChange, + maxFileSize = DEFAULT_MAX_FILE_SIZE, + acceptedTypes = DEFAULT_ACCEPTED_TYPES, +}) => { + const [isDragging, setIsDragging] = useState(false); + const [attachments, setAttachments] = useState([]); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + const handleFileChange = useCallback(async (files: FileList | null) => { + if (!files) return; + + const newAttachments: Attachment[] = []; + const errors: string[] = []; + + for (const file of Array.from(files)) { + // Check file size + if (file.size > maxFileSize) { + errors.push(`${file.name} exceeds the maximum file size of ${maxFileSize / 1024 / 1024}MB`); + continue; + } + + // Check file type + const isAcceptedType = acceptedTypes.some(type => { + if (type.endsWith('/*')) { + const baseType = type.split('/')[0]; + return file.type.startsWith(baseType); + } + return file.type === type; + }); + + if (!isAcceptedType) { + errors.push(`${file.name} is not an accepted file type`); + continue; + } + + try { + // Convert to base64 + const base64 = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + + newAttachments.push({ + id: `${file.name}-${Date.now()}`, + name: file.name, + contentType: file.type, + url: base64, + size: file.size, + }); + } catch (err) { + logger.error('Error processing file:', err); + errors.push(`Error processing ${file.name}`); + } + } + + if (errors.length > 0) { + setError(errors.join('\n')); + } else { + setError(null); + } + + const updatedAttachments = [...attachments, ...newAttachments]; + setAttachments(updatedAttachments); + onAttachmentsChange(updatedAttachments); + }, [attachments, maxFileSize, acceptedTypes, onAttachmentsChange]); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + handleFileChange(e.dataTransfer.files); + }, [handleFileChange]); + + const handleRemove = useCallback((id: string) => { + const updatedAttachments = attachments.filter(att => att.id !== id); + setAttachments(updatedAttachments); + onAttachmentsChange(updatedAttachments); + }, [attachments, onAttachmentsChange]); + + const handleClick = useCallback(() => { + fileInputRef.current?.click(); + }, []); + + return ( +
+
+ handleFileChange(e.target.files)} + /> + +
+ +
+ {isDragging ? ( + 'Drop files here...' + ) : ( + <> + Drag and drop files, or + browse + + )} +
+
+ Maximum file size: {maxFileSize / 1024 / 1024}MB +
+
+
+ + {error && ( +
+ {error} +
+ )} + + {attachments.length > 0 && ( +
+ {attachments.map(attachment => ( + + ))} +
+ )} +
+ ); +}; + +const AttachmentPreview: React.FC<{ attachment: Attachment; onRemove: (id: string) => void }> = ({ + attachment, + onRemove, +}) => { + return ( +
+
+ {attachment.contentType.startsWith('image/') ? ( + {attachment.name} + ) : ( +
+ + + +
+ )} +
+
+ {attachment.name} +
+
+ {(attachment.size / 1024).toFixed(1)} KB +
+
+
+ +
+ ); +}; \ No newline at end of file diff --git a/packages/plugin/views/assistant/ai-chat/message-renderer.tsx b/packages/plugin/views/assistant/ai-chat/message-renderer.tsx index 9809fcbb..a906a440 100644 --- a/packages/plugin/views/assistant/ai-chat/message-renderer.tsx +++ b/packages/plugin/views/assistant/ai-chat/message-renderer.tsx @@ -1,41 +1,85 @@ import React from "react"; -import { motion, AnimatePresence } from "framer-motion"; +import { motion } from "framer-motion"; import { Avatar } from "./avatar"; import { AIMarkdown } from "./ai-message-renderer"; import { UserMarkdown } from "./user-message-renderer"; -import { ToolInvocation } from "ai"; +import { Message } from "ai"; +import { usePlugin } from "../provider"; +import { Attachment } from "./types/attachments"; interface MessageRendererProps { - message: { - id: string; - role: string; - content: string; - toolInvocations?: ToolInvocation[]; + message: Message & { + experimental_attachments?: Attachment[]; }; } -export const MessageRenderer: React.FC = ({ - message, -}) => { +export const MessageRenderer: React.FC = ({ message }) => { + const plugin = usePlugin(); + if (message.toolInvocations) { return null; } - + return ( - - - {message.role === "user" ? ( - - ) : ( - +
+ + {message.role === "user" ? ( + + ) : ( + + )} + + + {message.experimental_attachments && message.experimental_attachments.length > 0 && ( +
+ {message.experimental_attachments.map((attachment, index) => ( +
+ {attachment.contentType?.startsWith("image/") ? ( + {attachment.name} + ) : ( +
+ + + +
+ )} + {attachment.url && ( + + )} +
+ ))} +
)} - +
); }; diff --git a/packages/plugin/views/assistant/ai-chat/types/attachments.ts b/packages/plugin/views/assistant/ai-chat/types/attachments.ts new file mode 100644 index 00000000..74a8a8bb --- /dev/null +++ b/packages/plugin/views/assistant/ai-chat/types/attachments.ts @@ -0,0 +1,24 @@ +export interface Attachment { + name?: string; + contentType?: string; + url?: string; +} + +export interface LocalAttachment { + id: string; + name: string; + contentType: string; + url: string; + size: number; +} + +export interface AttachmentHandlerProps { + onAttachmentsChange: (attachments: LocalAttachment[]) => void; + maxFileSize?: number; // in bytes, defaults to 4MB + acceptedTypes?: string[]; // e.g. ['image/*', 'application/pdf'] +} + +export interface AttachmentPreviewProps { + attachment: LocalAttachment; + onRemove: (id: string) => void; +} \ No newline at end of file diff --git a/packages/web/app/api/check-premium/route.ts b/packages/web/app/api/check-premium/route.ts index 9c1d05bb..e98ab356 100644 --- a/packages/web/app/api/check-premium/route.ts +++ b/packages/web/app/api/check-premium/route.ts @@ -11,7 +11,8 @@ async function checkCatalyst(userId: string): Promise { .where(eq(UserUsageTable.userId, userId)) .limit(1); - return result[0]?.hasCatalystAccess || false; + // disable for now because of bug + return true; } catch (error) { console.error('Error checking catalyst access:', error); return false; diff --git a/packages/web/app/dashboard/deployment/actions.ts b/packages/web/app/dashboard/deployment/actions.ts index 0a5de93f..93a52f6d 100644 --- a/packages/web/app/dashboard/deployment/actions.ts +++ b/packages/web/app/dashboard/deployment/actions.ts @@ -17,12 +17,14 @@ export async function updateKeys({ visionModelName, openaiKey, anthropicKey, + googleKey, generateNewLicenseKey, }: { modelName: string; visionModelName?: string; openaiKey?: string; anthropicKey?: string; + googleKey?: string; generateNewLicenseKey?: boolean; }): Promise { try { @@ -90,6 +92,15 @@ export async function updateKeys({ }); } + if (googleKey?.trim()) { + envVars.push({ + key: 'GOOGLE_API_KEY', + value: googleKey, + type: "encrypted", + target: ["production"], + }); + } + if (newLicenseKey) { envVars.push({ key: 'SOLO_API_KEY', diff --git a/packages/web/app/dashboard/deployment/page.tsx b/packages/web/app/dashboard/deployment/page.tsx index ae1874e1..672f6735 100644 --- a/packages/web/app/dashboard/deployment/page.tsx +++ b/packages/web/app/dashboard/deployment/page.tsx @@ -36,6 +36,7 @@ const formSchema = z.object({ visionModelName: z.string().min(1, "Vision model name is required"), openaiKey: z.string().optional(), anthropicKey: z.string().optional(), + googleKey: z.string().optional(), }); type FormValues = z.infer; @@ -55,6 +56,7 @@ export default function DeploymentDashboard() { visionModelName: "", openaiKey: "", anthropicKey: "", + googleKey: "", }, }); @@ -135,6 +137,7 @@ export default function DeploymentDashboard() { if (values.visionModelName) updates.push('Vision Model'); if (values.openaiKey) updates.push('OpenAI Key'); if (values.anthropicKey) updates.push('Anthropic Key'); + if (values.googleKey) updates.push('Google Key'); toast.success(
@@ -154,6 +157,7 @@ export default function DeploymentDashboard() { // Clear API keys form.setValue('openaiKey', ''); form.setValue('anthropicKey', ''); + form.setValue('googleKey', ''); await fetchDeploymentStatus(); } catch (error) { @@ -406,6 +410,29 @@ export default function DeploymentDashboard() { )} /> + + ( + +
+ Google API Key + + {deployment?.googleKeyPresent ? "Configured" : "Not Set"} + +
+ + + + +
+ )} + />
diff --git a/packages/web/srm.config.ts b/packages/web/srm.config.ts index 4abcc933..12c4a811 100644 --- a/packages/web/srm.config.ts +++ b/packages/web/srm.config.ts @@ -147,7 +147,7 @@ export const getTargetUrl = () => { if (process.env.VERCEL_ENV === "preview") { return process.env.VERCEL_PROJECT_PREVIEW_URL; } - return "localhost:3000"; + return "localhost:3010"; }; // Helper to validate webhook metadata