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
- - {process.env.NEXT_PUBLIC_AKIRADOCS_EDIT_MODE === 'true' && ( - - )} - - {post.title} - {post.description} - {post.blocks.map((block) => ( - block.content !== post.title && ( - - ) - ))} - +
+ +
+ {process.env.NEXT_PUBLIC_AKIRADOCS_EDIT_MODE === 'true' && ( + + )} + +
+ {post.title} + {post.description} + {post.blocks.map((block) => ( + block.content !== post.title && ( + + ) + ))} + +
diff --git a/docs/src/components/tts/TextToSpeech.tsx b/docs/src/components/tts/TextToSpeech.tsx new file mode 100644 index 0000000..aef01d1 --- /dev/null +++ b/docs/src/components/tts/TextToSpeech.tsx @@ -0,0 +1,210 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Volume2, VolumeX, Settings2 } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +interface TextToSpeechProps { + blocks: any[]; +} + +// Define fixed voice options with clearer voices +const VOICE_OPTIONS = [ + { name: 'Samantha', lang: 'en-US' }, // Clear, natural female voice + { name: 'Daniel', lang: 'en-GB' }, // Clear male voice + { name: 'Karen', lang: 'en-AU' }, // Clear Australian female voice + { name: 'Moira', lang: 'en-IE' } // Clear Irish female voice +]; + +export function TextToSpeech({ blocks }: TextToSpeechProps) { + const [mounted, setMounted] = useState(false); + const [speaking, setSpeaking] = useState(false); + const [selectedVoiceIndex, setSelectedVoiceIndex] = useState(0); + + useEffect(() => { + setMounted(true); + }, []); + + const getVoice = useCallback(() => { + if (typeof window === 'undefined') return null; + const voices = window.speechSynthesis.getVoices(); + const preferredVoice = voices.find(voice => + voice.name.includes(VOICE_OPTIONS[selectedVoiceIndex].name) + ); + return preferredVoice || voices[0]; + }, [selectedVoiceIndex]); + + const generateSpeechText = (blocks: any[]) => { + return blocks + .filter(block => block.content !== undefined) + .map(block => { + try { + switch (block.type) { + case 'heading': + return `${block.content}. `; + + case 'paragraph': + return `${block.content} `; + + case 'list': + const listItems = Array.isArray(block.content) ? block.content : block.content.split('\n').filter((item: string) => item.trim()); + return `${listItems.join(', ')}`; + + case 'checkList': + if (block.metadata?.checkedItems) { + return `Checklist: ${block.metadata.checkedItems.map((item: { text: string; checked: boolean }) => + `${item.text} (${item.checked ? 'checked' : 'unchecked'})` + ).join(', ')}`; + } + return ''; + + case 'toggleList': + if (block.metadata?.items) { + return `Toggle list: ${block.metadata.items.map((item: { title: string; content: string }) => + `${item.title}: ${item.content}` + ).join('. ')}`; + } + return ''; + + case 'blockquote': + return `Quote: ${block.content}. `; + + case 'code': + const lang = block.metadata?.language || 'code'; + const filename = block.metadata?.filename ? ` in file ${block.metadata.filename}` : ''; + return `Code block${filename} in ${lang}. `; + + case 'table': + try { + const tableData = typeof block.content === 'string' + ? JSON.parse(block.content) + : block.content; + return `Table with ${tableData.rows?.length || 0} rows and ${tableData.columns?.length || 0} columns. `; + } catch { + return 'Table content. '; + } + + case 'image': + try { + const imageContent = typeof block.content === 'string' + ? JSON.parse(block.content) + : block.content; + const caption = imageContent.caption ? `: ${imageContent.caption}` : ''; + const alt = imageContent.alt ? ` showing ${imageContent.alt}` : ''; + return `Image ${alt} ${caption}. `; + } catch { + return 'Image. '; + } + + case 'video': + const videoCaption = block.metadata?.caption ? `: ${block.metadata.caption}` : ''; + return `Video for ${videoCaption}. `; + + case 'audio': + const audioCaption = block.metadata?.caption ? `: ${block.metadata.caption}` : ''; + return `Audio for ${audioCaption}. `; + + case 'file': + const fileName = block.metadata?.filename || 'file'; + return `Downloadable file called ${fileName}. `; + + case 'emoji': + return ''; + + case 'callout': + const type = block.metadata?.type || 'info'; + const title = block.metadata?.title ? `, titled ${block.metadata.title}` : ''; + return `${type} callout${title}: ${block.content}. `; + + case 'divider': + return ''; + + default: + return ''; + } + } catch (error) { + console.error(`Error processing block of type ${block.type}:`, error); + return ''; + } + }) + .join(' '); + }; + + const speak = useCallback(() => { + if (typeof window === 'undefined') return; + + window.speechSynthesis.cancel(); + const text = generateSpeechText(blocks); + const utterance = new SpeechSynthesisUtterance(text); + + const voice = getVoice(); + if (voice) utterance.voice = voice; + + utterance.rate = 0.9; + utterance.pitch = 1; + utterance.volume = 1; + + utterance.onstart = () => setSpeaking(true); + utterance.onend = () => setSpeaking(false); + utterance.onerror = () => setSpeaking(false); + + window.speechSynthesis.speak(utterance); + }, [blocks, getVoice]); + + const stop = useCallback(() => { + if (typeof window !== 'undefined') { + window.speechSynthesis.cancel(); + setSpeaking(false); + } + }, []); + + if (!mounted) return null; + + return ( +
+ + + + + + {VOICE_OPTIONS.map((voice, index) => ( + setSelectedVoiceIndex(index)} + className={selectedVoiceIndex === index ? "bg-accent" : ""} + > + {voice.name} + + ))} + + + + +
+ ); +} \ No newline at end of file diff --git a/packages/akiradocs/package-lock.json b/packages/akiradocs/package-lock.json index 3ce768e..0c5b59b 100644 --- a/packages/akiradocs/package-lock.json +++ b/packages/akiradocs/package-lock.json @@ -15,8 +15,8 @@ "@heroicons/react": "^2.1.5", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@mlc-ai/web-llm": "^0.2.76", "@million/lint": "^1.0.12", + "@mlc-ai/web-llm": "^0.2.76", "@next/mdx": "^15.0.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-avatar": "^1.1.1", @@ -32,6 +32,8 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.3", "ai": "^4.0.2", "class-variance-authority": "^0.7.0", @@ -1449,10 +1451,9 @@ } }, "node_modules/@mlc-ai/web-llm": { - "version": "0.2.75", - "resolved": "https://registry.npmjs.org/@mlc-ai/web-llm/-/web-llm-0.2.75.tgz", - "integrity": "sha512-U3ytE38mzIR/mDGwEl3nbutoIRFyPrsKAwu4A7N8rmHxLtb+gIEu4pfVPQb8uhvrDTsJz2L0zuCE5vRdIf1DUQ==", - "license": "Apache-2.0", + "version": "0.2.76", + "resolved": "https://registry.npmjs.org/@mlc-ai/web-llm/-/web-llm-0.2.76.tgz", + "integrity": "sha512-iUQXiGSCjfaiG8UDE2ZlH61DTGABXKTduiZecIMAXP9XV4LzFEeNRL+76QXstaYkBLvJvgfmKzO/ImJgdJe9zw==", "dependencies": { "loglevel": "^1.9.1" } @@ -1745,100 +1746,6 @@ "node": ">=10" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mdx-js/loader": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-3.1.0.tgz", - "integrity": "sha512-xU/lwKdOyfXtQGqn3VnJjlDrmKXEvMi1mgYxVmukEUtVycIz1nh7oQ40bKTd4cA7rLStqu0740pnhGYxGoqsCg==", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "webpack": ">=5" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", - "integrity": "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", - "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@mlc-ai/web-llm": { - "version": "0.2.76", - "resolved": "https://registry.npmjs.org/@mlc-ai/web-llm/-/web-llm-0.2.76.tgz", - "integrity": "sha512-iUQXiGSCjfaiG8UDE2ZlH61DTGABXKTduiZecIMAXP9XV4LzFEeNRL+76QXstaYkBLvJvgfmKzO/ImJgdJe9zw==", - "dependencies": { - "loglevel": "^1.9.1" - } - }, "node_modules/@next/env": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.3.tgz", @@ -2971,6 +2878,72 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", + "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", + "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-toggle": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", diff --git a/packages/akiradocs/package.json b/packages/akiradocs/package.json index 9d4b8a3..2f4677e 100644 --- a/packages/akiradocs/package.json +++ b/packages/akiradocs/package.json @@ -19,8 +19,8 @@ "@heroicons/react": "^2.1.5", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@mlc-ai/web-llm": "^0.2.76", "@million/lint": "^1.0.12", + "@mlc-ai/web-llm": "^0.2.76", "@next/mdx": "^15.0.1", "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-avatar": "^1.1.1", @@ -36,6 +36,8 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.3", "ai": "^4.0.2", "class-variance-authority": "^0.7.0", diff --git a/packages/akiradocs/src/app/[locale]/[type]/[...slug]/page.tsx b/packages/akiradocs/src/app/[locale]/[type]/[...slug]/page.tsx index 5d0fa0d..15a5933 100644 --- a/packages/akiradocs/src/app/[locale]/[type]/[...slug]/page.tsx +++ b/packages/akiradocs/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
- - {process.env.NEXT_PUBLIC_AKIRADOCS_EDIT_MODE === 'true' && ( - - )} - - {post.title} - {post.description} - {post.blocks.map((block) => ( - block.content !== post.title && ( - - ) - ))} - +
+ +
+ {process.env.NEXT_PUBLIC_AKIRADOCS_EDIT_MODE === 'true' && ( + + )} + +
+ {post.title} + {post.description} + {post.blocks.map((block) => ( + block.content !== post.title && ( + + ) + ))} + +
diff --git a/packages/akiradocs/src/components/blocks/BlockquoteBlock.tsx b/packages/akiradocs/src/components/blocks/BlockquoteBlock.tsx index 395ac10..4873b24 100644 --- a/packages/akiradocs/src/components/blocks/BlockquoteBlock.tsx +++ b/packages/akiradocs/src/components/blocks/BlockquoteBlock.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { cn } from "@/lib/utils"; interface BlockquoteProps { @@ -10,17 +10,64 @@ interface BlockquoteProps { italic?: boolean; underline?: boolean; }; + isEditing?: boolean; + onUpdate?: (content: string) => void; } -export function Blockquote({ id, children, align = 'left', styles }: BlockquoteProps) { +export function Blockquote({ id, children, align = 'left', styles, isEditing, onUpdate }: BlockquoteProps) { + const textareaRef = useRef(null); const alignClass = align === 'center' ? 'mx-auto' : align === 'right' ? 'ml-auto' : ''; + + const adjustHeight = () => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = '0px'; + textarea.style.height = `${textarea.scrollHeight}px`; + } + }; + + useEffect(() => { + if (isEditing) { + adjustHeight(); + } + }, [isEditing, children]); + + const commonStyles = cn( + styles?.bold && 'font-bold', + styles?.italic && 'italic', + styles?.underline && 'underline' + ); + + if (isEditing) { + return ( +
+