diff --git a/.gitignore b/.gitignore index 33afb52..44c9bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ template/ *node_modules* million/ -wrangler/ \ No newline at end of file +wrangler/ +DemoApp \ No newline at end of file diff --git a/docs/akiradocs.config.json b/docs/akiradocs.config.json index d1f8bb9..567ffc7 100644 --- a/docs/akiradocs.config.json +++ b/docs/akiradocs.config.json @@ -78,6 +78,13 @@ "enabled": true }, "debug": false + }, + "rewrite": { + "provider": "anthropic", + "settings": { + "model": "claude-3-sonnet-20240229", + "temperature": 0.7 + } }, "features": { "textToSpeech": false diff --git a/docs/package-lock.json b/docs/package-lock.json index 0c5b59b..276555a 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,10 +8,11 @@ "name": "akiradocs", "version": "1.0.1", "dependencies": { - "@ai-sdk/anthropic": "^1.0.1", + "@anthropic-ai/sdk": "^0.17.1", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", + "@google/generative-ai": "^0.2.1", "@heroicons/react": "^2.1.5", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", @@ -49,6 +50,7 @@ "lucide-react": "^0.453.0", "next": "^15.0.1", "next-themes": "^0.3.0", + "openai": "^4.28.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-error-boundary": "^4.1.2", @@ -689,10 +691,95 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, + "node_modules/@google/generative-ai": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.2.1.tgz", + "integrity": "sha512-gNmMFadfwi7qf/6M9gImgyGJXY1jKQ/de8vGOqgJ0PPYgQ7WwzZDavbKrIuXS2zdqZZaYtxW3EFN6aG9x5wtFw==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@heroicons/react": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.5.tgz", + "integrity": "sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA==", + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], @@ -4297,6 +4384,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6723,6 +6818,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-bun-module": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", @@ -7501,6 +7601,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", diff --git a/docs/package.json b/docs/package.json index d6c7a0e..e5e7901 100644 --- a/docs/package.json +++ b/docs/package.json @@ -53,6 +53,7 @@ "lucide-react": "^0.453.0", "next": "^15.0.1", "next-themes": "^0.3.0", + "openai": "^4.28.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-error-boundary": "^4.1.2", @@ -61,7 +62,9 @@ "sonner": "^1.7.0", "styled-components": "^6.1.13", "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "@anthropic-ai/sdk": "^0.17.1", + "@google/generative-ai": "^0.2.1" }, "devDependencies": { "@tailwindcss/typography": "^0.5.15", diff --git a/docs/src/app/api/rewrite/route.ts b/docs/src/app/api/rewrite/route.ts new file mode 100644 index 0000000..a0ceae7 --- /dev/null +++ b/docs/src/app/api/rewrite/route.ts @@ -0,0 +1,186 @@ +import { NextResponse } from 'next/server'; +import OpenAI from 'openai'; +import Anthropic from '@anthropic-ai/sdk'; +import { GoogleGenerativeAI } from '@google/generative-ai'; +import { getAkiradocsConfig } from '@/lib/getAkiradocsConfig'; +import { validateConfig, getProviderConfig, isAzureProvider } from '@/lib/AIConfig'; +import type { SupportedProvider } from '@/types/AkiraConfigType'; + +// Define block-specific system prompts +const blockPrompts = { + paragraph: { + system: "You are an expert content editor. Maintain paragraph structure and formatting. Output only the rewritten paragraph text without any markdown or HTML.", + format: "Plain text paragraph" + }, + heading: { + system: "You are a headline optimization expert. Create impactful, concise headings. Output only the heading text without any formatting.", + format: "Single line heading text" + }, + code: { + system: "You are an expert code optimizer. Maintain the exact programming language syntax and structure. Output only valid code without any explanations.", + format: "Valid code in the original language" + }, + list: { + system: "You are a list organization expert. Maintain the list structure with one item per line. Do not include bullets or numbers.", + format: "One item per line, no bullets/numbers" + }, + blockquote: { + system: "You are a quote refinement expert. Maintain the quote's core message and emotional impact. Output only the quote text.", + format: "Single quote text without quotation marks" + }, + callout: { + system: "You are a technical documentation expert. Maintain the callout's type (info/warning/success/error) and structure. Output only the callout content.", + format: "Callout content preserving type indicators" + }, + image: { + system: "You are an image description expert. Optimize image alt text and captions. Output in JSON format with alt and caption fields.", + format: '{ "alt": "...", "caption": "..." }' + } +}; + +// Provider-specific implementations +const providers = { + openai: async (config: any, messages: any[]) => { + const openai = new OpenAI({ + apiKey: config.apiKey || '', + }); + + const completion = await openai.chat.completions.create({ + model: config.model || 'gpt-4', + temperature: config.temperature || 0.7, + messages, + }); + + return completion.choices[0].message.content || ''; + }, + + azure: async (config: any, messages: any[]) => { + const openai = new OpenAI({ + apiKey: config.apiKey || '', + baseURL: config.endpoint, + defaultQuery: { 'api-version': '2024-02-15-preview' }, + defaultHeaders: { 'api-key': config.apiKey || '' }, + }); + + const completion = await openai.chat.completions.create({ + model: config.deploymentName, + temperature: config.temperature || 0.7, + messages + }); + + return completion.choices[0].message.content || ''; + }, + + anthropic: async (config: any, messages: any[]) => { + const anthropic = new Anthropic({ + apiKey: config.apiKey || '', + }); + + // Convert messages format to Anthropic's format + const systemMessage = messages.find(m => m.role === 'system')?.content || ''; + const userMessage = messages.find(m => m.role === 'user')?.content || ''; + + const message = await anthropic.messages.create({ + model: config.model || 'claude-3-sonnet', + max_tokens: 1024, + temperature: config.temperature || 0.7, + system: systemMessage, + messages: [{ role: 'user', content: userMessage }], + }); + + return message.content[0].text; + }, + + google: async (config: any, messages: any[]) => { + const genAI = new GoogleGenerativeAI(config.apiKey || ''); + const model = genAI.getGenerativeModel({ + model: config.model || 'gemini-pro', + }); + + // Convert messages format to Google's format + const systemMessage = messages.find(m => m.role === 'system')?.content || ''; + const userMessage = messages.find(m => m.role === 'user')?.content || ''; + const prompt = `${systemMessage}\n\n${userMessage}`; + + const result = await model.generateContent(prompt); + const response = await result.response; + return response.text(); + }, +}; + +export async function POST(request: Request) { + try { + validateConfig(); + const config = getProviderConfig(); + const akiraConfig = getAkiradocsConfig(); + const provider = akiraConfig.rewrite?.provider || 'openai'; + + const { content, blockType, style } = await request.json(); + + if (!content || !blockType || !style) { + return NextResponse.json( + { error: 'Missing required fields: content, blockType, or style' }, + { status: 400 } + ); + } + + const blockPrompt = blockPrompts[blockType as keyof typeof blockPrompts]; + if (!blockPrompt) { + return NextResponse.json( + { error: 'Invalid block type' }, + { status: 400 } + ); + } + + const messages = [ + { + role: "system", + content: `${blockPrompt.system}\n\nExpected output format: ${blockPrompt.format}\n\nDo not include any explanations or markdown formatting in the response.` + }, + { + role: "user", + content: `Rewrite the following ${blockType} content in a ${style} style while maintaining its structure:\n\n${content}` + } + ]; + + const providerImpl = providers[provider as SupportedProvider]; + if (!providerImpl) { + return NextResponse.json( + { error: `Unsupported AI provider: ${provider}` }, + { status: 400 } + ); + } + + try { + const rewrittenContent = await providerImpl(config, messages); + return NextResponse.json({ content: rewrittenContent }); + } catch (providerError: any) { + console.error('Provider API error:', providerError); + return NextResponse.json( + { + error: providerError.message || 'Provider API error', + details: providerError.response?.data || providerError.response || providerError + }, + { status: 500 } + ); + } + + } catch (error: any) { + console.error('AI API error:', error); + + if (error instanceof Error) { + return NextResponse.json( + { + error: error.message, + details: error + }, + { status: error.message.includes('API key') ? 400 : 500 } + ); + } + + return NextResponse.json( + { error: 'Failed to generate AI content', details: error }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/docs/src/components/editor/AIRewriteButton.tsx b/docs/src/components/editor/AIRewriteButton.tsx index c398ed7..cd3df17 100644 --- a/docs/src/components/editor/AIRewriteButton.tsx +++ b/docs/src/components/editor/AIRewriteButton.tsx @@ -4,6 +4,7 @@ import { Wand2 } from 'lucide-react' import { useState } from 'react' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { BlockType } from '@/types/Block' +import { toast } from 'sonner' const blockStyles = { article: [ @@ -156,6 +157,18 @@ export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewrite return blockStyles[blockType] || blockStyles['paragraph']; }; + const handleRewrite = async () => { + try { + await onRewrite(style) + } catch (error) { + if (error instanceof Error && error.message.toLowerCase().includes('API key')) { + toast.error(error.message) + } else { + toast.error('Failed to rewrite content') + } + } + } + return ( @@ -184,7 +197,7 @@ export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewrite