diff --git a/docs/src/app/[locale]/[type]/[...slug]/page.tsx b/docs/src/app/[locale]/[type]/[...slug]/page.tsx
index 5d0fa0d..15a5933 100644
--- a/docs/src/app/[locale]/[type]/[...slug]/page.tsx
+++ b/docs/src/app/[locale]/[type]/[...slug]/page.tsx
@@ -17,6 +17,7 @@ import { PageNavigation } from '@/components/layout/PageNavigation'
import { MainTitle, SubTitle } from '@/components/blocks/HeadingBlock'
import { SEO } from '@/components/layout/SEO'
import { NotFound } from '@/components/layout/NotFound'
+import { TextToSpeech } from '@/components/tts/TextToSpeech'
export const runtime = 'edge'
@@ -61,27 +62,31 @@ export default function ContentPage({ params }: { params: Promise<{ locale: stri
+{children}); diff --git a/packages/akiradocs/src/components/blocks/CalloutBlock.tsx b/packages/akiradocs/src/components/blocks/CalloutBlock.tsx index b0b3589..62532c7 100644 --- a/packages/akiradocs/src/components/blocks/CalloutBlock.tsx +++ b/packages/akiradocs/src/components/blocks/CalloutBlock.tsx @@ -1,6 +1,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Info, AlertTriangle, CheckCircle, XCircle } from 'lucide-react' import { cn } from '@/lib/utils' +import { useEffect, useRef } from 'react' type CalloutType = 'info' | 'warning' | 'success' | 'error' @@ -15,6 +16,8 @@ interface CalloutProps { italic?: boolean; underline?: boolean; }; + isEditing?: boolean; + onUpdate?: (content: string) => void; } const calloutStyles: Record= { @@ -24,9 +27,23 @@ const calloutStyles: Record (null); + + const adjustHeight = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = '0px'; + textarea.style.height = `${textarea.scrollHeight}px`; + } + }; + + useEffect(() => { + if (isEditing) { + adjustHeight(); + } + }, [isEditing, children]); return ( ) diff --git a/packages/akiradocs/src/components/blocks/CodeBlock.tsx b/packages/akiradocs/src/components/blocks/CodeBlock.tsx index 4c24b69..3c8e19b 100644 --- a/packages/akiradocs/src/components/blocks/CodeBlock.tsx +++ b/packages/akiradocs/src/components/blocks/CodeBlock.tsx @@ -2,7 +2,6 @@ import { useState } from 'react'; import { Check, Copy } from 'lucide-react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { useAnalytics } from '@/hooks/useAnalytics'; interface CodeBlockProps { id?: string; @@ -10,7 +9,9 @@ interface CodeBlockProps { language?: string; filename?: string; showLineNumbers?: boolean; - align?: 'left' | 'center' | 'right'; // Add align prop + align?: 'left' | 'center' | 'right'; + isEditing?: boolean; + onUpdate?: (content: string) => void; } export function CodeBlock({ @@ -19,22 +20,50 @@ export function CodeBlock({ language = 'typescript', filename, showLineNumbers = true, - align = 'left' + align = 'left', + isEditing = false, + onUpdate }: CodeBlockProps) { const [copied, setCopied] = useState(false); const alignClass = align === 'center' ? 'mx-auto' : align === 'right' ? 'ml-auto' : ''; - const { track } = useAnalytics() - const copyToClipboard = async () => { - track('copy_code_button_click', { - code_type: 'code_block', - code_text: code - }) + const copyToClipboard = async () => { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }; + if (!isEditing) { + return ( + {title &&{title} } -{children} ++ {isEditing ? ( + + {filename && ( ++ ); + } + return (+ {filename} ++ )} + + + ++ {code} + +{filename && ( @@ -50,17 +79,52 @@ export function CodeBlock({ {copied ?: } - - {code} - ++++ ++ {code} + +{ + const target = e.target as HTMLElement; + if (!target) return; + + let content = target.innerHTML; + content = content.replace(/); } diff --git a/packages/akiradocs/src/components/blocks/HeadingBlock.tsx b/packages/akiradocs/src/components/blocks/HeadingBlock.tsx index 94c0a0f..a93c186 100644 --- a/packages/akiradocs/src/components/blocks/HeadingBlock.tsx +++ b/packages/akiradocs/src/components/blocks/HeadingBlock.tsx @@ -11,9 +11,11 @@ interface HeadingProps { italic?: boolean; underline?: boolean; }; + isEditing?: boolean; + onUpdate?: (content: string) => void; } -export function HeadingTitle({ id, level, children, align = 'left', styles }: HeadingProps) { +export function HeadingTitle({ id, level, children, align = 'left', styles, isEditing, onUpdate }: HeadingProps) { const Tag = `h${level}` as keyof JSX.IntrinsicElements; const sizeClasses = { 1: 'text-4xl', @@ -25,6 +27,28 @@ export function HeadingTitle({ id, level, children, align = 'left', styles }: He }; const alignClass = align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : ''; + if (isEditing) { + return ( +
<\/div>/g, '\n') + content = content.replace(//g, '\n') + content = content.replace(/<\/div>/g, '') + content = content.replace(/
/g, '\n') + content = content.replace(/
/g, '\n') + content = content.replace(/ /g, ' ') + content = content.replace(/\n\n+/g, '\n\n') + content = content.trim() + + onUpdate?.(content); + }} + className="absolute inset-0 m-0 whitespace-pre-wrap bg-[#282c34] focus:bg-[#282c34] focus:opacity-100 opacity-0 focus:outline-none" + style={{ + ...oneDark['pre[class*="language-"]'], + ...oneDark['code[class*="language-"]'], + padding: '1rem', + margin: 0, + lineHeight: 'inherit', + }} + dangerouslySetInnerHTML={{ __html: code.replace(/\n/g, '
') }} + /> +{ + const target = e.target as HTMLElement; + if (!target) return; + onUpdate?.(target.textContent || ''); + }} + className={cn( + `font-bold mb-2 py-4 ${sizeClasses[level as keyof typeof sizeClasses]} ${alignClass}`, + 'focus:outline-none rounded-md', + styles?.italic && 'italic', + styles?.underline && 'underline' + )} + > + {children} ++ ); + } + return (void + isEditing?: boolean + metadata?: { + alt?: string + caption?: string + alignment?: 'left' | 'center' | 'right' + size?: 'small' | 'medium' | 'large' | 'full' + } } -export function Image({ id, src, alt, caption, size = 'medium', position = 'center', align = 'left', styles }: ImageBlockProps) { - const sizeClasses = { - small: 'w-1/3', - medium: 'w-2/3', - large: 'w-5/6', - full: 'w-full', - }; +export function ImageBlock({ content, id, onUpdate, isEditing, metadata }: ImageBlockProps) { + const [isHovered, setIsHovered] = useState(false) - const positionClasses = { - left: 'mr-auto', - center: '', // Remove mx-auto for center - right: 'ml-auto', - }; + const handleFileUpload = async (e: React.ChangeEvent ) => { + e.stopPropagation() + const file = e.target.files?.[0] + if (!file) return - const alignClass = align === 'center' ? 'mx-auto' : align === 'right' ? 'ml-auto' : ''; + const url = URL.createObjectURL(file) + + const imageContent = JSON.stringify({ + url, + alt: metadata?.alt || file.name, + caption: metadata?.caption, + alignment: metadata?.alignment || 'center', + size: metadata?.size || 'medium' + }) - return ( - - { + try { + // Try parsing as JSON first + return typeof content === 'string' ? JSON.parse(content) : content + } catch { + // If parsing fails, assume it's a direct URL string + return { + url: content, + alt: metadata?.alt || '', + caption: metadata?.caption || '', + alignment: metadata?.alignment || 'center', + size: metadata?.size || 'medium' + } + } + } + + const UploadButton = () => ( ++ - {caption && ( -+ ) + + if (!content && isEditing) { + return ( +- {caption} + + ++++ ) + } + + const imageContent = parseImageContent(content) + + return ( + - ); + ) } diff --git a/packages/akiradocs/src/components/blocks/ListBlock.tsx b/packages/akiradocs/src/components/blocks/ListBlock.tsx index 2f4b719..1e337d3 100644 --- a/packages/akiradocs/src/components/blocks/ListBlock.tsx +++ b/packages/akiradocs/src/components/blocks/ListBlock.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { cn } from "@/lib/utils"; interface ListProps { id?: string; - items: string[]; + content: string; listType?: 'ordered' | 'unordered'; align?: 'left' | 'center' | 'right'; styles?: { @@ -11,23 +11,230 @@ interface ListProps { italic?: boolean; underline?: boolean; }; + isEditing?: boolean; + onUpdate?: (content: string) => void; } -export function List({ id, items, listType = 'unordered', align = 'left', styles }: ListProps) { - const Tag = listType === 'ordered' ? 'ol' : 'ul'; - const listStyle = listType === 'ordered' ? 'list-decimal' : 'list-disc'; - const alignClass = align === 'center' ? 'mx-auto' : align === 'right' ? 'ml-auto' : ''; +export function List({ + id, + content, + listType = 'unordered', + align = 'left', + styles, + isEditing, + onUpdate +}: ListProps) { + const [isActive, setIsActive] = useState(false); + const textareaRef = useRef+ (null); + const containerRef = useRef (null); + + // Handle click to edit + const handleClick = () => { + if (isEditing) { + setIsActive(true); + } + }; + + const handleBlur = (e: React.FocusEvent) => { + // Only blur if clicking outside the component + if (!containerRef.current?.contains(e.relatedTarget as Node)) { + setIsActive(false); + } + }; + + // Parse content if it's accidentally stringified + const parseContent = (rawContent: string): string => { + try { + // If content is a stringified array, parse it and join with newlines + if (rawContent.startsWith('[')) { + return JSON.parse(rawContent).join('\n'); + } + return rawContent; + } catch { + return rawContent; + } + }; + + const normalizedContent = parseContent(content); + + // Auto-adjust height function + const adjustTextareaHeight = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = '0'; // Reset height + textarea.style.height = `${textarea.scrollHeight}px`; + } + }; + + // Determine if we should show the editing view + const showEditingView = isEditing && isActive; + + // Adjust height on content change + useEffect(() => { + adjustTextareaHeight(); + }, [normalizedContent]); + + // Adjust height when switching to edit mode + useEffect(() => { + if (showEditingView) { + adjustTextareaHeight(); + } + }, [showEditingView]); + + // Format content for editing view + const getEditableContent = (rawContent: string): string => { + const items = rawContent.split('\n'); + return items + .map((item, index) => + listType === 'ordered' + ? `${index + 1}. ${item}` + : `• ${item}` + ) + .join('\n'); + }; + + // Parse content back to normal format when saving + const parseEditableContent = (editableContent: string): string => { + return editableContent + .split('\n') + .map(line => line.replace(/^[\d]+\.\s+/, '').replace(/^[•]\s+/, '')) + .join('\n'); + }; + + // Handle key presses for new lines and backspace + const handleKeyDown = (e: React.KeyboardEvent ) => { + const textarea = e.currentTarget; + const { selectionStart, selectionEnd } = textarea; + const currentContent = textarea.value; + const lines = currentContent.split('\n'); + + // Find which line we're on + let currentLineIndex = 0; + let charCount = 0; + for (let i = 0; i < lines.length; i++) { + charCount += lines[i].length + 1; // +1 for newline + if (charCount > selectionStart) { + currentLineIndex = i; + break; + } + } + + if (e.key === 'Enter') { + e.preventDefault(); + // Insert new line with proper prefix + const prefix = listType === 'ordered' ? `${currentLineIndex + 2}. ` : '• '; + const newContent = [ + ...lines.slice(0, currentLineIndex + 1), + prefix, + ...lines.slice(currentLineIndex + 1) + ].join('\n'); + + onUpdate?.(parseEditableContent(newContent)); + } else if (e.key === 'Backspace') { + const currentLine = lines[currentLineIndex]; + const prefix = listType === 'ordered' ? `${currentLineIndex + 1}. ` : '• '; + + // If we're at the start of a line's content (right after the prefix) + if (currentLine === prefix || currentLine === prefix.trim()) { + e.preventDefault(); + + // Remove the current line and update content + const newContent = [ + ...lines.slice(0, currentLineIndex), + ...lines.slice(currentLineIndex + 1) + ] + .map((line, index) => { + // Reformat ordered list numbers if needed + if (listType === 'ordered') { + return line.replace(/^\d+\./, `${index + 1}.`); + } + return line; + }) + .join('\n'); + + onUpdate?.(parseEditableContent(newContent)); + } + } + }; + + // Modified content change handler + const handleContentChange = (e: React.ChangeEvent ) => { + const rawContent = parseEditableContent(e.target.value); + onUpdate?.(rawContent); + }; + + // Determine alignment classes + const alignmentClasses = { + left: 'text-left', + center: 'text-center mx-auto', + right: 'text-right ml-auto' + }[align]; + + // Style classes based on props + const textStyles = cn( + styles?.bold && 'font-bold', + styles?.italic && 'italic', + styles?.underline && 'underline' + ); + + if (showEditingView) { + return ( + + ++ ); + } + + // Split content into items and filter out empty lines + const items = normalizedContent + .split('\n') + .filter(item => item.trim() !== ''); + + const ListComponent = listType === 'ordered' ? 'ol' : 'ul'; + const listStyleClass = listType === 'ordered' ? 'list-decimal' : 'list-disc'; return ( -- {items.map((item, index) => ( - +{item} - ))} -+); } diff --git a/packages/akiradocs/src/components/blocks/ParagraphBlock.tsx b/packages/akiradocs/src/components/blocks/ParagraphBlock.tsx index 7e3889e..ce6b959 100644 --- a/packages/akiradocs/src/components/blocks/ParagraphBlock.tsx +++ b/packages/akiradocs/src/components/blocks/ParagraphBlock.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { cn } from "@/lib/utils"; interface ParagraphProps { @@ -10,19 +10,80 @@ interface ParagraphProps { italic?: boolean; underline?: boolean; }; + isEditing?: boolean; + onUpdate?: (content: string) => void; } -export function Paragraph({ id, children, align = 'left', styles }: ParagraphProps) { +export function Paragraph({ + id, + children, + align = 'left', + styles = {}, + isEditing, + onUpdate +}: ParagraphProps) { + const inputRef = useRef+ {items.map((item, index) => ( + ++ {item} + + ))} +(null); + + if (isEditing) { + return ( + { + const target = e.target as HTMLElement; + if (!target) return; + + // Preserve newlines by getting the raw HTML and converting it properly + let content = target.innerHTML; + + // Normalize line breaks + content = content.replace(/
<\/div>/g, '\n') + content = content.replace(//g, '\n') + content = content.replace(/<\/div>/g, '') + content = content.replace(/
/g, '\n') + content = content.replace(/
/g, '\n') + content = content.replace(/ /g, ' ') + + // Clean up double newlines + content = content.replace(/\n\n+/g, '\n\n') + content = content.trim() + + onUpdate?.(content); + }} + dangerouslySetInnerHTML={{ __html: String(children).replace(/\n/g, '
') }} + className={cn( + 'mb-6 text-base leading-relaxed py-1 whitespace-pre-wrap focus:outline-none rounded-md', + align === 'center' && 'text-center', + align === 'right' && 'text-right', + styles.bold && 'font-bold', + styles.italic && 'italic', + styles.underline && 'underline' + )} + /> + ); + } + + // Non-editing mode remains the same + const content = typeof children === 'string' + ? children.split('\n').map((line, i) => ( ++ {line} + {i < children.split('\n').length - 1 && + )) + : children; + return (
} +- {children} + {content}
); } diff --git a/packages/akiradocs/src/components/blocks/SortableBlock.tsx b/packages/akiradocs/src/components/blocks/SortableBlock.tsx index c11b2fa..9eb3de9 100644 --- a/packages/akiradocs/src/components/blocks/SortableBlock.tsx +++ b/packages/akiradocs/src/components/blocks/SortableBlock.tsx @@ -3,21 +3,39 @@ import { CSS } from '@dnd-kit/utilities' import { GripVertical } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' -import { BlockType } from '../../types/Block' +import { Block, BlockType } from '../../types/Block' import { AddBlockButton } from '../editor/AddBlockButton' import { BlockRenderer } from '@/lib/renderers/BlockRenderer' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { Plus, MoreHorizontal, Trash2, Upload } from 'lucide-react' +import { Plus, Trash2 } from 'lucide-react' import { useRef, useCallback, useState } from 'react' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { BlockFormatToolbar } from '../editor/BlockFormatToolbar' +import { toast } from 'sonner' +import { rewriteBlockContent } from '@/lib/aiRewrite' interface SortableBlockProps { block: { id: string type: BlockType content: string + metadata?: { + level?: number + styles?: { + bold?: boolean + italic?: boolean + underline?: boolean + } + language?: string + alt?: string + caption?: string + listType?: 'ordered' | 'unordered' + size?: 'small' | 'medium' | 'large' | 'full' + position?: 'left' | 'center' | 'right' + filename?: string + showLineNumbers?: boolean + align?: 'left' | 'center' | 'right' + type?: 'info' | 'warning' | 'success' | 'error' + title?: string + } } updateBlock: (id: string, content: string) => void changeBlockType: (id: string, newType: BlockType) => void @@ -26,6 +44,7 @@ interface SortableBlockProps { showPreview: boolean isChangeTypeActive: boolean setActiveChangeTypeId: (id: string | null) => void + updateBlockMetadata: (id: string, metadata: Partial) => void } interface ImageBlockContent { @@ -45,7 +64,8 @@ export function SortableBlock({ deleteBlock, showPreview, isChangeTypeActive, - setActiveChangeTypeId + setActiveChangeTypeId, + updateBlockMetadata }: SortableBlockProps) { const { attributes, @@ -161,20 +181,36 @@ export function SortableBlock({ updateBlock(block.id, JSON.stringify(updatedContent)) } - if (showPreview) { - return + const [isFocused, setIsFocused] = useState(false) + + const handleFocus = () => { + setIsFocused(true) + } + + const handleBlur = (e: React.FocusEvent) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + setIsFocused(false) + } } - return ( + const [isRewriting, setIsRewriting] = useState(false) + + return showPreview ? ( + + ) : ( -+ {/* Left Controls Container */} +{/* Drag Handle */}{/* Block Controls */} -Drag handle @@ -192,7 +228,7 @@ export function SortableBlock({ ++ {/* Main Content */} ++{ @@ -221,183 +257,139 @@ export function SortableBlock({ Add Block {/* Content Editor */} -) -} \ No newline at end of file +} diff --git a/packages/akiradocs/src/components/editor/AIRewriteButton.tsx b/packages/akiradocs/src/components/editor/AIRewriteButton.tsx new file mode 100644 index 0000000..c398ed7 --- /dev/null +++ b/packages/akiradocs/src/components/editor/AIRewriteButton.tsx @@ -0,0 +1,196 @@ +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Wand2 } from 'lucide-react' +import { useState } from 'react' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { BlockType } from '@/types/Block' + +const blockStyles = { + article: [ + { + value: 'professional', + label: 'Professional', + prompt: 'Rewrite this article title and subtitle to be clear and professional. Format as JSON with title and subtitle fields.' + }, + { + value: 'engaging', + label: 'Engaging', + prompt: 'Rewrite this article title and subtitle to be more engaging and attention-grabbing. Format as JSON with title and subtitle fields.' + } + ], + + heading: [ + { + value: 'concise', + label: 'Concise', + prompt: 'Rewrite this heading to be clear and impactful in 2-6 words. Output only the heading text.' + }, + { + value: 'descriptive', + label: 'Descriptive', + prompt: 'Rewrite this heading to be more descriptive while maintaining clarity. Keep under 10 words. Output only the heading text.' + } + ], + + paragraph: [ + { + value: 'professional', + label: 'Professional', + prompt: 'Rewrite this paragraph in a clear, professional tone. Keep it concise and direct. Output only the paragraph text.' + }, + { + value: 'casual', + label: 'Casual', + prompt: 'Rewrite this paragraph in a friendly, conversational tone. Make it engaging but clear. Output only the paragraph text.' + } + ], + + blockquote: [ + { + value: 'inspirational', + label: 'Inspirational', + prompt: 'Rewrite this quote to be more inspirational while keeping its core message. Output only the quote text without any formatting or quotation marks.' + }, + { + value: 'powerful', + label: 'Powerful', + prompt: 'Rewrite this quote to be more impactful and memorable. Output only the quote text without any formatting or quotation marks.' + } + ], + + list: [ + { + value: 'structured', + label: 'Structured', + prompt: 'Reorganize this list to be more structured and clear. Each item should be concise. Output only list items, one per line without bullets or numbers.' + }, + { + value: 'detailed', + label: 'Detailed', + prompt: 'Expand each list item with more detail while keeping clarity. Output only list items, one per line without bullets or numbers.' + } + ], + + code: [ + { + value: 'optimize', + label: 'Optimize', + prompt: 'Optimize this code for better performance and readability. Keep the exact same language and functionality. Do not change imports or dependencies. Output only the optimized code.' + }, + { + value: 'document', + label: 'Document', + prompt: 'Add clear comments explaining the code functionality. Keep the exact same code and language. Only add or modify comments. Output only the documented code.' + } + ], + + image: [ + { + value: 'descriptive', + label: 'Descriptive', + prompt: 'Rewrite only the alt text and caption to be more descriptive. Keep the image URL and other properties unchanged. Format as JSON with only alt and caption fields.' + }, + { + value: 'concise', + label: 'Concise', + prompt: 'Rewrite only the alt text and caption to be clear and concise. Keep the image URL and other properties unchanged. Format as JSON with only alt and caption fields.' + } + ], + + callout: [ + { + value: 'clear', + label: 'Clear', + prompt: 'Rewrite this callout to be clearer and more direct. Keep the callout type (info/warning/success/error) and title unchanged. Output only the callout content.' + }, + { + value: 'actionable', + label: 'Actionable', + prompt: 'Rewrite this callout with clear action items. Keep the callout type and title unchanged. Output only the callout content.' + } + ], + + divider: [ + { + value: 'default', + label: 'Default', + prompt: 'No rewriting options available for divider' + } + ], + + toggle: [ + { + value: 'clear', + label: 'Clear', + prompt: 'Rewrite the toggle content to be clearer and more organized. Keep the toggle structure. Output only the toggle content.' + }, + { + value: 'detailed', + label: 'Detailed', + prompt: 'Expand the toggle content with more detailed explanations. Keep the toggle structure. Output only the toggle content.' + } + ], + + apiReference: [ + { + value: 'default', + label: 'Default', + prompt: 'No rewriting options available for API reference' + } + ], +} as const; + +interface AIRewriteButtonProps { + onRewrite: (style: string) => Promise- { block.type === 'image' ? ( -- {getImageContent().url ? ( -- ) : block.type === 'divider' ? ( --- ) : ( - - )} -- - {getImageContent().caption && ( -- -- {getImageContent().caption} -
- )} ----- -- - updateImageMetadata({ alt: e.target.value })} - /> -- -- - updateImageMetadata({ caption: e.target.value })} - /> -- -- - -- -- - --- ---- ) : ( -
-{ - const target = e.target as HTMLElement - if (!target) return - - setTimeout(() => { - updateBlock(block.id, target.textContent || '') - }, 100) +- {/* Block Menu */} -+ {!showPreview && block.type !== 'divider' && ( ++ onListTypeChange={(listType) => { + updateBlockMetadata(block.id, { + ...block.metadata, + listType + }) + }} + onLanguageChange={(language) => { + updateBlockMetadata(block.id, { + ...block.metadata, + language + }) + }} + onFilenameChange={(filename) => { + updateBlockMetadata(block.id, { + ...block.metadata, + filename + }) + }} + onShowLineNumbersChange={(showLineNumbers) => { + updateBlockMetadata(block.id, { + ...block.metadata, + showLineNumbers + }) + }} + showImageControls={block.type === 'image'} + imageContent={block.type === 'image' ? getImageContent() : undefined} + onImageMetadataChange={(metadata) => { + const currentContent = getImageContent(); + const updatedContent = { + ...currentContent, + ...metadata + }; + updateBlock(block.id, JSON.stringify(updatedContent)); + }} + showCalloutControls={block.type === 'callout'} + calloutType={block.metadata?.type || 'info'} + calloutTitle={block.metadata?.title || ''} + onCalloutTypeChange={(type) => { + updateBlockMetadata(block.id, { + ...block.metadata, + type + }); + }} + onCalloutTitleChange={(title) => { + updateBlockMetadata(block.id, { + ...block.metadata, + title + }); + }} + blockType={block.type} + onAiRewrite={async (style) => { + setIsRewriting(true); + try { + const newContent = await rewriteBlockContent(block.content, block.type, style); + updateBlock(block.id, newContent); + toast.success('Content rewritten successfully'); + } catch (error) { + console.error('Error rewriting content:', error); + toast.error(error instanceof Error ? error.message : 'Failed to rewrite content'); + } finally { + setIsRewriting(false); + } + }} + isAiRewriting={isRewriting} + /> )} +{ + updateBlockMetadata(block.id, { + ...block.metadata, + styles + }) + }} + onAlignChange={(newAlign) => { + if (block.type === 'image') { + const currentContent = getImageContent(); + const updatedContent = { + ...currentContent, + alignment: newAlign, // For the image container alignment + position: newAlign, // For the image position within container + }; + updateBlock(block.id, JSON.stringify(updatedContent)); + } else { + updateBlockMetadata(block.id, { + ...block.metadata, + align: newAlign + }); + } }} - className={cn( - "w-full p-2 focus:outline-none border border-transparent focus:border-border rounded-md bg-gray-800", - block.type === 'heading' && "font-bold text-2xl", - block.type === 'code' && "font-mono bg-muted p-4" - )} - style={{ - display: 'block', - whiteSpace: block.type === 'code' ? 'pre-wrap' : 'normal' + onLevelChange={(level) => { + updateBlockMetadata(block.id, { + ...block.metadata, + level + }) }} - > - {block.content} - updateBlock(id, content)} + /> - + {/* Delete Button */} +- - -- -deleteBlock(block.id)}> - -- Delete block - + ++ blockType: BlockType + isRewriting?: boolean +} + +export function AIRewriteButton({ onRewrite, blockType, isRewriting }: AIRewriteButtonProps) { + const [style, setStyle] = useState (() => { + const styles = blockStyles[blockType] || blockStyles['paragraph'] + return styles[0].value + }) + + const getBlockStyles = (blockType: BlockType) => { + return blockStyles[blockType] || blockStyles['paragraph']; + }; + + return ( + + + ) +} \ No newline at end of file diff --git a/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx b/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx new file mode 100644 index 0000000..2bdbf3d --- /dev/null +++ b/packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx @@ -0,0 +1,299 @@ +import { Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, Wand2 } from 'lucide-react' +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' +import { Separator } from '@/components/ui/separator' +import { cn } from '@/lib/utils' +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select' +import { ListIcon, ListOrdered } from 'lucide-react' +import { Input } from '@/components/ui/input' +import { BlockType } from '@/types/Block' +import { AIRewriteButton } from '@/components/editor/AIRewriteButton' + +interface BlockFormatToolbarProps { + styles?: { + bold?: boolean + italic?: boolean + underline?: boolean + } + align?: 'left' | 'center' | 'right' + onStyleChange: (styles: { bold?: boolean; italic?: boolean; underline?: boolean }) => void + onAlignChange: (align: 'left' | 'center' | 'right') => void + className?: string + level?: number + onLevelChange?: (level: number) => void + showLevelSelect?: boolean + listType?: 'ordered' | 'unordered' + onListTypeChange?: (listType: 'ordered' | 'unordered') => void + showListControls?: boolean + language?: string + filename?: string + showLineNumbers?: boolean + onLanguageChange?: (language: string) => void + onFilenameChange?: (filename: string) => void + onShowLineNumbersChange?: (show: boolean) => void + showCodeControls?: boolean + showImageControls?: boolean; + imageContent?: { + url: string; + alt?: string; + caption?: string; + alignment?: 'left' | 'center' | 'right'; + size?: 'small' | 'medium' | 'large' | 'full'; + }; + onImageMetadataChange?: (metadata: Partial<{ + alt: string; + caption: string; + alignment: 'left' | 'center' | 'right'; + size: 'small' | 'medium' | 'large' | 'full'; + }>) => void; + showCalloutControls?: boolean; + calloutType?: 'info' | 'warning' | 'success' | 'error'; + calloutTitle?: string; + onCalloutTypeChange?: (type: 'info' | 'warning' | 'success' | 'error') => void; + onCalloutTitleChange?: (title: string) => void; + isVisible?: boolean; + onAiRewrite?: (style: string) => Promise+ + ++ +++Rewrite with AI
+ + ++ isAiRewriting?: boolean + blockType?: BlockType +} + +export function BlockFormatToolbar({ + styles = { + bold: false, + italic: false, + underline: false + }, + align = 'left', + onStyleChange, + onAlignChange, + className, + level = 1, + onLevelChange, + showLevelSelect = false, + listType = 'unordered', + onListTypeChange, + showListControls = false, + language = 'typescript', + filename, + showLineNumbers = true, + onLanguageChange, + onFilenameChange, + onShowLineNumbersChange, + showCodeControls = false, + showImageControls = false, + imageContent, + onImageMetadataChange, + showCalloutControls = false, + calloutType = 'info', + calloutTitle = '', + onCalloutTypeChange, + onCalloutTitleChange, + isVisible = false, + onAiRewrite, + isAiRewriting, + blockType, +}: BlockFormatToolbarProps) { + return ( + + {!showImageControls && ( + <> ++ ); +} \ No newline at end of file diff --git a/packages/akiradocs/src/components/layout/TitleBar.tsx b/packages/akiradocs/src/components/layout/TitleBar.tsx index 191396c..6e0b658 100644 --- a/packages/akiradocs/src/components/layout/TitleBar.tsx +++ b/packages/akiradocs/src/components/layout/TitleBar.tsx @@ -1,34 +1,39 @@ 'use client' -// import Link from 'next/link' -import { Save } from 'lucide-react' +import { ArrowLeft, Save } from 'lucide-react' import { Button } from "@/components/ui/button" +import Link from "next/link" +import { usePathname } from 'next/navigation' interface TitleBarProps { - showPreview: boolean - setShowPreview: (show: boolean) => void onSave: () => void isSaving?: boolean } -export function TitleBar({ showPreview, setShowPreview, onSave, isSaving = false }: TitleBarProps) { +export function TitleBar({ onSave, isSaving = false }: TitleBarProps) { + const pathname = usePathname() + + // Extract filename from URL, remove .json extension, and capitalize + const filename = pathname + ?.split('/') + .pop() + ?.replace('.json', '') + ?.split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') || 'Untitled' return (value).map(([key]) => key)} + onValueChange={(values) => { + onStyleChange({ + bold: values.includes('bold'), + italic: values.includes('italic'), + underline: values.includes('underline') + }) + }} + className="flex gap-0.5" + > + ++ ++ + ++ + ++ + > + )} + + value && onAlignChange(value as 'left' | 'center' | 'right')} className="flex gap-0.5"> + + + {showCodeControls && ( + <> ++ ++ + ++ + ++ + + onLanguageChange?.(e.target.value)} + placeholder="Language" + className="h-7 w-24 text-xs" + /> + + onFilenameChange?.(e.target.value)} + placeholder="Filename" + className="h-7 w-32 text-xs" + /> + + onShowLineNumbersChange?.(value === 'show')} className="flex gap-0.5"> + + > + )} + + {showLevelSelect && onLevelChange && ( + <> ++ ++ + + + > + )} + + {showListControls && onListTypeChange && ( + <> + + + onListTypeChange?.(value as 'ordered' | 'unordered')} + className="flex gap-0.5" + > + + > + )} + + {showImageControls && ( + <> ++ ++ + ++ + + onImageMetadataChange?.({ alt: e.target.value })} + placeholder="Alt text" + className="h-7 w-24 text-xs" + /> + + onImageMetadataChange?.({ caption: e.target.value })} + placeholder="Caption" + className="h-7 w-32 text-xs" + /> + + + > + )} + + {showCalloutControls && ( + <> + + + + + onCalloutTitleChange?.(e.target.value)} + placeholder="Title" + className="h-7 w-32 text-xs" + /> + > + )} + + + {/* Only show AI rewrite button if not an image block */} + {!showImageControls && ( + ++ )} +{})} + isRewriting={isAiRewriting} + /> + -- - ++ + + ++{filename}