diff --git a/apps/mail/actions/ai-reply.ts b/apps/mail/actions/ai-reply.ts new file mode 100644 index 0000000000..5f762c8bd9 --- /dev/null +++ b/apps/mail/actions/ai-reply.ts @@ -0,0 +1,100 @@ +'use server'; + +import { headers } from 'next/headers'; +import { auth } from '@/lib/auth'; + +// Function to truncate email thread content to fit within token limits +function truncateThreadContent(threadContent: string, maxTokens: number = 12000): string { + // Split the thread into individual emails + const emails = threadContent.split('\n---\n'); + + // Start with the most recent email (last in the array) + let truncatedContent = emails[emails.length - 1]; + + // Add previous emails until we reach the token limit + for (let i = emails.length - 2; i >= 0; i--) { + const newContent = `${emails[i]}\n---\n${truncatedContent}`; + + // Rough estimation of tokens (1 token ≈ 4 characters) + const estimatedTokens = newContent.length / 4; + + if (estimatedTokens > maxTokens) { + break; + } + + truncatedContent = newContent; + } + + return truncatedContent; +} + +export async function generateAIResponse(threadContent: string, originalSender: string): Promise { + const headersList = await headers(); + const session = await auth.api.getSession({ headers: headersList }); + + if (!session?.user) { + throw new Error('Unauthorized'); + } + + if (!process.env.OPENAI_API_KEY) { + throw new Error('OpenAI API key is not configured'); + } + + // Truncate the thread content to fit within token limits + const truncatedThreadContent = truncateThreadContent(threadContent); + + // Create the prompt for OpenAI + const prompt = ` + You are ${session.user.name}, writing an email reply. + + Here's the context of the email thread: + ${truncatedThreadContent} + + Generate a professional, helpful, and concise email reply to ${originalSender}. + + Requirements: + - Be concise but thorough (2-3 paragraphs maximum) + - Maintain a professional and friendly tone + - Address the key points from the original email + - Close with an appropriate sign-off + - Don't use placeholder text or mention that you're an AI + - Write as if you are (${session.user.name}) + - Don't include the subject line in the reply + - Double space paragraphs (2 newlines) + - Add two spaces bellow the sign-off + `; + + try { + // Direct OpenAI API call using fetch + const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: 'You are a helpful email assistant that generates concise, professional replies.', + }, + { role: 'user', content: prompt }, + ], + temperature: 0.7, + max_tokens: 500, + }), + }); + + if (!openaiResponse.ok) { + const errorData = await openaiResponse.json(); + throw new Error(`OpenAI API Error: ${errorData.error?.message || 'Unknown error'}`); + } + + const data = await openaiResponse.json(); + return data.choices[0]?.message?.content || ''; + } catch (error: any) { + console.error('OpenAI API Error:', error); + throw new Error(`OpenAI API Error: ${error.message || 'Unknown error'}`); + } +} \ No newline at end of file diff --git a/apps/mail/components/create/editor.tsx b/apps/mail/components/create/editor.tsx index 045e214231..30b1f9471d 100644 --- a/apps/mail/components/create/editor.tsx +++ b/apps/mail/components/create/editor.tsx @@ -398,16 +398,11 @@ export default function Editor({ // Function to focus the editor const focusEditor = (e: React.MouseEvent) => { - if (e.target === containerRef.current) { - editorRef.current?.commands.focus('end'); + if (e.target === containerRef.current && editorRef.current?.commands) { + editorRef.current.commands.focus('end'); } }; - // Toggle AI menu - const toggleAIMenu = () => { - dispatch({ type: 'TOGGLE_AI', payload: !openAI }); - }; - // Function to clear editor content const clearEditorContent = React.useCallback(() => { if (editorRef.current) { diff --git a/apps/mail/components/create/prosemirror.css b/apps/mail/components/create/prosemirror.css index 7622627121..b2a52172dc 100644 --- a/apps/mail/components/create/prosemirror.css +++ b/apps/mail/components/create/prosemirror.css @@ -3,7 +3,7 @@ font-size: 1rem; font-family: 'Inter', sans-serif; font-weight: 400; - line-height: 1.4; + } /* Add placeholder styles */ @@ -220,11 +220,7 @@ mark[style] > strong { line-height: 1.4; /* Reduced from 1.6 to 1.4 */ } -/* Adjust space between paragraphs */ -.ProseMirror p { - margin-bottom: 1.25rem; /* Reduced from 1.5rem to 1.25rem */ - line-height: 1.4; /* Reduced from 1.6 to 1.4 */ -} + /* Keep the list indentation the same */ .ProseMirror ul { diff --git a/apps/mail/components/mail/reply-composer.tsx b/apps/mail/components/mail/reply-composer.tsx index 9359a16f63..860af4f670 100644 --- a/apps/mail/components/mail/reply-composer.tsx +++ b/apps/mail/components/mail/reply-composer.tsx @@ -1,16 +1,87 @@ -import { type Dispatch, type SetStateAction, useRef, useState, useEffect, useCallback } from 'react'; +'use client'; + +import { type Dispatch, type SetStateAction, useRef, useState, useEffect, useCallback, useReducer } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { UploadedFileIcon } from '@/components/create/uploaded-file-icon'; -import { ArrowUp, Paperclip, Reply, X, Plus } from 'lucide-react'; -import { cleanEmailAddress, truncateFileName } from '@/lib/utils'; +import { ArrowUp, Paperclip, Reply, X, Plus, Sparkles, Check, X as XIcon } from 'lucide-react'; +import { cleanEmailAddress, truncateFileName, cn, convertJSONToHTML, createAIJsonContent } from '@/lib/utils'; import { Separator } from '@/components/ui/separator'; import Editor from '@/components/create/editor'; import { Button } from '@/components/ui/button'; import type { ParsedMessage } from '@/types'; import { useTranslations } from 'next-intl'; import { sendEmail } from '@/actions/send'; -import { cn } from '@/lib/utils'; import { toast } from 'sonner'; +import type { JSONContent } from 'novel'; +import { useForm } from "react-hook-form"; +import type { z } from "zod"; +import { generateAIResponse } from '@/actions/ai-reply'; + +// Define state interfaces +interface ComposerState { + isUploading: boolean; + isComposerOpen: boolean; + isDragging: boolean; + isEditorFocused: boolean; + editorKey: number; + editorInitialValue?: JSONContent; +} + +interface AIState { + isLoading: boolean; + suggestion: string | null; + showOptions: boolean; +} + +// Define action types +type ComposerAction = + | { type: 'SET_UPLOADING'; payload: boolean } + | { type: 'SET_COMPOSER_OPEN'; payload: boolean } + | { type: 'SET_DRAGGING'; payload: boolean } + | { type: 'SET_EDITOR_FOCUSED'; payload: boolean } + | { type: 'INCREMENT_EDITOR_KEY' } + | { type: 'SET_EDITOR_INITIAL_VALUE'; payload: JSONContent | undefined }; + +type AIAction = + | { type: 'SET_LOADING'; payload: boolean } + | { type: 'SET_SUGGESTION'; payload: string | null } + | { type: 'SET_SHOW_OPTIONS'; payload: boolean } + | { type: 'RESET' }; + +// Create reducers +const composerReducer = (state: ComposerState, action: ComposerAction): ComposerState => { + switch (action.type) { + case 'SET_UPLOADING': + return { ...state, isUploading: action.payload }; + case 'SET_COMPOSER_OPEN': + return { ...state, isComposerOpen: action.payload }; + case 'SET_DRAGGING': + return { ...state, isDragging: action.payload }; + case 'SET_EDITOR_FOCUSED': + return { ...state, isEditorFocused: action.payload }; + case 'INCREMENT_EDITOR_KEY': + return { ...state, editorKey: state.editorKey + 1 }; + case 'SET_EDITOR_INITIAL_VALUE': + return { ...state, editorInitialValue: action.payload }; + default: + return state; + } +}; + +const aiReducer = (state: AIState, action: AIAction): AIState => { + switch (action.type) { + case 'SET_LOADING': + return { ...state, isLoading: action.payload }; + case 'SET_SUGGESTION': + return { ...state, suggestion: action.payload }; + case 'SET_SHOW_OPTIONS': + return { ...state, showOptions: action.payload }; + case 'RESET': + return { isLoading: false, suggestion: null, showOptions: false }; + default: + return state; + } +}; interface ReplyComposeProps { emailData: ParsedMessage[]; @@ -18,24 +89,40 @@ interface ReplyComposeProps { setIsOpen?: Dispatch>; } +type FormData = { + messageContent: string; +}; + export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComposeProps) { - const editorRef = useRef(null); + // Keep attachments separate as it's an array that needs direct manipulation const [attachments, setAttachments] = useState([]); - const [isUploading, setIsUploading] = useState(false); - const [messageContent, setMessageContent] = useState(''); - const [isComposerOpen, setIsComposerOpen] = useState(false); - const [isDragging, setIsDragging] = useState(false); - const [isEditorFocused, setIsEditorFocused] = useState(false); + + // Use reducers instead of multiple useState + const [composerState, composerDispatch] = useReducer(composerReducer, { + isUploading: false, + isComposerOpen: false, + isDragging: false, + isEditorFocused: false, + editorKey: 0, + editorInitialValue: undefined, + }); + + const [aiState, aiDispatch] = useReducer(aiReducer, { + isLoading: false, + suggestion: null, + showOptions: false, + }); + const composerRef = useRef(null); const t = useTranslations(); // Use external state if provided, otherwise use internal state - const composerIsOpen = isOpen !== undefined ? isOpen : isComposerOpen; + const composerIsOpen = isOpen !== undefined ? isOpen : composerState.isComposerOpen; const setComposerIsOpen = (value: boolean) => { if (setIsOpen) { setIsOpen(value); } else { - setIsComposerOpen(value); + composerDispatch({ type: 'SET_COMPOSER_OPEN', payload: value }); } }; @@ -52,12 +139,12 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp const handleAttachment = async (e: React.ChangeEvent) => { if (e.target.files) { - setIsUploading(true); + composerDispatch({ type: 'SET_UPLOADING', payload: true }); try { await new Promise((resolve) => setTimeout(resolve, 500)); setAttachments([...attachments, ...Array.from(e.target.files)]); } finally { - setIsUploading(false); + composerDispatch({ type: 'SET_UPLOADING', payload: false }); } } }; @@ -70,7 +157,7 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) { e.preventDefault(); e.stopPropagation(); - setIsDragging(true); + composerDispatch({ type: 'SET_DRAGGING', payload: true }); } }; @@ -78,7 +165,7 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) { e.preventDefault(); e.stopPropagation(); - setIsDragging(false); + composerDispatch({ type: 'SET_DRAGGING', payload: false }); } }; @@ -86,7 +173,7 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp if (!e.target || !(e.target as HTMLElement).closest('.ProseMirror')) { e.preventDefault(); e.stopPropagation(); - setIsDragging(false); + composerDispatch({ type: 'SET_DRAGGING', payload: false }); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { setAttachments([...attachments, ...Array.from(e.dataTransfer.files)]); @@ -122,6 +209,12 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp `; }; + const form = useForm({ + defaultValues: { + messageContent: '', + }, + }); + const handleSendEmail = async (e: React.MouseEvent) => { e.preventDefault(); try { @@ -137,7 +230,7 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp const messageId = emailData[0]?.messageId; const threadId = emailData[0]?.threadId; - const formattedMessage = messageContent; + const formattedMessage = form.getValues('messageContent'); const replyBody = constructReplyBody( formattedMessage, @@ -166,7 +259,7 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp }, }); - setMessageContent(''); + form.reset(); setComposerIsOpen(false); toast.success(t('pages.createEmail.emailSentSuccessfully')); } catch (error) { @@ -200,8 +293,8 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp // Check if the message is empty const isMessageEmpty = - !messageContent || - messageContent === + !form.getValues('messageContent') || + form.getValues('messageContent') === JSON.stringify({ type: 'doc', content: [ @@ -215,6 +308,71 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp // Check if form is valid for submission const isFormValid = !isMessageEmpty || attachments.length > 0; + const handleAIButtonClick = async () => { + aiDispatch({ type: 'SET_LOADING', payload: true }); + try { + // Extract relevant information from the email thread for context + const latestEmail = emailData[emailData.length - 1]; + const originalSender = latestEmail?.sender?.name || 'the recipient'; + + // Create a summary of the thread content for context + const threadContent = emailData + .map((email) => { + return ` +From: ${email.sender?.name || 'Unknown'} <${email.sender?.email || 'unknown@email.com'}> +Subject: ${email.subject || 'No Subject'} +Date: ${new Date(email.receivedOn || '').toLocaleString()} + +${email.decodedBody || 'No content'} + `; + }) + .join('\n---\n'); + + const suggestion = await generateAIResponse(threadContent, originalSender); + aiDispatch({ type: 'SET_SUGGESTION', payload: suggestion }); + composerDispatch({ + type: 'SET_EDITOR_INITIAL_VALUE', + payload: createAIJsonContent(suggestion) + }); + composerDispatch({ type: 'INCREMENT_EDITOR_KEY' }); + aiDispatch({ type: 'SET_SHOW_OPTIONS', payload: true }); + } catch (error: any) { + console.error('Error generating AI response:', error); + + let errorMessage = 'Failed to generate AI response. Please try again or compose manually.'; + + if (error.message) { + if (error.message.includes('OpenAI API')) { + errorMessage = 'AI service is currently unavailable. Please try again later.'; + } else if (error.message.includes('key is not configured')) { + errorMessage = 'AI service is not properly configured. Please contact support.'; + } + } + + toast.error(errorMessage); + } finally { + aiDispatch({ type: 'SET_LOADING', payload: false }); + } + }; + + const acceptAISuggestion = () => { + if (aiState.suggestion) { + const jsonContent = createAIJsonContent(aiState.suggestion); + const htmlContent = convertJSONToHTML(jsonContent); + + form.setValue('messageContent', htmlContent); + + composerDispatch({ type: 'SET_EDITOR_INITIAL_VALUE', payload: undefined }); + aiDispatch({ type: 'RESET' }); + } + }; + + const rejectAISuggestion = () => { + composerDispatch({ type: 'SET_EDITOR_INITIAL_VALUE', payload: undefined }); + composerDispatch({ type: 'INCREMENT_EDITOR_KEY' }); + aiDispatch({ type: 'RESET' }); + }; + if (!composerIsOpen) { return (
@@ -239,18 +397,17 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp ref={composerRef} className={cn( 'border-border ring-offset-background flex h-fit flex-col space-y-2.5 rounded-[10px] border px-2 py-2 transition-shadow duration-300 ease-in-out', - isEditorFocused ? 'ring-2 ring-[#3D3D3D] ring-offset-1' : '', + composerState.isEditorFocused ? 'ring-2 ring-[#3D3D3D] ring-offset-1' : '', )} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} onSubmit={(e) => { - // Prevent default form submission e.preventDefault(); }} onKeyDown={handleKeyDown} > - {isDragging && ( + {composerState.isDragging && (
@@ -280,24 +437,31 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp
-
-
+ {aiState.showOptions && ( +
+ + AI reply suggestion. Review and edit before sending. +
+ )} + +
+
{ - setMessageContent(content); + form.setValue('messageContent', content); }} - className="sm:max-w-[600px] md:max-w-[2050px]" - placeholder="Type your reply here..." + initialValue={composerState.editorInitialValue} + className={cn( + "sm:max-w-[600px] md:max-w-[2050px]", + aiState.showOptions ? "border border-blue-200 dark:border-blue-800 rounded-md bg-blue-50/30 dark:bg-blue-950/30 p-1" : "" + )} + placeholder={aiState.showOptions ? "AI-generated reply (you can edit)" : "Type your reply here..."} onFocus={() => { - setIsEditorFocused(true); + composerDispatch({ type: 'SET_EDITOR_FOCUSED', payload: true }); }} onBlur={() => { - console.log('Editor blurred'); - setIsEditorFocused(false); + composerDispatch({ type: 'SET_EDITOR_FOCUSED', payload: false }); }} />
@@ -318,6 +482,52 @@ export default function ReplyCompose({ emailData, isOpen, setIsOpen }: ReplyComp {t('common.replyCompose.attachments')} + + {!aiState.showOptions ? ( + + ) : ( +
+ + +
+ )} {attachments.length > 0 && ( diff --git a/apps/mail/lib/utils.ts b/apps/mail/lib/utils.ts index a3acb90b4b..88cd0d1e43 100644 --- a/apps/mail/lib/utils.ts +++ b/apps/mail/lib/utils.ts @@ -158,3 +158,183 @@ export const getFileIcon = (mimeType: string): string => { if (mimeType.includes("image")) return ""; // Empty for images as they're handled separately return "📎"; // Default icon }; + +export const convertJSONToHTML = (json: any): string => { + if (!json) return ""; + + // Handle different types + if (typeof json === "string") return json; + if (typeof json === "number" || typeof json === "boolean") return json.toString(); + if (json === null) return ""; + + // Handle arrays + if (Array.isArray(json)) { + return json.map(item => convertJSONToHTML(item)).join(""); + } + + // Handle objects (assuming they might have specific email content structure) + if (typeof json === "object") { + // Check if it's a text node + if (json.type === "text") { + let text = json.text || ""; + + // Apply formatting if present + if (json.bold) text = `${text}`; + if (json.italic) text = `${text}`; + if (json.underline) text = `${text}`; + if (json.code) text = `${text}`; + + return text; + } + + // Handle paragraph + if (json.type === "paragraph") { + return `

${convertJSONToHTML(json.children)}

`; + } + + // Handle headings + if (json.type?.startsWith("heading-")) { + const level = json.type.split("-")[1]; + return `${convertJSONToHTML(json.children)}`; + } + + // Handle lists + if (json.type === "bulleted-list") { + return `
    ${convertJSONToHTML(json.children)}
`; + } + + if (json.type === "numbered-list") { + return `
    ${convertJSONToHTML(json.children)}
`; + } + + if (json.type === "list-item") { + return `
  • ${convertJSONToHTML(json.children)}
  • `; + } + + // Handle links + if (json.type === "link") { + return `${convertJSONToHTML(json.children)}`; + } + + // Handle images + if (json.type === "image") { + return `${json.alt || ''}`; + } + + // Handle blockquote + if (json.type === "block-quote") { + return `
    ${convertJSONToHTML(json.children)}
    `; + } + + // Handle code blocks + if (json.type === "code-block") { + return `
    ${convertJSONToHTML(json.children)}
    `; + } + + // If it has children property, process it + if (json.children) { + return convertJSONToHTML(json.children); + } + + // Process all other properties + return Object.values(json).map(value => convertJSONToHTML(value)).join(""); + } + + return ""; +}; + +export const createAIJsonContent = (text: string): JSONContent => { + // Try to identify common sign-off patterns with a more comprehensive regex + const signOffPatterns = [ + /\b((?:Best regards|Regards|Sincerely|Thanks|Thank you|Cheers|Best|All the best|Yours truly|Yours sincerely|Cordially)(?:,)?)\s*\n+\s*([A-Za-z][A-Za-z\s.]*)$/i + ]; + + let mainContent = text; + let signatureLines: string[] = []; + + // Extract sign-off if found + for (const pattern of signOffPatterns) { + const match = text.match(pattern); + if (match) { + // Find the index where the sign-off starts + const signOffIndex = text.lastIndexOf(match[0]); + if (signOffIndex > 0) { + // Split the content + mainContent = text.substring(0, signOffIndex).trim(); + + // Split the signature part into separate lines + const signature = text.substring(signOffIndex).trim(); + signatureLines = signature.split(/\n+/).map(line => line.trim()).filter(Boolean); + break; + } + } + } + + // If no signature was found with regex but there are newlines at the end, + // check if the last lines could be a signature + if (signatureLines.length === 0) { + const allLines = text.split(/\n+/); + if (allLines.length > 1) { + // Check if last 1-3 lines might be a signature (short lines at the end) + const potentialSigLines = allLines.slice(-3).filter(line => + line.trim().length < 60 && + !line.trim().endsWith('?') && + !line.trim().endsWith('.') + ); + + if (potentialSigLines.length > 0) { + signatureLines = potentialSigLines; + mainContent = allLines.slice(0, allLines.length - potentialSigLines.length).join('\n').trim(); + } + } + } + + // Split the main content into paragraphs + const paragraphs = mainContent.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean); + + if (paragraphs.length === 0 && signatureLines.length === 0) { + // If no paragraphs and no signature were found, treat the whole text as one paragraph + paragraphs.push(text); + } + + // Create a content array with appropriate spacing between paragraphs + const content = []; + + paragraphs.forEach((paragraph, index) => { + // Add the content paragraph + content.push({ + type: "paragraph", + content: [{ type: "text", text: paragraph }] + }); + + // Add an empty paragraph between main paragraphs + if (index < paragraphs.length - 1) { + content.push({ + type: "paragraph" + }); + } + }); + + // If we found a signature, add it with proper spacing + if (signatureLines.length > 0) { + // Add spacing before the signature if there was content + if (paragraphs.length > 0) { + content.push({ + type: "paragraph" + }); + } + + // Add each line of the signature as a separate paragraph + signatureLines.forEach(line => { + content.push({ + type: "paragraph", + content: [{ type: "text", text: line }] + }); + }); + } + + return { + type: "doc", + content: content + }; +}; diff --git a/bun.lock b/bun.lock index fb5eb97c9d..aa76a37229 100644 --- a/bun.lock +++ b/bun.lock @@ -3,6 +3,9 @@ "workspaces": { "": { "name": "zero", + "dependencies": { + "openai": "^4.89.1", + }, "devDependencies": { "@types/node": "22.13.8", "@zero/tsconfig": "workspace:*", @@ -664,6 +667,8 @@ "@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="], + "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + "@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], @@ -738,12 +743,16 @@ "abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -1064,6 +1073,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -1104,6 +1115,10 @@ "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "framer-motion": ["framer-motion@12.5.0", "", { "dependencies": { "motion-dom": "^12.5.0", "motion-utils": "^12.5.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA=="], "framework-utils": ["framework-utils@1.1.0", "", {}, "sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg=="], @@ -1186,6 +1201,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], @@ -1476,6 +1493,8 @@ "next-themes": ["next-themes@0.4.4", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], @@ -1508,6 +1527,8 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "openai": ["openai@4.89.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-k6t7WfnodIctPo40/9sy7Ww4VypnfkKi/urO2VQx4trCIwgzeroO1jkaCL2f5MyTS1H3HT9X+M2qLsc7NSXwTw=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], @@ -1952,6 +1973,8 @@ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], @@ -2054,6 +2077,8 @@ "novel/cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "openai/@types/node": ["@types/node@18.19.84", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ACYy2HGcZPHxEeWTqowTF7dhXN+JU1o7Gr4b41klnn6pj2LD6rsiGqSZojMdk1Jh2ys3m76ap+ae1vvE4+5+vg=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "postcss-import/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -2184,6 +2209,8 @@ "js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], "prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], diff --git a/package.json b/package.json index a956d98c00..5cb975fc20 100644 --- a/package.json +++ b/package.json @@ -35,5 +35,8 @@ "workspaces": [ "apps/*", "packages/*" - ] + ], + "dependencies": { + "openai": "^4.89.1" + } }